@markuplint/ml-spec 5.4.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +4 -0
- package/lib/algorithm/aria/accname/aria-steps.d.ts +12 -7
- package/lib/algorithm/aria/accname/aria-steps.js +16 -7
- package/lib/algorithm/aria/accname/compute.d.ts +3 -3
- package/lib/algorithm/aria/accname/compute.js +3 -3
- package/lib/algorithm/aria/accname/element-names.d.ts +2 -2
- package/lib/algorithm/aria/accname/element-names.js +29 -29
- package/lib/algorithm/aria/accname/helpers.d.ts +30 -9
- package/lib/algorithm/aria/accname/helpers.js +34 -12
- package/lib/algorithm/aria/accname/label-steps.d.ts +2 -2
- package/lib/algorithm/aria/accname/label-steps.js +2 -2
- package/lib/algorithm/aria/accname/svg-helpers.d.ts +2 -2
- package/lib/algorithm/aria/accname/svg-helpers.js +2 -2
- package/lib/algorithm/aria/accname/types.d.ts +12 -0
- package/lib/algorithm/aria/accname-computation.d.ts +1 -1
- package/lib/algorithm/aria/accname-computation.js +6 -6
- package/lib/algorithm/aria/get-aria.js +2 -2
- package/lib/algorithm/aria/get-computed-role.js +14 -9
- package/lib/algorithm/aria/get-explicit-role.d.ts +2 -2
- package/lib/algorithm/aria/get-explicit-role.js +11 -4
- package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +2 -0
- package/lib/algorithm/aria/get-non-presentational-ancestor.js +5 -2
- package/lib/algorithm/aria/is-exposed.d.ts +3 -1
- package/lib/algorithm/aria/is-exposed.js +21 -8
- package/lib/algorithm/aria/is-presentational.d.ts +2 -3
- package/lib/algorithm/aria/is-presentational.js +2 -3
- package/lib/algorithm/aria/matches-context-role.d.ts +4 -1
- package/lib/algorithm/aria/matches-context-role.js +11 -7
- package/lib/algorithm/dom/tree-boundary.d.ts +108 -0
- package/lib/algorithm/dom/tree-boundary.js +172 -0
- package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +7 -3
- package/lib/algorithm/html/content-model-category-to-tag-names.js +7 -3
- package/lib/algorithm/html/select-display-size.d.ts +29 -0
- package/lib/algorithm/html/select-display-size.js +38 -0
- package/lib/const/accname.d.ts +1 -1
- package/lib/const/accname.js +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/types/index.d.ts +63 -2
- package/lib/types/permitted-structures.d.ts +12 -1
- package/lib/utils/directive-resolver.d.ts +8 -0
- package/lib/utils/directive-resolver.js +1 -0
- package/lib/utils/schema-to-spec.d.ts +4 -0
- package/lib/utils/schema-to-spec.js +42 -0
- package/package.json +6 -8
|
@@ -31,7 +31,7 @@ const computingElements = new WeakSet();
|
|
|
31
31
|
* @param specs - The ML specification data for role resolution
|
|
32
32
|
* @param version - The ARIA version to use for role computation
|
|
33
33
|
* @returns The computed accessible name string, or an empty string if none is found
|
|
34
|
-
* @see https://
|
|
34
|
+
* @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2
|
|
35
35
|
*/
|
|
36
36
|
export function getAccname(
|
|
37
37
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -130,15 +130,15 @@ el, specs, version) {
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
133
|
+
* Finds the root node (Document or DocumentFragment) of the tree the element is in: for an
|
|
134
|
+
* element in the content of a `template` element or a shadow root, that content, not the
|
|
135
|
+
* document. A host that holds the content in the syntax tree under the `template` element (the
|
|
136
|
+
* markuplint DOM) answers it in `getRootNode()`, as the DOM does.
|
|
134
137
|
*/
|
|
135
138
|
function getRootNode(
|
|
136
139
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
137
140
|
el) {
|
|
138
|
-
|
|
139
|
-
while (root.parentNode) {
|
|
140
|
-
root = root.parentNode;
|
|
141
|
-
}
|
|
141
|
+
const root = el.getRootNode();
|
|
142
142
|
if (root.nodeType === 9 || root.nodeType === 11) {
|
|
143
143
|
return root;
|
|
144
144
|
}
|
|
@@ -89,14 +89,14 @@ function optimizePermittedRoles(permittedRoles, version) {
|
|
|
89
89
|
return permittedRoles;
|
|
90
90
|
}
|
|
91
91
|
const unique = new Set(permittedRoles);
|
|
92
|
-
// https://
|
|
92
|
+
// https://w3c.github.io/aria/#none
|
|
93
93
|
if (unique.has('presentation')) {
|
|
94
94
|
unique.add('none');
|
|
95
95
|
}
|
|
96
96
|
if (unique.has('none')) {
|
|
97
97
|
unique.add('presentation');
|
|
98
98
|
}
|
|
99
|
-
// https://w3c.github.io/aria/#
|
|
99
|
+
// https://w3c.github.io/aria/#image
|
|
100
100
|
// In ARIA 1.3, `image` is the primary role name and `img` is a synonym.
|
|
101
101
|
if (version === '1.3') {
|
|
102
102
|
if (unique.has('image')) {
|
|
@@ -3,6 +3,7 @@ import { isPresentational } from './is-presentational.js';
|
|
|
3
3
|
import { SVG_NAMESPACE } from '../../const/index.js';
|
|
4
4
|
import { hasSvgAccessibleNameSource } from './accname/svg-helpers.js';
|
|
5
5
|
import { getAttrSpecs } from '../../utils/get-attr-specs.js';
|
|
6
|
+
import { getAccessibilityParentElement, getComposedParentElement } from '../dom/tree-boundary.js';
|
|
6
7
|
import { getExplicitRole } from './get-explicit-role.js';
|
|
7
8
|
import { getImplicitRole } from './get-implicit-role.js';
|
|
8
9
|
import { getNonPresentationalAncestor } from './get-non-presentational-ancestor.js';
|
|
@@ -98,6 +99,7 @@ el, version, assumeSingleNode) {
|
|
|
98
99
|
* "accessibility child" and "accessibility parent", and
|
|
99
100
|
* `matchesContextRole` additionally skips `generic` elements.
|
|
100
101
|
*
|
|
102
|
+
* @see https://w3c.github.io/aria/#conflict_resolution_presentation_none
|
|
101
103
|
* @see https://w3c.github.io/aria/#scope
|
|
102
104
|
* @see https://github.com/w3c/aria/issues/1033
|
|
103
105
|
* @see https://github.com/w3c/aria/pull/1454
|
|
@@ -115,12 +117,14 @@ el, version, assumeSingleNode) {
|
|
|
115
117
|
!isNativeContextIntact(computedRole, el, specs, version)) {
|
|
116
118
|
/**
|
|
117
119
|
* An element fragment that serves as the root without a parent element
|
|
118
|
-
*
|
|
120
|
+
* (or the top of the content of an ordinary `template` element, whose
|
|
121
|
+
* placement is not known) cannot satisfy the "Required Accessibility
|
|
122
|
+
* Parent Role" condition.
|
|
119
123
|
* Therefore, under normal circumstances, the `role` will disappear.
|
|
120
124
|
* However, in this specific case, it will fall back to both explicit
|
|
121
125
|
* and implicit roles. Note that the explicit role takes precedence.
|
|
122
126
|
*/
|
|
123
|
-
if (el
|
|
127
|
+
if (getComposedParentElement(el) === null) {
|
|
124
128
|
return {
|
|
125
129
|
...computedRole,
|
|
126
130
|
errorType: 'NO_OWNER',
|
|
@@ -145,8 +149,7 @@ el, version, assumeSingleNode) {
|
|
|
145
149
|
/**
|
|
146
150
|
* SVG: Including Elements in the Accessibility Tree
|
|
147
151
|
*
|
|
148
|
-
* > Many SVG elements—although rendered to the screen—
|
|
149
|
-
* > do not have an intrinsic semantic meaning. Instead,
|
|
152
|
+
* > Many SVG elements—although rendered to the screen—do not have an intrinsic semantic meaning. Instead,
|
|
150
153
|
* > they represent components of the visual presentation of the document.
|
|
151
154
|
* > To simplify the accessible representation of the document,
|
|
152
155
|
* > these purely presentational elements should normally be omitted
|
|
@@ -166,11 +169,10 @@ el, version, assumeSingleNode) {
|
|
|
166
169
|
* > - the use element
|
|
167
170
|
* > - the grouping (g) element
|
|
168
171
|
* > - the image element
|
|
169
|
-
* > - the mesh element
|
|
170
172
|
* > - text formatting elements (textPath, tspan)
|
|
171
173
|
* > - the foreignObject element
|
|
172
174
|
*
|
|
173
|
-
* @see https://www.w3.org/TR/svg-aam
|
|
175
|
+
* @see https://www.w3.org/TR/svg-aam/#include_elements
|
|
174
176
|
*/
|
|
175
177
|
if (
|
|
176
178
|
// It doesn't been specified a valid explicit role.
|
|
@@ -189,10 +191,12 @@ el, version, assumeSingleNode) {
|
|
|
189
191
|
const implicitRole = lazyImplicitRole ?? getImplicitRole(specs, el, version);
|
|
190
192
|
/**
|
|
191
193
|
* > If an element is focusable,
|
|
192
|
-
* > user agents MUST ignore the presentation
|
|
194
|
+
* > user agents MUST ignore the none/presentation role
|
|
193
195
|
* > and expose the element with its implicit role,
|
|
194
196
|
* > in order to ensure that the element is operable.
|
|
195
197
|
*
|
|
198
|
+
* @see https://w3c.github.io/aria/#conflict_resolution_presentation_none
|
|
199
|
+
*
|
|
196
200
|
* With the issue: What does focusable or otherwise interactive mean](https://github.com/w3c/aria/issues/1192)
|
|
197
201
|
*
|
|
198
202
|
* - Focus: https://html.spec.whatwg.org/multipage/interaction.html#focus
|
|
@@ -286,6 +290,7 @@ el, specs, attrName) {
|
|
|
286
290
|
}
|
|
287
291
|
/**
|
|
288
292
|
* Determines whether some ancestors match the condition that the specified callback function.
|
|
293
|
+
* The ancestors are those of the accessibility tree: see {@link getComposedParentElement}.
|
|
289
294
|
*/
|
|
290
295
|
function someAncestors(
|
|
291
296
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -294,7 +299,7 @@ el, predicate) {
|
|
|
294
299
|
let current = el;
|
|
295
300
|
while (current) {
|
|
296
301
|
list.push(current);
|
|
297
|
-
current = current
|
|
302
|
+
current = getComposedParentElement(current);
|
|
298
303
|
}
|
|
299
304
|
return list.some(predicate);
|
|
300
305
|
}
|
|
@@ -320,7 +325,7 @@ el, specs, version) {
|
|
|
320
325
|
if (!computedRole.role?.isImplicit) {
|
|
321
326
|
return false;
|
|
322
327
|
}
|
|
323
|
-
const parent = el
|
|
328
|
+
const parent = getAccessibilityParentElement(el);
|
|
324
329
|
if (!parent) {
|
|
325
330
|
return false;
|
|
326
331
|
}
|
|
@@ -4,10 +4,10 @@ import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
|
|
|
4
4
|
*
|
|
5
5
|
* The roles that ARIA in HTML does not permit on the element are authoring
|
|
6
6
|
* errors that the user agent does not correct: such a role is returned with
|
|
7
|
-
* `errorType: 'NO_PERMITTED'` (Core-AAM
|
|
7
|
+
* `errorType: 'NO_PERMITTED'` (Core-AAM, the conflicts between the mappings), except on an element that is
|
|
8
8
|
* never rendered.
|
|
9
9
|
*
|
|
10
10
|
* @see https://w3c.github.io/aria/#document-handling_author-errors
|
|
11
|
-
* @see https://w3c.github.io/core-aam/#mapping_conflicts
|
|
11
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts — Core-AAM §3.2
|
|
12
12
|
*/
|
|
13
13
|
export declare function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeEntities } from '@markuplint/shared';
|
|
1
2
|
import { XHTML_NAMESPACE } from '../../const/index.js';
|
|
2
3
|
import { getRoleSpec } from './get-role-spec.js';
|
|
3
4
|
import { resolveNamespace } from '../../utils/resolve-namespace.js';
|
|
@@ -7,11 +8,11 @@ import { getPermittedRoles } from './get-permitted-roles.js';
|
|
|
7
8
|
*
|
|
8
9
|
* The roles that ARIA in HTML does not permit on the element are authoring
|
|
9
10
|
* errors that the user agent does not correct: such a role is returned with
|
|
10
|
-
* `errorType: 'NO_PERMITTED'` (Core-AAM
|
|
11
|
+
* `errorType: 'NO_PERMITTED'` (Core-AAM, the conflicts between the mappings), except on an element that is
|
|
11
12
|
* never rendered.
|
|
12
13
|
*
|
|
13
14
|
* @see https://w3c.github.io/aria/#document-handling_author-errors
|
|
14
|
-
* @see https://w3c.github.io/core-aam/#mapping_conflicts
|
|
15
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts — Core-AAM §3.2
|
|
15
16
|
*/
|
|
16
17
|
export function getExplicitRole(specs,
|
|
17
18
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -162,8 +163,14 @@ el, role) {
|
|
|
162
163
|
if (el.getAttribute('aria-label')) {
|
|
163
164
|
return true;
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
// `aria-labelledby` is a list of ids, and each is resolved in the tree of the element: the
|
|
167
|
+
// content of a `template` element and a shadow root are trees of their own. A DOM has resolved the
|
|
168
|
+
// character references of a token already, but the markuplint DOM gives it as written in the markup,
|
|
169
|
+
// so the token is looked for as it is, and then with its character references resolved.
|
|
170
|
+
const ids = el.getAttribute('aria-labelledby')?.split(/\s+/).filter(Boolean) ?? [];
|
|
171
|
+
const root = el.getRootNode();
|
|
172
|
+
if ((root.nodeType === 9 || root.nodeType === 11) &&
|
|
173
|
+
ids.some(id => root.getElementById(id) ?? root.getElementById(decodeEntities(id)))) {
|
|
167
174
|
return true;
|
|
168
175
|
}
|
|
169
176
|
}
|
|
@@ -2,6 +2,8 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* In ARIA 1.3, `generic` role elements are additionally transparent for
|
|
4
4
|
* ownership traversal.
|
|
5
|
+
*
|
|
6
|
+
* The ancestors are those of the accessibility tree: see {@link getAccessibilityParentElement}.
|
|
5
7
|
*/
|
|
6
8
|
export declare function getNonPresentationalAncestor(el: Element, specs: MLMLSpec, version: ARIAVersion): import("../../types/index.js").ComputedRole | {
|
|
7
9
|
el: null;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { getAccessibilityParentElement } from '../dom/tree-boundary.js';
|
|
1
2
|
import { isTransparentForOwnership } from './is-presentational.js';
|
|
2
3
|
import { getComputedRole } from './get-computed-role.js';
|
|
3
4
|
/**
|
|
4
5
|
* In ARIA 1.3, `generic` role elements are additionally transparent for
|
|
5
6
|
* ownership traversal.
|
|
7
|
+
*
|
|
8
|
+
* The ancestors are those of the accessibility tree: see {@link getAccessibilityParentElement}.
|
|
6
9
|
*/
|
|
7
10
|
export function getNonPresentationalAncestor(
|
|
8
11
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
9
12
|
el, specs, version) {
|
|
10
|
-
let ancestor = el
|
|
13
|
+
let ancestor = getAccessibilityParentElement(el);
|
|
11
14
|
/**
|
|
12
15
|
* In ARIA 1.1 and 1.2, the role of an element is not affected by the role of its ancestor elements.
|
|
13
16
|
*
|
|
@@ -19,7 +22,7 @@ el, specs, version) {
|
|
|
19
22
|
if (!isTransparentForOwnership(ancestorRole.role?.name, version)) {
|
|
20
23
|
return ancestorRole;
|
|
21
24
|
}
|
|
22
|
-
ancestor = ancestor
|
|
25
|
+
ancestor = getAccessibilityParentElement(ancestor);
|
|
23
26
|
}
|
|
24
27
|
return {
|
|
25
28
|
el: ancestor,
|
|
@@ -4,7 +4,9 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
|
4
4
|
* Applies WAI-ARIA exclusion/inclusion rules, SVG rendering rules, and
|
|
5
5
|
* HTML metadata element filtering.
|
|
6
6
|
*
|
|
7
|
-
* @
|
|
7
|
+
* The ancestors it reads are those of the accessibility tree ({@link getComposedParentElement}).
|
|
8
|
+
*
|
|
9
|
+
* @see https://w3c.github.io/aria/#accessibility_tree
|
|
8
10
|
*
|
|
9
11
|
* @param el - The DOM element to check for accessibility tree exposure
|
|
10
12
|
* @param specs - The full markup language specification
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ariaSpecs } from './aria-specs.js';
|
|
2
|
+
import { getComposedParentElement, isNotRendered } from '../dom/tree-boundary.js';
|
|
2
3
|
import { getSpecByTagName } from '../../utils/get-spec-by-tag-name.js';
|
|
3
4
|
import { isPresentational } from './is-presentational.js';
|
|
4
5
|
import { resolveNamespace } from '../../utils/resolve-namespace.js';
|
|
@@ -8,7 +9,9 @@ import { getComputedRole } from './get-computed-role.js';
|
|
|
8
9
|
* Applies WAI-ARIA exclusion/inclusion rules, SVG rendering rules, and
|
|
9
10
|
* HTML metadata element filtering.
|
|
10
11
|
*
|
|
11
|
-
* @
|
|
12
|
+
* The ancestors it reads are those of the accessibility tree ({@link getComposedParentElement}).
|
|
13
|
+
*
|
|
14
|
+
* @see https://w3c.github.io/aria/#accessibility_tree
|
|
12
15
|
*
|
|
13
16
|
* @param el - The DOM element to check for accessibility tree exposure
|
|
14
17
|
* @param specs - The full markup language specification
|
|
@@ -47,7 +50,7 @@ el, specs, version) {
|
|
|
47
50
|
/**
|
|
48
51
|
* Excluding Elements from the Accessibility Tree
|
|
49
52
|
*
|
|
50
|
-
* @see https://
|
|
53
|
+
* @see https://w3c.github.io/aria/#tree_exclusion
|
|
51
54
|
*
|
|
52
55
|
* @param specs
|
|
53
56
|
* @param el
|
|
@@ -63,14 +66,19 @@ el, specs, version) {
|
|
|
63
66
|
* that have host language semantics specifying
|
|
64
67
|
* that the element is not displayed, such as CSS display:none,
|
|
65
68
|
* visibility:hidden, or the HTML hidden attribute.
|
|
69
|
+
*
|
|
70
|
+
* An element that is not in the flat tree is not displayed either: a light child of a shadow host
|
|
71
|
+
* that no slot takes.
|
|
66
72
|
*/
|
|
67
73
|
{
|
|
68
74
|
let currentEl = el;
|
|
69
75
|
while (currentEl) {
|
|
70
|
-
if (
|
|
76
|
+
if (isNotRendered(currentEl) ||
|
|
77
|
+
hasDisplayNodeOrVisibilityHidden(currentEl) ||
|
|
78
|
+
currentEl.hasAttribute('hidden')) {
|
|
71
79
|
return true;
|
|
72
80
|
}
|
|
73
|
-
currentEl = currentEl
|
|
81
|
+
currentEl = getComposedParentElement(currentEl);
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
/**
|
|
@@ -97,7 +105,7 @@ el, specs, version) {
|
|
|
97
105
|
if (currentEl.getAttribute('aria-hidden') === 'true') {
|
|
98
106
|
return true;
|
|
99
107
|
}
|
|
100
|
-
currentEl = currentEl
|
|
108
|
+
currentEl = getComposedParentElement(currentEl);
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
/**
|
|
@@ -109,13 +117,13 @@ el, specs, version) {
|
|
|
109
117
|
* However, the text content of any excluded descendants is included.
|
|
110
118
|
*/
|
|
111
119
|
{
|
|
112
|
-
let currentEl = el
|
|
120
|
+
let currentEl = getComposedParentElement(el);
|
|
113
121
|
while (currentEl) {
|
|
114
122
|
const { role } = getComputedRole(specs, currentEl, version);
|
|
115
123
|
if (role?.childrenPresentational) {
|
|
116
124
|
return true;
|
|
117
125
|
}
|
|
118
|
-
currentEl = currentEl
|
|
126
|
+
currentEl = getComposedParentElement(currentEl);
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
return false;
|
|
@@ -128,7 +136,7 @@ el, specs, version) {
|
|
|
128
136
|
* user agents MUST provide an accessible object in the accessibility tree
|
|
129
137
|
* for DOM elements that meet any of the following criteria:
|
|
130
138
|
*
|
|
131
|
-
* @see https://
|
|
139
|
+
* @see https://w3c.github.io/aria/#tree_inclusion
|
|
132
140
|
*
|
|
133
141
|
* @param specs
|
|
134
142
|
* @param el
|
|
@@ -172,6 +180,11 @@ el, specs, version) {
|
|
|
172
180
|
}
|
|
173
181
|
// Has a global WAI-ARIA attribute
|
|
174
182
|
for (const attr of el.attributes) {
|
|
183
|
+
// An attribute that the host does not output (`MLAttr#isAbsent`, e.g. `aria-label={null}`)
|
|
184
|
+
// is not an attribute of the DOM
|
|
185
|
+
if (attr.isAbsent) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
175
188
|
if (globalAria.some(aria => aria.name === attr.localName)) {
|
|
176
189
|
results.push(true);
|
|
177
190
|
break;
|
|
@@ -27,9 +27,8 @@ export declare function isExposedAsPresentational(computed: ComputedRole): boole
|
|
|
27
27
|
* - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
|
|
28
28
|
* This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
|
|
29
29
|
* and `getNonPresentationalAncestor`.
|
|
30
|
-
* - **ARIA 1.3:** `generic` is additionally transparent per the spec
|
|
31
|
-
*
|
|
32
|
-
* `generic` or `none`."
|
|
30
|
+
* - **ARIA 1.3:** `generic` is additionally transparent per the spec, which has
|
|
31
|
+
* the user agent ignore the intervening elements with the role `generic` or `none`.
|
|
33
32
|
*
|
|
34
33
|
* **Note:** `matchesContextRole` calls this function unconditionally
|
|
35
34
|
* (without a version gate) so that `presentation`/`none` are always
|
|
@@ -33,9 +33,8 @@ export function isExposedAsPresentational(computed) {
|
|
|
33
33
|
* - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
|
|
34
34
|
* This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
|
|
35
35
|
* and `getNonPresentationalAncestor`.
|
|
36
|
-
* - **ARIA 1.3:** `generic` is additionally transparent per the spec
|
|
37
|
-
*
|
|
38
|
-
* `generic` or `none`."
|
|
36
|
+
* - **ARIA 1.3:** `generic` is additionally transparent per the spec, which has
|
|
37
|
+
* the user agent ignore the intervening elements with the role `generic` or `none`.
|
|
39
38
|
*
|
|
40
39
|
* **Note:** `matchesContextRole` calls this function unconditionally
|
|
41
40
|
* (without a version gate) so that `presentation`/`none` are always
|
|
@@ -4,11 +4,14 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
|
4
4
|
* Context Role" in ARIA 1.2). Each condition string describes a chain of
|
|
5
5
|
* ancestor roles separated by ` > ` (e.g., `"list > group"`).
|
|
6
6
|
*
|
|
7
|
-
* TODO: This function only walks the
|
|
7
|
+
* TODO: This function only walks the composed parent chain and does not
|
|
8
8
|
* consider `aria-owns` relationships. An element referenced by `aria-owns` on
|
|
9
9
|
* a remote ancestor should be treated as if it were an accessibility child of
|
|
10
10
|
* that ancestor. Implementing this requires a document-wide reverse lookup of
|
|
11
11
|
* `aria-owns` attributes, which is a separate architectural concern.
|
|
12
12
|
* See also: `has-required-owned-elements.ts` has a similar limitation.
|
|
13
|
+
*
|
|
14
|
+
* The chain is the one the accessibility tree is built from ({@link getAccessibilityParentElement}), which
|
|
15
|
+
* does not have a `slot`.
|
|
13
16
|
*/
|
|
14
17
|
export declare function matchesContextRole(conditions: readonly string[], ownedEl: Element, specs: MLMLSpec, version: ARIAVersion): boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAccessibilityParentElement } from '../dom/tree-boundary.js';
|
|
1
2
|
import { isTransparentForOwnership } from './is-presentational.js';
|
|
2
3
|
import { getComputedRole } from './get-computed-role.js';
|
|
3
4
|
/**
|
|
@@ -5,17 +6,20 @@ import { getComputedRole } from './get-computed-role.js';
|
|
|
5
6
|
* Context Role" in ARIA 1.2). Each condition string describes a chain of
|
|
6
7
|
* ancestor roles separated by ` > ` (e.g., `"list > group"`).
|
|
7
8
|
*
|
|
8
|
-
* TODO: This function only walks the
|
|
9
|
+
* TODO: This function only walks the composed parent chain and does not
|
|
9
10
|
* consider `aria-owns` relationships. An element referenced by `aria-owns` on
|
|
10
11
|
* a remote ancestor should be treated as if it were an accessibility child of
|
|
11
12
|
* that ancestor. Implementing this requires a document-wide reverse lookup of
|
|
12
13
|
* `aria-owns` attributes, which is a separate architectural concern.
|
|
13
14
|
* See also: `has-required-owned-elements.ts` has a similar limitation.
|
|
15
|
+
*
|
|
16
|
+
* The chain is the one the accessibility tree is built from ({@link getAccessibilityParentElement}), which
|
|
17
|
+
* does not have a `slot`.
|
|
14
18
|
*/
|
|
15
19
|
export function matchesContextRole(conditions,
|
|
16
20
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
17
21
|
ownedEl, specs, version) {
|
|
18
|
-
return conditions.some(condition => matchesCondition(condition, ownedEl
|
|
22
|
+
return conditions.some(condition => matchesCondition(condition, getAccessibilityParentElement(ownedEl), specs, version));
|
|
19
23
|
}
|
|
20
24
|
function matchesCondition(condition,
|
|
21
25
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -27,9 +31,9 @@ parentEl, specs, version) {
|
|
|
27
31
|
}
|
|
28
32
|
const parentRole = getComputedRole(specs, parentEl, version, true).role;
|
|
29
33
|
/**
|
|
30
|
-
* ARIA 1.3 (
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* ARIA 1.3 (Required Accessibility Parent Role): to determine whether an
|
|
35
|
+
* element has a parent with the required role, the user agent ignores the
|
|
36
|
+
* elements with the role `generic` or `none`.
|
|
33
37
|
*
|
|
34
38
|
* `presentation`/`none` are transparent in all ARIA versions to stay
|
|
35
39
|
* consistent with `getNonPresentationalAncestor` (which already skips
|
|
@@ -47,14 +51,14 @@ parentEl, specs, version) {
|
|
|
47
51
|
* @see https://w3c.github.io/aria/#scope
|
|
48
52
|
*/
|
|
49
53
|
if (isTransparentForOwnership(parentRole?.name, version)) {
|
|
50
|
-
parentEl = parentEl
|
|
54
|
+
parentEl = getAccessibilityParentElement(parentEl);
|
|
51
55
|
continue;
|
|
52
56
|
}
|
|
53
57
|
const condition = conditions.shift();
|
|
54
58
|
if (condition !== parentRole?.name) {
|
|
55
59
|
return false;
|
|
56
60
|
}
|
|
57
|
-
parentEl = parentEl
|
|
61
|
+
parentEl = getAccessibilityParentElement(parentEl);
|
|
58
62
|
}
|
|
59
63
|
return true;
|
|
60
64
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the helpers read of an element: the DOM `Element`, and `MLElement` of `@markuplint/ml-core`.
|
|
3
|
+
* An element without `getRootNode` (an adapter that has no notion of a tree boundary) is in one
|
|
4
|
+
* tree with its parent element.
|
|
5
|
+
*/
|
|
6
|
+
type TreeElement = {
|
|
7
|
+
readonly parentElement: unknown;
|
|
8
|
+
getRootNode?(): unknown;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* What {@link getComposedParentElement} reads besides {@link TreeElement}: the slot that places the
|
|
12
|
+
* element in the flat tree. `null` is an element of a shadow host that no slot takes (not rendered),
|
|
13
|
+
* `undefined` an element that is not one or whose assignment is not known.
|
|
14
|
+
*
|
|
15
|
+
* @template P - The type of the parent element the element reads
|
|
16
|
+
*/
|
|
17
|
+
type FlatTreeElement<P extends TreeElement> = {
|
|
18
|
+
readonly parentElement: P | null;
|
|
19
|
+
getRootNode?(): unknown;
|
|
20
|
+
getFlatTreeSlot?(): P | null | undefined;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The parent element of an element in its own tree, which is what the DOM calls `parentElement`:
|
|
24
|
+
* `null` at the top of the content of a `template` element or of a shadow root, even where the
|
|
25
|
+
* `parentElement` of the element implementation is the `template` element.
|
|
26
|
+
*
|
|
27
|
+
* @template P - The type of the parent element the element reads
|
|
28
|
+
* @param el - The element to start from
|
|
29
|
+
* @returns The parent element, or `null` when the element is at the top of its tree
|
|
30
|
+
*/
|
|
31
|
+
export declare function getTreeParentElement<P extends TreeElement>(el: {
|
|
32
|
+
readonly parentElement: P | null;
|
|
33
|
+
getRootNode?(): unknown;
|
|
34
|
+
}): P | null;
|
|
35
|
+
/**
|
|
36
|
+
* The parent element of an element as the accessibility tree is built: the parent element in its own
|
|
37
|
+
* tree, or, at the top of a shadow root, the host of the shadow root. `null` at the top of the content
|
|
38
|
+
* of an ordinary `template` element, and at the top of a document or of any other root that is not a
|
|
39
|
+
* shadow root. For a light child of a shadow host, the slot that takes it, or `null` when none does.
|
|
40
|
+
*
|
|
41
|
+
* @template P - The type of the parent element the element reads
|
|
42
|
+
* @param el - The element to start from
|
|
43
|
+
* @returns The parent element or the shadow host, or `null` when the walk ends
|
|
44
|
+
*/
|
|
45
|
+
export declare function getComposedParentElement<P extends TreeElement>(el: FlatTreeElement<P>): P | null;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a root is a shadow root: a document fragment that has a host.
|
|
48
|
+
* The shadow root of `@markuplint/ml-core` is not a `ShadowRoot` of the DOM by class, and the
|
|
49
|
+
* content of an ordinary `template` element is a document fragment that has no host.
|
|
50
|
+
*
|
|
51
|
+
* @template P - The type of the host
|
|
52
|
+
* @param root - The root of a tree
|
|
53
|
+
* @returns `true` if the root has a host element
|
|
54
|
+
*/
|
|
55
|
+
export declare function isShadowRoot<P = unknown>(root: unknown): root is {
|
|
56
|
+
readonly host: P;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Whether an element is a `slot` element of the HTML namespace, which the accessibility tree does not
|
|
60
|
+
* map: it has no role of its own, and what it renders (the nodes assigned to it, or its children)
|
|
61
|
+
* belongs to the parent of the `slot`. A walk up the ancestors of the accessibility tree that reads
|
|
62
|
+
* roles passes through it. The walks that read an attribute of the ancestors (`hidden`, `aria-hidden`)
|
|
63
|
+
* do not skip it: what a `slot` that has them renders is hidden as well.
|
|
64
|
+
*
|
|
65
|
+
* It does not look at `MLParser#slotElement` on purpose: the `slot` of a framework (Vue, Svelte, Astro)
|
|
66
|
+
* is replaced with what it is given, so it is no element of the accessibility tree either, and the
|
|
67
|
+
* pass-through is right for it too. Which `slot` has an assignment is `MLElement#getAssignedNodes` of
|
|
68
|
+
* `@markuplint/ml-core`, which does look at it.
|
|
69
|
+
*
|
|
70
|
+
* > No corresponding role
|
|
71
|
+
*
|
|
72
|
+
* @param el - An element
|
|
73
|
+
* @returns `true` for the `slot` element of the HTML namespace
|
|
74
|
+
* @see https://w3c.github.io/html-aam/#el-slot
|
|
75
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
76
|
+
*/
|
|
77
|
+
export declare function isSlotElement(el: {
|
|
78
|
+
readonly localName: string;
|
|
79
|
+
readonly namespaceURI?: string | null;
|
|
80
|
+
}): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Whether an element is not rendered: it is a light child of a shadow host, and no slot of the shadow
|
|
83
|
+
* root takes it (`getFlatTreeSlot()` is `null`). The flat tree has no place for it, so it is not in the
|
|
84
|
+
* accessibility tree either, whatever its host is.
|
|
85
|
+
*
|
|
86
|
+
* @param el - An element
|
|
87
|
+
* @returns `true` for an element that is known not to be rendered
|
|
88
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
89
|
+
*/
|
|
90
|
+
export declare function isNotRendered(el: object): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The parent element of an element in the accessibility tree: the composed parent element
|
|
93
|
+
* ({@link getComposedParentElement}) with the `slot` elements passed through ({@link isSlotElement}). The walks
|
|
94
|
+
* that read the roles of the ancestors take it; the walks that read an attribute of the ancestors take
|
|
95
|
+
* {@link getComposedParentElement}, as what a `slot` that has `hidden` renders is hidden.
|
|
96
|
+
*
|
|
97
|
+
* @template E - The type of the element
|
|
98
|
+
* @param el - The element to start from
|
|
99
|
+
* @returns The parent element, or `null` when the walk ends
|
|
100
|
+
*/
|
|
101
|
+
export declare function getAccessibilityParentElement<E extends {
|
|
102
|
+
readonly parentElement: E | null;
|
|
103
|
+
readonly localName: string;
|
|
104
|
+
readonly namespaceURI?: string | null;
|
|
105
|
+
getRootNode?(): unknown;
|
|
106
|
+
getFlatTreeSlot?(): E | null | undefined;
|
|
107
|
+
}>(el: E): E | null;
|
|
108
|
+
export {};
|