Skip to content

Commit 6342610

Browse files
committed
feat: support (some) context based roles
1 parent 94574e6 commit 6342610

File tree

13 files changed

+135
-254
lines changed

13 files changed

+135
-254
lines changed

src/getLiveSpokenPhrase.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ enum Relevant {
4949
TEXT = "text",
5050
}
5151

52-
const RELEVANT_VALUES = Object.values(Relevant);
52+
const RELEVANT_VALUES = new Set(Object.values(Relevant));
5353
const DEFAULT_ATOMIC = false;
5454
const DEFAULT_LIVE = Live.OFF;
5555
const DEFAULT_RELEVANT = [Relevant.ADDITIONS, Relevant.TEXT];
@@ -60,7 +60,7 @@ function getSpokenPhraseForNode(node: Node) {
6060
getAccessibleValue(node) ||
6161
// `node.textContent` is only `null` if the `node` is a `document` or a
6262
// `doctype`. We don't consider either.
63-
63+
6464
sanitizeString(node.textContent!)
6565
);
6666
}
@@ -229,12 +229,12 @@ function getLiveRegionAttributes(
229229
if (typeof relevant === "undefined" && target.hasAttribute("aria-relevant")) {
230230
// The `target.hasAttribute("aria-relevant")` check is sufficient to guard
231231
// against the `target.getAttribute("aria-relevant")` being null.
232-
232+
233233
relevant = target
234234
.getAttribute("aria-relevant")!
235235
.split(" ")
236236
.filter(
237-
(token) => !!RELEVANT_VALUES.includes(token as Relevant)
237+
(token) => !!RELEVANT_VALUES.has(token as Relevant)
238238
) as Relevant[];
239239

240240
if (relevant.includes(Relevant.ALL)) {

src/getNodeAccessibilityData/getAccessibleAttributeLabels/getAttributesByRole.ts

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

4-
const ignoreAttributesWithAccessibleValue = ["aria-placeholder"];
4+
const ignoreAttributesWithAccessibleValue = new Set(["aria-placeholder"]);
55

66
export const getAttributesByRole = ({
77
accessibleValue,
@@ -27,8 +27,7 @@ export const getAttributesByRole = ({
2727
.filter((attribute) => !prohibitedAttributes.includes(attribute))
2828
.filter(
2929
(attribute) =>
30-
!accessibleValue ||
31-
!ignoreAttributesWithAccessibleValue.includes(attribute)
30+
!accessibleValue || !ignoreAttributesWithAccessibleValue.has(attribute)
3231
);
3332

3433
return uniqueAttributes.map((attribute) => [

src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const getNodeSet = ({
6363
});
6464
};
6565

66+
const levelItemRoles = new Set(["listitem", "treeitem"]);
67+
6668
type Mapper = ({
6769
node,
6870
tree,
@@ -112,7 +114,7 @@ const mapHtmlElementAriaToImplicitValue: Record<string, Mapper> = {
112114
});
113115
}
114116

115-
if (["listitem", "treeitem"].includes(role)) {
117+
if (levelItemRoles.has(role)) {
116118
return getLevelFromDocumentStructure({
117119
role,
118120
tree,

src/getNodeAccessibilityData/getAccessibleAttributeLabels/postProcessAriaValueNow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const percentageBasedValueRoles = ["progressbar", "scrollbar"];
1+
const percentageBasedValueRoles = new Set(["progressbar", "scrollbar"]);
22

33
const isNumberLike = (value: string) => {
44
return !isNaN(parseFloat(value));
@@ -24,7 +24,7 @@ export const postProcessAriaValueNow = ({
2424
role: string;
2525
value: string;
2626
}) => {
27-
if (!percentageBasedValueRoles.includes(role)) {
27+
if (!percentageBasedValueRoles.has(role)) {
2828
return value;
2929
}
3030

src/getNodeAccessibilityData/getAccessibleValue.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export type HTMLElementWithValue =
1111
| HTMLProgressElement
1212
| HTMLParamElement;
1313

14-
const ignoredInputTypes = ["checkbox", "radio"];
15-
const allowedLocalNames = [
14+
const ignoredInputTypes = new Set(["checkbox", "radio"]);
15+
const allowedLocalNames = new Set([
1616
"button",
1717
"data",
1818
"input",
@@ -21,7 +21,7 @@ const allowedLocalNames = [
2121
"option",
2222
"progress",
2323
"param",
24-
];
24+
]);
2525

2626
function getSelectValue(node: HTMLSelectElement) {
2727
const selectedOptions = [...node.options].filter(
@@ -42,7 +42,7 @@ function getSelectValue(node: HTMLSelectElement) {
4242
}
4343

4444
function getInputValue(node: HTMLInputElement) {
45-
if (ignoredInputTypes.includes(node.type)) {
45+
if (ignoredInputTypes.has(node.type)) {
4646
return "";
4747
}
4848

@@ -55,7 +55,7 @@ function getValue(node: HTMLElementWithValue) {
5555
// TODO: handle use of explicit roles where a value taken from content is
5656
// expected, e.g. combobox.
5757
// See core-aam/combobox-value-calculation-manual.html
58-
if (!allowedLocalNames.includes(localName)) {
58+
if (!allowedLocalNames.has(localName)) {
5959
return "";
6060
}
6161

src/getNodeAccessibilityData/getRole.ts

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type AncestorList,
23
getRole as getImplicitRole,
34
type TagName,
45
type VirtualElement,
@@ -7,14 +8,16 @@ import { getLocalName } from "../getLocalName";
78
import { isElement } from "../isElement";
89
import { roles } from "aria-query";
910

10-
export const presentationRoles = ["presentation", "none"];
11+
export const presentationRoles = new Set(["presentation", "none"]);
1112

12-
const allowedNonAbstractRoles = roles
13-
.entries()
14-
.filter(([, { abstract }]) => !abstract)
15-
.map(([key]) => key) as string[];
13+
const allowedNonAbstractRoles = new Set(
14+
roles
15+
.entries()
16+
.filter(([, { abstract }]) => !abstract)
17+
.map(([key]) => key) as string[]
18+
);
1619

17-
const rolesRequiringName = ["form", "region"];
20+
const rolesRequiringName = new Set(["form", "region"]);
1821

1922
export const globalStatesAndProperties = [
2023
"aria-atomic",
@@ -90,7 +93,7 @@ function getExplicitRole({
9093
*
9194
* REF: https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles
9295
*/
93-
.filter((role) => allowedNonAbstractRoles.includes(role))
96+
.filter((role) => allowedNonAbstractRoles.has(role))
9497
/**
9598
* Certain landmark roles require names from authors. In situations where
9699
* an author has not specified names for these landmarks, it is
@@ -105,7 +108,7 @@ function getExplicitRole({
105108
*
106109
* REF: https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles
107110
*/
108-
.filter((role) => !!accessibleName || !rolesRequiringName.includes(role));
111+
.filter((role) => !!accessibleName || !rolesRequiringName.has(role));
109112

110113
/**
111114
* If an allowed child element has an explicit non-presentational role, user
@@ -152,7 +155,7 @@ function getExplicitRole({
152155
* REF: https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none
153156
*/
154157
.filter((role) => {
155-
if (!presentationRoles.includes(role)) {
158+
if (!presentationRoles.has(role)) {
156159
return true;
157160
}
158161

@@ -182,6 +185,57 @@ function virtualizeElement(element: HTMLElement): VirtualElement {
182185
return { tagName, attributes };
183186
}
184187

188+
const rolesDependentOnHierarchy = new Set([
189+
"footer",
190+
"header",
191+
"li",
192+
"td",
193+
"th",
194+
"tr",
195+
]);
196+
const ignoredAncestors = new Set(["body", "document"]);
197+
198+
// TODO: Thought needed if the `getAncestors()` can limit the number of parents
199+
// it enumerates? Presumably as ancestors only matter for a limited number of
200+
// roles, there might be a ceiling to the amount of nesting that is even valid,
201+
// and therefore put an upper bound on how far to backtrack without having to
202+
// stop at the document level for every single element.
203+
//
204+
// Another thought is that we special case each element so the backtracking can
205+
// exit early if an ancestor with a relevant role has already been found.
206+
//
207+
// Alternatively see if providing an element that is part of a DOM can be
208+
// traversed by the `html-aria` library itself so these concerns are
209+
// centralised.
210+
function getAncestors(node: HTMLElement): AncestorList | undefined {
211+
if (!rolesDependentOnHierarchy.has(getLocalName(node))) {
212+
return undefined;
213+
}
214+
215+
const ancestors: AncestorList = [];
216+
217+
let target: HTMLElement | null = node;
218+
let targetLocalName: string;
219+
220+
while (true) {
221+
target = target.parentElement;
222+
223+
if (!target) {
224+
break;
225+
}
226+
227+
targetLocalName = getLocalName(target);
228+
229+
if (ignoredAncestors.has(targetLocalName)) {
230+
break;
231+
}
232+
233+
ancestors.push({ tagName: targetLocalName as TagName });
234+
}
235+
236+
return ancestors;
237+
}
238+
185239
export function getRole({
186240
accessibleName,
187241
allowedAccessibilityRoles,
@@ -222,21 +276,9 @@ export function getRole({
222276

223277
const baseImplicitRole = isBodyElement
224278
? "document"
225-
: // TODO: explore whether can supply ancestors without encountering
226-
// performance issues.
227-
//
228-
// `getImplicitRole(virtualizeElement(target, { ancestors: getAncestors(node) }));`
229-
//
230-
// Thought needed if the `getAncestors()` can limit the number of parents
231-
// it enumerates. Presumably as ancestors only matter for a limited
232-
// number of roles, there might be a ceiling to the amount of nesting
233-
// that is even valid, and therefore put an upper bound on how far to
234-
// backtrack without having to stop at the document level for every
235-
// single element.
236-
//
237-
// Alternatively see if providing an element that is part of a DOM can be
238-
// traversed by the `html-aria` library itself.
239-
getImplicitRole(virtualizeElement(target)) ?? "";
279+
: getImplicitRole(virtualizeElement(target), {
280+
ancestors: getAncestors(node),
281+
}) ?? "";
240282

241283
const implicitRole = mapAliasedRoles(baseImplicitRole);
242284

src/getNodeAccessibilityData/index.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { getAccessibleName } from "./getAccessibleName";
55
import { getAccessibleValue } from "./getAccessibleValue";
66
import { isElement } from "../isElement";
77

8-
const childrenPresentationalRoles = roles
9-
.entries()
10-
.filter(([, { childrenPresentational }]) => childrenPresentational)
11-
.map(([key]) => key) as string[];
8+
const childrenPresentationalRoles = new Set(
9+
roles
10+
.entries()
11+
.filter(([, { childrenPresentational }]) => childrenPresentational)
12+
.map(([key]) => key) as string[]
13+
);
1214

1315
const getSpokenRole = ({
1416
isGeneric,
@@ -72,8 +74,8 @@ export function getNodeAccessibilityData({
7274
const amendedAccessibleDescription =
7375
accessibleDescription === accessibleName ? "" : accessibleDescription;
7476

75-
const isExplicitPresentational = presentationRoles.includes(explicitRole);
76-
const isPresentational = presentationRoles.includes(role);
77+
const isExplicitPresentational = presentationRoles.has(explicitRole);
78+
const isPresentational = presentationRoles.has(role);
7779
const isGeneric = role === "generic";
7880

7981
const spokenRole = getSpokenRole({
@@ -103,8 +105,7 @@ export function getNodeAccessibilityData({
103105
*
104106
* REF: https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
105107
*/
106-
const isChildrenPresentationalRole =
107-
childrenPresentationalRoles.includes(role);
108+
const isChildrenPresentationalRole = childrenPresentationalRoles.has(role);
108109

109110
/**
110111
* When an explicit or inherited role of presentation is applied to an

src/isDialogRole.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export const isDialogRole = (role: string) =>
2-
["dialog", "alertdialog"].includes(role);
1+
const dialogRoles = new Set(["dialog", "alertdialog"]);
2+
3+
export const isDialogRole = (role: string) => dialogRoles.has(role);

test/int/multipleInstances.int.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,10 @@ describe("Multiple Instances", () => {
4040
"Section Text",
4141
"end of paragraph",
4242
"article",
43-
"banner",
4443
"heading, Article Header Heading 1, level 1",
4544
"paragraph",
4645
"Article Header Text",
4746
"end of paragraph",
48-
"end of banner",
4947
"paragraph",
5048
"Article Text",
5149
"end of paragraph",
@@ -68,12 +66,10 @@ describe("Multiple Instances", () => {
6866
"end of paragraph",
6967
"Article Text",
7068
"paragraph",
71-
"end of banner",
7269
"end of paragraph",
7370
"Article Header Text",
7471
"paragraph",
7572
"heading, Article Header Heading 1, level 1",
76-
"banner",
7773
"article",
7874
"end of paragraph",
7975
"Section Text",

test/int/next.int.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,11 @@ describe("next", () => {
3131
"Section Text",
3232
"",
3333
"",
34-
"",
3534
"Article Header Heading 1",
3635
"",
3736
"Article Header Text",
3837
"",
3938
"",
40-
"",
4139
"Article Text",
4240
"",
4341
"",
@@ -59,12 +57,10 @@ describe("next", () => {
5957
"Section Text",
6058
"end of paragraph",
6159
"article",
62-
"banner",
6360
"heading, Article Header Heading 1, level 1",
6461
"paragraph",
6562
"Article Header Text",
6663
"end of paragraph",
67-
"end of banner",
6864
"paragraph",
6965
"Article Text",
7066
"end of paragraph",

0 commit comments

Comments
 (0)