Skip to content

Commit 25871a0

Browse files
feat: don't announce placeholder when have a value (#9)
1 parent 8cc767f commit 25871a0

File tree

9 files changed

+282
-14
lines changed

9 files changed

+282
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
],
2121
"scripts": {
2222
"build": "yarn clean && yarn compile",
23-
"ci": "yarn clean && yarn lint && yarn test && yarn build",
23+
"ci": "yarn clean && yarn lint && yarn test:coverage && yarn build",
2424
"clean": "rimraf lib",
2525
"compile": "tsc",
2626
"lint": "eslint . --ext .ts --cache",

src/Virtual.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,6 @@ export class Virtual implements ScreenReader {
180180

181181
this.#invalidateTreeCache();
182182
const tree = this.#getAccessibilityTree();
183-
184-
if (!tree.length) {
185-
return;
186-
}
187-
188183
const currentIndex = this.#getCurrentIndexByNode(tree);
189184
const newActiveNode = tree.at(currentIndex);
190185

src/getNodeAccessibilityData/getAccessibleAttributeLabels/getAttributesByRole.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { ARIAPropertyMap, ARIARoleDefinitionKey, roles } from "aria-query";
22
import { globalStatesAndProperties } from "../../getRole";
33

4-
export const getAttributesByRole = (role: string) => {
4+
const ignoreAttributesWithAccessibleValue = ["aria-placeholder"];
5+
6+
export const getAttributesByRole = ({
7+
accessibleValue,
8+
role,
9+
}: {
10+
accessibleValue: string;
11+
role: string;
12+
}) => {
513
const {
614
props: implicitRoleAttributes = {},
715
prohibitedProps: prohibitedAttributes = [],
@@ -15,7 +23,13 @@ export const getAttributesByRole = (role: string) => {
1523
...Object.keys(implicitRoleAttributes),
1624
...globalStatesAndProperties,
1725
])
18-
).filter((attribute) => !prohibitedAttributes.includes(attribute));
26+
)
27+
.filter((attribute) => !prohibitedAttributes.includes(attribute))
28+
.filter(
29+
(attribute) =>
30+
!accessibleValue ||
31+
!ignoreAttributesWithAccessibleValue.includes(attribute)
32+
);
1933

2034
return uniqueAttributes.map((attribute) => [
2135
attribute,

src/getNodeAccessibilityData/getAccessibleAttributeLabels/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { isElement } from "../../isElement";
66
import { mapAttributeNameAndValueToLabel } from "./mapAttributeNameAndValueToLabel";
77

88
export const getAccessibleAttributeLabels = ({
9+
accessibleValue,
910
node,
1011
role,
1112
}: {
13+
accessibleValue: string;
1214
node: Node;
1315
role: string;
1416
}): string[] => {
@@ -18,7 +20,7 @@ export const getAccessibleAttributeLabels = ({
1820
return labels;
1921
}
2022

21-
const attributes = getAttributesByRole(role);
23+
const attributes = getAttributesByRole({ accessibleValue, role });
2224

2325
attributes.forEach(([attributeName, implicitAttributeValue]) => {
2426
const labelFromHtmlEquivalentAttribute =

src/getNodeAccessibilityData/getAccessibleAttributeLabels/mapAttributeNameAndValueToLabel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ function token(tokenMap: Record<string, string>) {
135135

136136
function concat(propertyName: string) {
137137
return function mapper({ attributeValue }) {
138-
return `${propertyName} ${attributeValue}`;
138+
return attributeValue ? `${propertyName} ${attributeValue}` : "";
139139
};
140140
}
141141

src/getNodeAccessibilityData/getAccessibleValue.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isElement } from "../isElement";
22

3-
function getSelectValue(node) {
3+
function getSelectValue(node: HTMLSelectElement) {
44
const selectedOptions = [...node.options].filter((option) => option.selected);
55

66
if (node.multiple) {
@@ -14,7 +14,7 @@ function getSelectValue(node) {
1414
return selectedOptions[0].value;
1515
}
1616

17-
function getInputValue(node) {
17+
function getInputValue(node: HTMLInputElement) {
1818
if (["checkbox", "radio"].includes(node.type)) {
1919
return "";
2020
}
@@ -29,10 +29,10 @@ export function getAccessibleValue(node: Node) {
2929

3030
switch (node.localName) {
3131
case "input": {
32-
return getInputValue(node);
32+
return getInputValue(node as HTMLInputElement);
3333
}
3434
case "select": {
35-
return getSelectValue(node);
35+
return getSelectValue(node as HTMLSelectElement);
3636
}
3737
}
3838

src/getNodeAccessibilityData/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function getNodeAccessibilityData({
2626
});
2727

2828
const accessibleAttributeLabels = getAccessibleAttributeLabels({
29+
accessibleValue,
2930
node,
3031
role,
3132
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { virtual } from "../../src";
2+
3+
describe("Placeholder Attribute Property", () => {
4+
afterEach(() => {
5+
document.body.innerHTML = "";
6+
});
7+
8+
it("should announce a non-empty value on a text input", async () => {
9+
document.body.innerHTML = `
10+
<div id="label">Label</div>
11+
<input type="text" aria-labelledby="label" id="search1" value="a11y"/>
12+
`;
13+
14+
await virtual.start({ container: document.body });
15+
await virtual.next();
16+
await virtual.next();
17+
18+
expect(await virtual.lastSpokenPhrase()).toBe("textbox, Label, a11y");
19+
20+
await virtual.stop();
21+
});
22+
23+
it("should not announce an empty value on a text input", async () => {
24+
document.body.innerHTML = `
25+
<div id="label">Label</div>
26+
<input type="text" aria-labelledby="label" id="search1" value=""/>
27+
`;
28+
29+
await virtual.start({ container: document.body });
30+
await virtual.next();
31+
await virtual.next();
32+
33+
expect(await virtual.lastSpokenPhrase()).toBe("textbox, Label");
34+
35+
await virtual.stop();
36+
});
37+
38+
it("should not announce a missing value on a text input", async () => {
39+
document.body.innerHTML = `
40+
<div id="label">Label</div>
41+
<input type="text" aria-labelledby="label" id="search1"/>
42+
`;
43+
44+
await virtual.start({ container: document.body });
45+
await virtual.next();
46+
await virtual.next();
47+
48+
expect(await virtual.lastSpokenPhrase()).toBe("textbox, Label");
49+
50+
await virtual.stop();
51+
});
52+
53+
it("should not announce a value on a checkbox input", async () => {
54+
document.body.innerHTML = `
55+
<div id="label">Label</div>
56+
<input type="checkbox" aria-labelledby="label" id="check1" value="forbidden"/>
57+
`;
58+
59+
await virtual.start({ container: document.body });
60+
await virtual.next();
61+
await virtual.next();
62+
63+
expect(await virtual.lastSpokenPhrase()).toBe("checkbox, Label");
64+
65+
await virtual.stop();
66+
});
67+
68+
it("should not announce a value on a radio input", async () => {
69+
document.body.innerHTML = `
70+
<div id="label">Label</div>
71+
<input type="radio" aria-labelledby="label" id="radio1" value="forbidden"/>
72+
`;
73+
74+
await virtual.start({ container: document.body });
75+
await virtual.next();
76+
await virtual.next();
77+
78+
expect(await virtual.lastSpokenPhrase()).toBe("radio, Label");
79+
80+
await virtual.stop();
81+
});
82+
83+
it("should not announce a value on a radio input", async () => {
84+
document.body.innerHTML = `
85+
<div id="label">Label</div>
86+
<input type="radio" aria-labelledby="label" id="radio1" value="forbidden"/>
87+
`;
88+
89+
await virtual.start({ container: document.body });
90+
await virtual.next();
91+
await virtual.next();
92+
93+
expect(await virtual.lastSpokenPhrase()).toBe("radio, Label");
94+
95+
await virtual.stop();
96+
});
97+
98+
it("should announce the selected value in a select with options", async () => {
99+
document.body.innerHTML = `
100+
<select>
101+
<option value="first">First Value</option>
102+
<option value="second" selected>Second Value</option>
103+
<option value="third">Third Value</option>
104+
</select>
105+
`;
106+
107+
await virtual.start({ container: document.body });
108+
await virtual.next();
109+
110+
expect(await virtual.lastSpokenPhrase()).toBe(
111+
"combobox, second, not expanded, has popup listbox"
112+
);
113+
114+
await virtual.stop();
115+
});
116+
117+
it("should announce the multiple selected values in a multi-select with options", async () => {
118+
document.body.innerHTML = `
119+
<select multiple>
120+
<option value="first">First Value</option>
121+
<option value="second" selected>Second Value</option>
122+
<option value="third" selected>Third Value</option>
123+
</select>
124+
`;
125+
126+
await virtual.start({ container: document.body });
127+
await virtual.next();
128+
129+
expect(await virtual.lastSpokenPhrase()).toBe(
130+
"listbox, second; third, orientated vertically"
131+
);
132+
133+
await virtual.stop();
134+
});
135+
136+
it("should not announce empty selected values in a select with options", async () => {
137+
document.body.innerHTML = `
138+
<select>
139+
<option value="" disabled selected>- Select some value - </option>
140+
<option value="first">First Value</option>
141+
<option value="second">Second Value</option>
142+
<option value="third">Third Value</option>
143+
</select>
144+
`;
145+
146+
await virtual.start({ container: document.body });
147+
await virtual.next();
148+
149+
expect(await virtual.lastSpokenPhrase()).toBe(
150+
"combobox, not expanded, has popup listbox"
151+
);
152+
153+
await virtual.stop();
154+
});
155+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { virtual } from "../../src";
2+
3+
describe("Placeholder Attribute Property", () => {
4+
afterEach(() => {
5+
document.body.innerHTML = "";
6+
});
7+
8+
it("should announce a non-empty aria-placeholder attribute on an input when there is no value", async () => {
9+
document.body.innerHTML = `
10+
<div id="label">Label</div>
11+
<input type="text" aria-labelledby="label" id="search1" value="" aria-placeholder="Search..."/>
12+
`;
13+
14+
await virtual.start({ container: document.body });
15+
await virtual.next();
16+
await virtual.next();
17+
18+
expect(await virtual.lastSpokenPhrase()).toBe(
19+
"textbox, Label, placeholder Search..."
20+
);
21+
22+
await virtual.stop();
23+
});
24+
25+
it("should not announce an empty aria-placeholder attribute on an input when there is no value", async () => {
26+
document.body.innerHTML = `
27+
<div id="label">Label</div>
28+
<input type="text" aria-labelledby="label" id="search1" value="" aria-placeholder=""/>
29+
`;
30+
31+
await virtual.start({ container: document.body });
32+
await virtual.next();
33+
await virtual.next();
34+
35+
expect(await virtual.lastSpokenPhrase()).toBe("textbox, Label");
36+
37+
await virtual.stop();
38+
});
39+
40+
it("should announce an input value in preference to a non-empty aria-placeholder attribute", async () => {
41+
document.body.innerHTML = `
42+
<div id="label">Label</div>
43+
<input type="text" aria-labelledby="label" id="search1" value="a11y" aria-placeholder="Search..."/>
44+
`;
45+
46+
await virtual.start({ container: document.body });
47+
await virtual.next();
48+
await virtual.next();
49+
50+
expect(await virtual.lastSpokenPhrase()).toBe("textbox, Label, a11y");
51+
52+
await virtual.stop();
53+
});
54+
55+
it("should announce a non-empty placeholder attribute on an input when there is no value", async () => {
56+
document.body.innerHTML = `
57+
<div id="label">Label</div>
58+
<input type="text" aria-labelledby="label" id="search1" value="" placeholder="Search..."/>
59+
`;
60+
61+
await virtual.start({ container: document.body });
62+
await virtual.next();
63+
await virtual.next();
64+
65+
expect(await virtual.lastSpokenPhrase()).toBe(
66+
"textbox, Label, placeholder Search..."
67+
);
68+
69+
await virtual.stop();
70+
});
71+
72+
it("should not announce an empty placeholder attribute on an input when there is no value", async () => {
73+
document.body.innerHTML = `
74+
<div id="label">Label</div>
75+
<input type="text" aria-labelledby="label" id="search1" value="" placeholder=""/>
76+
`;
77+
78+
await virtual.start({ container: document.body });
79+
await virtual.next();
80+
await virtual.next();
81+
82+
expect(await virtual.lastSpokenPhrase()).toBe("textbox, Label");
83+
84+
await virtual.stop();
85+
});
86+
87+
it("should announce an input value in preference to a non-empty placeholder attribute", async () => {
88+
document.body.innerHTML = `
89+
<div id="label">Label</div>
90+
<input type="text" aria-labelledby="label" id="search1" value="a11y" placeholder="Search..."/>
91+
`;
92+
93+
await virtual.start({ container: document.body });
94+
await virtual.next();
95+
await virtual.next();
96+
97+
expect(await virtual.lastSpokenPhrase()).toBe("textbox, Label, a11y");
98+
99+
await virtual.stop();
100+
});
101+
});

0 commit comments

Comments
 (0)