Skip to content

Commit 914883e

Browse files
feat: basic inert attribute support (#130)
1 parent 613e440 commit 914883e

File tree

6 files changed

+234
-45
lines changed

6 files changed

+234
-45
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@guidepup/virtual-screen-reader",
3-
"version": "0.29.1",
3+
"version": "0.30.0",
44
"description": "Virtual Screen Reader driver for unit test automation.",
55
"author": "Craig Morten <[email protected]>",
66
"license": "MIT",

src/createAccessibilityTree.ts

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getNodeAccessibilityData } from "./getNodeAccessibilityData/index";
44
import { getNodeByIdRef } from "./getNodeByIdRef";
55
import { isDialogRole } from "./isDialogRole";
66
import { isElement } from "./isElement";
7-
import { isInaccessible } from "dom-accessibility-api";
7+
import { isHiddenFromAccessibilityTree } from "./isHiddenFromAccessibilityTree";
88

99
export interface AccessibilityNode {
1010
accessibleAttributeLabels: string[];
@@ -15,6 +15,7 @@ export interface AccessibilityNode {
1515
allowedAccessibilityChildRoles: string[][];
1616
alternateReadingOrderParents: Node[];
1717
childrenPresentational: boolean;
18+
isInert: boolean;
1819
node: Node;
1920
parentAccessibilityNodeTree: AccessibilityNodeTree | null;
2021
parent: Node | null;
@@ -125,33 +126,6 @@ function getOwnedNodes(node: Node, container: Node) {
125126
return ownedNodes;
126127
}
127128

128-
const TEXT_NODE = 3;
129-
130-
function isHiddenFromAccessibilityTree(node: Node | null): node is null {
131-
if (!node) {
132-
return true;
133-
}
134-
135-
// `node.textContent` is only `null` for `document` and `doctype`.
136-
137-
if (node.nodeType === TEXT_NODE && node.textContent!.trim()) {
138-
return false;
139-
}
140-
141-
if (!isElement(node)) {
142-
return true;
143-
}
144-
145-
try {
146-
return isInaccessible(node);
147-
} catch {
148-
// Some elements aren't supported by DOM implementations such as JSDOM.
149-
// E.g. `<math>`, see https://github.com/jsdom/jsdom/issues/3515
150-
// We ignore these nodes at the moment as we can't support them.
151-
return true;
152-
}
153-
}
154-
155129
function growTree(
156130
node: Node,
157131
tree: Omit<
@@ -198,7 +172,7 @@ function growTree(
198172

199173
const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode)
200174
? // `alternateReadingOrderMap.has(childNode)` null guards here.
201-
175+
202176
Array.from(alternateReadingOrderMap.get(childNode)!)
203177
: [];
204178

@@ -209,14 +183,14 @@ function growTree(
209183
allowedAccessibilityChildRoles,
210184
childrenPresentational,
211185
isExplicitPresentational,
186+
isInert,
212187
role,
213188
spokenRole,
214189
} = getNodeAccessibilityData({
215190
allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles,
216-
alternateReadingOrderParents,
217-
container,
218-
node: childNode,
191+
inheritedImplicitInert: tree.isInert,
219192
inheritedImplicitPresentational: tree.childrenPresentational,
193+
node: childNode,
220194
});
221195

222196
const childTree = growTree(
@@ -229,6 +203,7 @@ function growTree(
229203
alternateReadingOrderParents,
230204
children: [],
231205
childrenPresentational,
206+
isInert,
232207
node: childNode,
233208
parentAccessibilityNodeTree: null, // Added during flattening
234209
parent: node,
@@ -266,7 +241,7 @@ function growTree(
266241

267242
const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode)
268243
? // `alternateReadingOrderMap.has(childNode)` null guards here.
269-
244+
270245
Array.from(alternateReadingOrderMap.get(childNode)!)
271246
: [];
272247

@@ -276,15 +251,15 @@ function growTree(
276251
accessibleValue,
277252
allowedAccessibilityChildRoles,
278253
childrenPresentational,
254+
isInert,
279255
isExplicitPresentational,
280256
role,
281257
spokenRole,
282258
} = getNodeAccessibilityData({
283259
allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles,
284-
alternateReadingOrderParents,
285-
container,
286-
node: childNode,
260+
inheritedImplicitInert: tree.isInert,
287261
inheritedImplicitPresentational: tree.childrenPresentational,
262+
node: childNode,
288263
});
289264

290265
const childTree = growTree(
@@ -297,6 +272,7 @@ function growTree(
297272
alternateReadingOrderParents,
298273
children: [],
299274
childrenPresentational,
275+
isInert,
300276
node: childNode,
301277
parentAccessibilityNodeTree: null, // Added during flattening
302278
parent: node,
@@ -334,14 +310,14 @@ export function createAccessibilityTree(
334310
accessibleValue,
335311
allowedAccessibilityChildRoles,
336312
childrenPresentational,
313+
isInert,
337314
role,
338315
spokenRole,
339316
} = getNodeAccessibilityData({
340317
allowedAccessibilityRoles: [],
341-
alternateReadingOrderParents: [],
342-
container: node,
343318
node,
344319
inheritedImplicitPresentational: false,
320+
inheritedImplicitInert: false,
345321
});
346322

347323
const tree = growTree(
@@ -354,6 +330,7 @@ export function createAccessibilityTree(
354330
alternateReadingOrderParents: [],
355331
children: [],
356332
childrenPresentational,
333+
isInert,
357334
node,
358335
parentAccessibilityNodeTree: null,
359336
parent: null,

src/flattenTree.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ export function flattenTree(
5757
};
5858

5959
const isAnnounced =
60-
!!treeNodeWithAttributeLabels.accessibleName ||
61-
!!treeNodeWithAttributeLabels.accessibleDescription ||
62-
treeNodeWithAttributeLabels.accessibleAttributeLabels.length > 0 ||
63-
!!treeNodeWithAttributeLabels.spokenRole;
60+
!treeNodeWithAttributeLabels.isInert &&
61+
(!!treeNodeWithAttributeLabels.accessibleName ||
62+
!!treeNodeWithAttributeLabels.accessibleDescription ||
63+
treeNodeWithAttributeLabels.accessibleAttributeLabels.length > 0 ||
64+
!!treeNodeWithAttributeLabels.spokenRole);
6465

6566
const ignoreChildren = shouldIgnoreChildren(tree);
6667

src/getNodeAccessibilityData/index.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getRole, presentationRoles, synonymRolesMap } from "./getRole";
33
import { getAccessibleDescription } from "./getAccessibleDescription";
44
import { getAccessibleName } from "./getAccessibleName";
55
import { getAccessibleValue } from "./getAccessibleValue";
6+
import { getLocalName } from "../getLocalName";
67
import { isElement } from "../isElement";
78

89
// TODO: swap out with the html-aria package once it supports `dpub-aam` /
@@ -62,14 +63,52 @@ const getSpokenRole = ({
6263
return role;
6364
};
6465

66+
/**
67+
* Nodes that are [inert](https://html.spec.whatwg.org/multipage/interaction.html#inert)
68+
* are not exposed to an accessibility API.
69+
*
70+
* Note: an inert node can have descendants that are not inert. For example,
71+
* a [modal dialog](https://html.spec.whatwg.org/multipage/interaction.html#modal-dialogs-and-inert-subtrees)
72+
* can escape an inert subtree.
73+
*
74+
* REF: https://www.w3.org/TR/html-aam-1.0/#att-inert
75+
*/
76+
const getIsInert = ({
77+
inheritedImplicitInert,
78+
node,
79+
role,
80+
}: {
81+
inheritedImplicitInert: boolean;
82+
node: Node;
83+
role: string;
84+
}) => {
85+
if (!isElement(node)) {
86+
return inheritedImplicitInert;
87+
}
88+
89+
// TODO: this doesn't cater to `<dialog>` elements which are model if opened
90+
// by `show()` vs `showModal()`.
91+
// REF: https://html.spec.whatwg.org/multipage/interaction.html#modal-dialogs-and-inert-subtrees
92+
const isNativeModalDialog =
93+
getLocalName(node) === "dialog" && node.hasAttribute("open");
94+
95+
const isNonNativeModalDialog =
96+
role === "dialog" && node.hasAttribute("aria-modal");
97+
98+
const isModalDialog = isNonNativeModalDialog || isNativeModalDialog;
99+
const isExplicitInert = node.hasAttribute("inert");
100+
101+
return isExplicitInert || (inheritedImplicitInert && !isModalDialog);
102+
};
103+
65104
export function getNodeAccessibilityData({
66105
allowedAccessibilityRoles,
106+
inheritedImplicitInert,
67107
inheritedImplicitPresentational,
68108
node,
69109
}: {
70110
allowedAccessibilityRoles: string[][];
71-
alternateReadingOrderParents: Node[];
72-
container: Node;
111+
inheritedImplicitInert: boolean;
73112
inheritedImplicitPresentational: boolean;
74113
node: Node;
75114
}) {
@@ -147,13 +186,20 @@ export function getNodeAccessibilityData({
147186
isChildrenPresentationalRole ||
148187
childrenInheritPresentationExceptAllowedRoles;
149188

189+
const isInert = getIsInert({
190+
inheritedImplicitInert,
191+
node,
192+
role,
193+
});
194+
150195
return {
151196
accessibleDescription: amendedAccessibleDescription,
152197
accessibleName,
153198
accessibleValue,
154199
allowedAccessibilityChildRoles,
155200
childrenPresentational,
156201
isExplicitPresentational,
202+
isInert,
157203
role,
158204
spokenRole,
159205
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { isElement } from "./isElement";
2+
3+
const TEXT_NODE = 3;
4+
5+
export function isHiddenFromAccessibilityTree(node: Node | null): node is null {
6+
if (!node) {
7+
return true;
8+
}
9+
10+
// `node.textContent` is only `null` for `document` and `doctype`.
11+
12+
if (node.nodeType === TEXT_NODE && node.textContent!.trim()) {
13+
return false;
14+
}
15+
16+
if (!isElement(node)) {
17+
return true;
18+
}
19+
20+
try {
21+
if (node.hidden === true) {
22+
return true;
23+
}
24+
25+
if (node.getAttribute("aria-hidden") === "true") {
26+
return true;
27+
}
28+
29+
const getComputedStyle = node.ownerDocument.defaultView?.getComputedStyle;
30+
const computedStyle = getComputedStyle?.(node);
31+
32+
if (
33+
computedStyle?.visibility === "hidden" ||
34+
computedStyle?.display === "none"
35+
) {
36+
return true;
37+
}
38+
} catch {
39+
// Some elements aren't supported by DOM implementations such as JSDOM.
40+
// E.g. `<math>`, see https://github.com/jsdom/jsdom/issues/3515
41+
// We ignore these nodes at the moment as we can't support them.
42+
return true;
43+
}
44+
45+
return false;
46+
}

0 commit comments

Comments
 (0)