@markuplint/ml-spec 5.3.0 → 5.5.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 +24 -0
- package/README.md +4 -0
- package/lib/algorithm/aria/accname/aria-steps.d.ts +26 -9
- package/lib/algorithm/aria/accname/aria-steps.js +38 -9
- package/lib/algorithm/aria/accname/compute.d.ts +38 -6
- package/lib/algorithm/aria/accname/compute.js +60 -12
- package/lib/algorithm/aria/accname/element-names.d.ts +4 -4
- package/lib/algorithm/aria/accname/element-names.js +31 -30
- package/lib/algorithm/aria/accname/helpers.d.ts +52 -12
- package/lib/algorithm/aria/accname/helpers.js +78 -23
- package/lib/algorithm/aria/accname/index.d.ts +2 -2
- package/lib/algorithm/aria/accname/index.js +1 -1
- package/lib/algorithm/aria/accname/label-steps.d.ts +4 -4
- package/lib/algorithm/aria/accname/label-steps.js +14 -5
- package/lib/algorithm/aria/accname/result.d.ts +21 -0
- package/lib/algorithm/aria/accname/result.js +28 -0
- 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 +51 -0
- package/lib/algorithm/aria/accname-computation.d.ts +1 -1
- package/lib/algorithm/aria/accname-computation.js +12 -6
- package/lib/algorithm/aria/get-aria.js +2 -2
- package/lib/algorithm/aria/get-computed-role.js +18 -14
- package/lib/algorithm/aria/get-explicit-role.d.ts +6 -0
- package/lib/algorithm/aria/get-explicit-role.js +82 -9
- 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 +15 -7
- package/lib/algorithm/aria/is-presentational.d.ts +15 -4
- package/lib/algorithm/aria/is-presentational.js +16 -3
- package/lib/algorithm/aria/matches-context-role.d.ts +3 -1
- package/lib/algorithm/aria/matches-context-role.js +10 -7
- package/lib/algorithm/dom/tree-boundary.d.ts +93 -0
- package/lib/algorithm/dom/tree-boundary.js +103 -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 +67 -2
- package/lib/types/permitted-structures.d.ts +5 -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 -5
|
@@ -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 { 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,15 +169,14 @@ 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.
|
|
177
|
-
|
|
179
|
+
explicitRole.role === null &&
|
|
178
180
|
// It is an SVG element.
|
|
179
181
|
el.namespaceURI === SVG_NAMESPACE &&
|
|
180
182
|
!hasSvgAccessibleNameSource(el)) {
|
|
@@ -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
|
|
@@ -204,10 +208,9 @@ el, version, assumeSingleNode) {
|
|
|
204
208
|
*
|
|
205
209
|
* 1. It may be focusable
|
|
206
210
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* in the previous processing that computes the permitted role (`getExplicitRole`).
|
|
211
|
+
* ARIA in HTML does not permit the presentational roles on most
|
|
212
|
+
* interactive elements, but `getExplicitRole` keeps such a role with
|
|
213
|
+
* `NO_PERMITTED`, as user agents expose it, so this resolves it.
|
|
211
214
|
*/
|
|
212
215
|
mayBeFocusable(el, specs) &&
|
|
213
216
|
/**
|
|
@@ -287,6 +290,7 @@ el, specs, attrName) {
|
|
|
287
290
|
}
|
|
288
291
|
/**
|
|
289
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}.
|
|
290
294
|
*/
|
|
291
295
|
function someAncestors(
|
|
292
296
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -295,7 +299,7 @@ el, predicate) {
|
|
|
295
299
|
let current = el;
|
|
296
300
|
while (current) {
|
|
297
301
|
list.push(current);
|
|
298
|
-
current = current
|
|
302
|
+
current = getComposedParentElement(current);
|
|
299
303
|
}
|
|
300
304
|
return list.some(predicate);
|
|
301
305
|
}
|
|
@@ -321,7 +325,7 @@ el, specs, version) {
|
|
|
321
325
|
if (!computedRole.role?.isImplicit) {
|
|
322
326
|
return false;
|
|
323
327
|
}
|
|
324
|
-
const parent = el
|
|
328
|
+
const parent = getComposedParentElement(el);
|
|
325
329
|
if (!parent) {
|
|
326
330
|
return false;
|
|
327
331
|
}
|
|
@@ -2,6 +2,12 @@ import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Implements the WAI-ARIA "Handling Author Errors" algorithm.
|
|
4
4
|
*
|
|
5
|
+
* The roles that ARIA in HTML does not permit on the element are authoring
|
|
6
|
+
* errors that the user agent does not correct: such a role is returned with
|
|
7
|
+
* `errorType: 'NO_PERMITTED'` (Core-AAM, the conflicts between the mappings), except on an element that is
|
|
8
|
+
* never rendered.
|
|
9
|
+
*
|
|
5
10
|
* @see https://w3c.github.io/aria/#document-handling_author-errors
|
|
11
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts — Core-AAM §3.2
|
|
6
12
|
*/
|
|
7
13
|
export declare function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole;
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { decodeEntities } from '@markuplint/shared';
|
|
2
|
+
import { XHTML_NAMESPACE } from '../../const/index.js';
|
|
1
3
|
import { getRoleSpec } from './get-role-spec.js';
|
|
2
4
|
import { resolveNamespace } from '../../utils/resolve-namespace.js';
|
|
3
5
|
import { getPermittedRoles } from './get-permitted-roles.js';
|
|
4
6
|
/**
|
|
5
7
|
* Implements the WAI-ARIA "Handling Author Errors" algorithm.
|
|
6
8
|
*
|
|
9
|
+
* The roles that ARIA in HTML does not permit on the element are authoring
|
|
10
|
+
* errors that the user agent does not correct: such a role is returned with
|
|
11
|
+
* `errorType: 'NO_PERMITTED'` (Core-AAM, the conflicts between the mappings), except on an element that is
|
|
12
|
+
* never rendered.
|
|
13
|
+
*
|
|
7
14
|
* @see https://w3c.github.io/aria/#document-handling_author-errors
|
|
15
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts — Core-AAM §3.2
|
|
8
16
|
*/
|
|
9
17
|
export function getExplicitRole(specs,
|
|
10
18
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -46,13 +54,6 @@ el, version) {
|
|
|
46
54
|
error = 'ABSTRACT';
|
|
47
55
|
continue;
|
|
48
56
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Whether the role is the permitted role according to ARIA in HTML.
|
|
51
|
-
*/
|
|
52
|
-
if (!permittedRoles.some(r => r.name === roleName)) {
|
|
53
|
-
error = 'NO_PERMITTED';
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
57
|
/**
|
|
57
58
|
* > Certain landmark roles require names from authors.
|
|
58
59
|
* > In situations where an author has not specified names for these landmarks,
|
|
@@ -66,6 +67,35 @@ el, version) {
|
|
|
66
67
|
error = 'INVALID_LANDMARK';
|
|
67
68
|
continue;
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* A role that ARIA in HTML does not permit on the element is an
|
|
72
|
+
* authoring error, but user agents still expose it, so the role is
|
|
73
|
+
* kept and the error is reported alongside. An element that is never
|
|
74
|
+
* rendered is not in the accessibility tree at all (WAI-ARIA §7.1),
|
|
75
|
+
* so the role is dropped there.
|
|
76
|
+
*
|
|
77
|
+
* @see https://w3c.github.io/aria/#tree_exclusion
|
|
78
|
+
*
|
|
79
|
+
* > Elements, including their descendent elements, that have host language semantics specifying that the element is not displayed, such as CSS display:none, visibility:hidden, or the HTML hidden attribute.
|
|
80
|
+
*
|
|
81
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts
|
|
82
|
+
*
|
|
83
|
+
* > While conformance checkers might signal an error or warning when a WAI-ARIA role is used on elements with strong native semantics, user agents MUST still use the value of the semantic of the WAI-ARIA role when exposing the element to accessibility APIs.
|
|
84
|
+
*/
|
|
85
|
+
if (!permittedRoles.some(r => r.name === roleName)) {
|
|
86
|
+
if (isNeverRendered(el, namespaceURI)) {
|
|
87
|
+
error = 'NO_PERMITTED';
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
el,
|
|
92
|
+
role: {
|
|
93
|
+
...spec,
|
|
94
|
+
isImplicit: false,
|
|
95
|
+
},
|
|
96
|
+
errorType: 'NO_PERMITTED',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
69
99
|
/**
|
|
70
100
|
* Otherwise
|
|
71
101
|
*/
|
|
@@ -83,6 +113,43 @@ el, version) {
|
|
|
83
113
|
errorType: error,
|
|
84
114
|
};
|
|
85
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* HTML elements that the user agent style sheet never renders (`display: none`),
|
|
118
|
+
* together with `input[type=hidden]` from the same rules (`isNeverRendered`).
|
|
119
|
+
* `area` is left out: it is rendered through its image map. The rules that
|
|
120
|
+
* depend on state (`[hidden]`, `noscript` with scripting) are left out too: the
|
|
121
|
+
* role stays on such an element, as its `hidden` attribute is a matter of the
|
|
122
|
+
* accessible name computation (Step 2A), not of the role.
|
|
123
|
+
*
|
|
124
|
+
* @see https://html.spec.whatwg.org/multipage/rendering.html#hidden-elements
|
|
125
|
+
*/
|
|
126
|
+
const NEVER_RENDERED_HTML_ELEMENTS = new Set([
|
|
127
|
+
'base',
|
|
128
|
+
'basefont',
|
|
129
|
+
'datalist',
|
|
130
|
+
'head',
|
|
131
|
+
'link',
|
|
132
|
+
'meta',
|
|
133
|
+
'noembed',
|
|
134
|
+
'noframes',
|
|
135
|
+
'param',
|
|
136
|
+
'rp',
|
|
137
|
+
'script',
|
|
138
|
+
'style',
|
|
139
|
+
'template',
|
|
140
|
+
'title',
|
|
141
|
+
]);
|
|
142
|
+
function isNeverRendered(
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
144
|
+
el, namespaceURI) {
|
|
145
|
+
if (namespaceURI !== XHTML_NAMESPACE) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (NEVER_RENDERED_HTML_ELEMENTS.has(el.localName)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return el.localName === 'input' && el.getAttribute('type')?.trim().toLowerCase() === 'hidden';
|
|
152
|
+
}
|
|
86
153
|
function isLandmarkRole(role) {
|
|
87
154
|
return role?.superClassRoles.some(su => su.name === 'landmark');
|
|
88
155
|
}
|
|
@@ -96,8 +163,14 @@ el, role) {
|
|
|
96
163
|
if (el.getAttribute('aria-label')) {
|
|
97
164
|
return true;
|
|
98
165
|
}
|
|
99
|
-
|
|
100
|
-
|
|
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)))) {
|
|
101
174
|
return true;
|
|
102
175
|
}
|
|
103
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 getComposedParentElement}.
|
|
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 { getComposedParentElement } 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 getComposedParentElement}.
|
|
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 = getComposedParentElement(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 = getComposedParentElement(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 } 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
|
|
@@ -70,7 +73,7 @@ el, specs, version) {
|
|
|
70
73
|
if (hasDisplayNodeOrVisibilityHidden(currentEl) || currentEl.hasAttribute('hidden')) {
|
|
71
74
|
return true;
|
|
72
75
|
}
|
|
73
|
-
currentEl = currentEl
|
|
76
|
+
currentEl = getComposedParentElement(currentEl);
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
@@ -97,7 +100,7 @@ el, specs, version) {
|
|
|
97
100
|
if (currentEl.getAttribute('aria-hidden') === 'true') {
|
|
98
101
|
return true;
|
|
99
102
|
}
|
|
100
|
-
currentEl = currentEl
|
|
103
|
+
currentEl = getComposedParentElement(currentEl);
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
/**
|
|
@@ -109,13 +112,13 @@ el, specs, version) {
|
|
|
109
112
|
* However, the text content of any excluded descendants is included.
|
|
110
113
|
*/
|
|
111
114
|
{
|
|
112
|
-
let currentEl = el
|
|
115
|
+
let currentEl = getComposedParentElement(el);
|
|
113
116
|
while (currentEl) {
|
|
114
117
|
const { role } = getComputedRole(specs, currentEl, version);
|
|
115
118
|
if (role?.childrenPresentational) {
|
|
116
119
|
return true;
|
|
117
120
|
}
|
|
118
|
-
currentEl = currentEl
|
|
121
|
+
currentEl = getComposedParentElement(currentEl);
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
return false;
|
|
@@ -128,7 +131,7 @@ el, specs, version) {
|
|
|
128
131
|
* user agents MUST provide an accessible object in the accessibility tree
|
|
129
132
|
* for DOM elements that meet any of the following criteria:
|
|
130
133
|
*
|
|
131
|
-
* @see https://
|
|
134
|
+
* @see https://w3c.github.io/aria/#tree_inclusion
|
|
132
135
|
*
|
|
133
136
|
* @param specs
|
|
134
137
|
* @param el
|
|
@@ -172,6 +175,11 @@ el, specs, version) {
|
|
|
172
175
|
}
|
|
173
176
|
// Has a global WAI-ARIA attribute
|
|
174
177
|
for (const attr of el.attributes) {
|
|
178
|
+
// An attribute that the host does not output (`MLAttr#isAbsent`, e.g. `aria-label={null}`)
|
|
179
|
+
// is not an attribute of the DOM
|
|
180
|
+
if (attr.isAbsent) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
175
183
|
if (globalAria.some(aria => aria.name === attr.localName)) {
|
|
176
184
|
results.push(true);
|
|
177
185
|
break;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ARIAVersion } from '../../types/index.js';
|
|
1
|
+
import type { ARIAVersion, ComputedRole } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Determines whether a given role name corresponds to a presentational role
|
|
4
4
|
* (`"presentation"` or `"none"`), which indicates the element should be
|
|
@@ -8,6 +8,18 @@ import type { ARIAVersion } from '../../types/index.js';
|
|
|
8
8
|
* @returns `true` if the role name is `"presentation"` or `"none"`, `false` otherwise
|
|
9
9
|
*/
|
|
10
10
|
export declare function isPresentational(roleName?: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Determines whether an element is exposed with the `none` or `presentation`
|
|
13
|
+
* role that its author specified, after the Presentational Roles Conflict
|
|
14
|
+
* Resolution. The implicit `presentation` role of `<img alt="">` does not count:
|
|
15
|
+
* HTML-AAM gives that image no name by its own rule.
|
|
16
|
+
*
|
|
17
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
18
|
+
*
|
|
19
|
+
* @param computed - The computed role of the element
|
|
20
|
+
* @returns `true` if the computed role is an explicit presentational role
|
|
21
|
+
*/
|
|
22
|
+
export declare function isExposedAsPresentational(computed: ComputedRole): boolean;
|
|
11
23
|
/**
|
|
12
24
|
* Determines whether a given role is transparent for ownership traversal.
|
|
13
25
|
*
|
|
@@ -15,9 +27,8 @@ export declare function isPresentational(roleName?: string): boolean;
|
|
|
15
27
|
* - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
|
|
16
28
|
* This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
|
|
17
29
|
* and `getNonPresentationalAncestor`.
|
|
18
|
-
* - **ARIA 1.3:** `generic` is additionally transparent per the spec
|
|
19
|
-
*
|
|
20
|
-
* `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`.
|
|
21
32
|
*
|
|
22
33
|
* **Note:** `matchesContextRole` calls this function unconditionally
|
|
23
34
|
* (without a version gate) so that `presentation`/`none` are always
|
|
@@ -12,6 +12,20 @@ export function isPresentational(roleName) {
|
|
|
12
12
|
}
|
|
13
13
|
return ['presentation', 'none'].includes(roleName);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Determines whether an element is exposed with the `none` or `presentation`
|
|
17
|
+
* role that its author specified, after the Presentational Roles Conflict
|
|
18
|
+
* Resolution. The implicit `presentation` role of `<img alt="">` does not count:
|
|
19
|
+
* HTML-AAM gives that image no name by its own rule.
|
|
20
|
+
*
|
|
21
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
22
|
+
*
|
|
23
|
+
* @param computed - The computed role of the element
|
|
24
|
+
* @returns `true` if the computed role is an explicit presentational role
|
|
25
|
+
*/
|
|
26
|
+
export function isExposedAsPresentational(computed) {
|
|
27
|
+
return !!computed.role && !computed.role.isImplicit && isPresentational(computed.role.name);
|
|
28
|
+
}
|
|
15
29
|
/**
|
|
16
30
|
* Determines whether a given role is transparent for ownership traversal.
|
|
17
31
|
*
|
|
@@ -19,9 +33,8 @@ export function isPresentational(roleName) {
|
|
|
19
33
|
* - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
|
|
20
34
|
* This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
|
|
21
35
|
* and `getNonPresentationalAncestor`.
|
|
22
|
-
* - **ARIA 1.3:** `generic` is additionally transparent per the spec
|
|
23
|
-
*
|
|
24
|
-
* `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`.
|
|
25
38
|
*
|
|
26
39
|
* **Note:** `matchesContextRole` calls this function unconditionally
|
|
27
40
|
* (without a version gate) so that `presentation`/`none` are always
|
|
@@ -4,11 +4,13 @@ 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 getComposedParentElement}).
|
|
13
15
|
*/
|
|
14
16
|
export declare function matchesContextRole(conditions: readonly string[], ownedEl: Element, specs: MLMLSpec, version: ARIAVersion): boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getComposedParentElement } 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,19 @@ 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 getComposedParentElement}).
|
|
14
17
|
*/
|
|
15
18
|
export function matchesContextRole(conditions,
|
|
16
19
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
17
20
|
ownedEl, specs, version) {
|
|
18
|
-
return conditions.some(condition => matchesCondition(condition, ownedEl
|
|
21
|
+
return conditions.some(condition => matchesCondition(condition, getComposedParentElement(ownedEl), specs, version));
|
|
19
22
|
}
|
|
20
23
|
function matchesCondition(condition,
|
|
21
24
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -27,9 +30,9 @@ parentEl, specs, version) {
|
|
|
27
30
|
}
|
|
28
31
|
const parentRole = getComputedRole(specs, parentEl, version, true).role;
|
|
29
32
|
/**
|
|
30
|
-
* ARIA 1.3 (
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
+
* ARIA 1.3 (Required Accessibility Parent Role): to determine whether an
|
|
34
|
+
* element has a parent with the required role, the user agent ignores the
|
|
35
|
+
* elements with the role `generic` or `none`.
|
|
33
36
|
*
|
|
34
37
|
* `presentation`/`none` are transparent in all ARIA versions to stay
|
|
35
38
|
* consistent with `getNonPresentationalAncestor` (which already skips
|
|
@@ -47,14 +50,14 @@ parentEl, specs, version) {
|
|
|
47
50
|
* @see https://w3c.github.io/aria/#scope
|
|
48
51
|
*/
|
|
49
52
|
if (isTransparentForOwnership(parentRole?.name, version)) {
|
|
50
|
-
parentEl = parentEl
|
|
53
|
+
parentEl = getComposedParentElement(parentEl);
|
|
51
54
|
continue;
|
|
52
55
|
}
|
|
53
56
|
const condition = conditions.shift();
|
|
54
57
|
if (condition !== parentRole?.name) {
|
|
55
58
|
return false;
|
|
56
59
|
}
|
|
57
|
-
parentEl = parentEl
|
|
60
|
+
parentEl = getComposedParentElement(parentEl);
|
|
58
61
|
}
|
|
59
62
|
return true;
|
|
60
63
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
*
|
|
4
|
+
* The parent of an element as the DOM sees it, for the algorithms that walk up from an element.
|
|
5
|
+
*
|
|
6
|
+
* The algorithms of this package are written against the DOM `Element`, where `parentElement`
|
|
7
|
+
* stops at the boundary of a tree: the parent element of a node whose parent is not an element
|
|
8
|
+
* (a document fragment, such as the content of a `template` element or a shadow root) is `null`.
|
|
9
|
+
* `@markuplint/ml-core` hands them its own elements, whose `parentElement` is the syntactical
|
|
10
|
+
* parent: the syntax tree has no other place for the content of a `template` element than its
|
|
11
|
+
* children, so the top of the content has the `template` element as its parent element. A walk
|
|
12
|
+
* that reads `parentElement` there leaves the content and enters the tree that holds the
|
|
13
|
+
* `template`. `getRootNode()` is where `@markuplint/ml-core` puts the boundary, and in a real DOM
|
|
14
|
+
* a parent is always in the tree of its child, so comparing the two roots gives the DOM's answer
|
|
15
|
+
* to both.
|
|
16
|
+
*
|
|
17
|
+
* This package cannot use `MLNode#treeParentElement` of `@markuplint/ml-core`, which depends on it,
|
|
18
|
+
* so the helpers derive the same answer from `getRootNode()`. The two are pinned to agree by
|
|
19
|
+
* `tree-scope.spec.ts` of `@markuplint/ml-core`: a change to the boundary in one belongs to both.
|
|
20
|
+
*
|
|
21
|
+
* Which of the two helpers a walk takes depends on what it follows:
|
|
22
|
+
*
|
|
23
|
+
* - {@link getTreeParentElement} follows a relation that the DOM defines in the tree, such as
|
|
24
|
+
* the ancestor `label` element of a labelable element (`findAncestorLabel`, the one walk of
|
|
25
|
+
* this package that does not follow the accessibility tree).
|
|
26
|
+
* - {@link getComposedParentElement} follows what the accessibility tree is built from. Its
|
|
27
|
+
* content is the flat tree: the content of a shadow root is rendered in its host, so the
|
|
28
|
+
* elements above it are the host and the ancestors of the host. The content of an ordinary
|
|
29
|
+
* `template` element is not rendered, and where it is inserted is not known from the markup,
|
|
30
|
+
* so a walk ends at its top.
|
|
31
|
+
*
|
|
32
|
+
* A `template` element that may have a declarative shadow root but whose attachment is not known
|
|
33
|
+
* from the markup (a host that is not in the file, a component, or a dynamic `shadowrootmode`),
|
|
34
|
+
* or that fails to attach one, is treated as an ordinary `template` element: a walk ends at the
|
|
35
|
+
* top of its content.
|
|
36
|
+
*
|
|
37
|
+
* Known limitation: the flat tree places the content of a shadow root where the `slot` element
|
|
38
|
+
* is, between the host and the nodes the host has as children. A walk goes from the top of the
|
|
39
|
+
* shadow root to the host and does not model the position of the `slot`.
|
|
40
|
+
*
|
|
41
|
+
* @see https://dom.spec.whatwg.org/#parent-element
|
|
42
|
+
* @see https://dom.spec.whatwg.org/#concept-shadow-including-ancestor
|
|
43
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* What the helpers read of an element: the DOM `Element`, and `MLElement` of `@markuplint/ml-core`.
|
|
47
|
+
* An element without `getRootNode` (an adapter that has no notion of a tree boundary) is in one
|
|
48
|
+
* tree with its parent element.
|
|
49
|
+
*/
|
|
50
|
+
type TreeElement = {
|
|
51
|
+
readonly parentElement: unknown;
|
|
52
|
+
getRootNode?(): unknown;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The parent element of an element in its own tree, which is what the DOM calls `parentElement`:
|
|
56
|
+
* `null` at the top of the content of a `template` element or of a shadow root, even where the
|
|
57
|
+
* `parentElement` of the element implementation is the `template` element.
|
|
58
|
+
*
|
|
59
|
+
* @template P - The type of the parent element the element reads
|
|
60
|
+
* @param el - The element to start from
|
|
61
|
+
* @returns The parent element, or `null` when the element is at the top of its tree
|
|
62
|
+
*/
|
|
63
|
+
export declare function getTreeParentElement<P extends TreeElement>(el: {
|
|
64
|
+
readonly parentElement: P | null;
|
|
65
|
+
getRootNode?(): unknown;
|
|
66
|
+
}): P | null;
|
|
67
|
+
/**
|
|
68
|
+
* The parent element of an element as the accessibility tree is built: the parent element in its own
|
|
69
|
+
* tree, or, at the top of a shadow root, the host of the shadow root. `null` at the top of the content
|
|
70
|
+
* of an ordinary `template` element, and at the top of a document or of any other root that is not a
|
|
71
|
+
* shadow root.
|
|
72
|
+
*
|
|
73
|
+
* @template P - The type of the parent element the element reads
|
|
74
|
+
* @param el - The element to start from
|
|
75
|
+
* @returns The parent element or the shadow host, or `null` when the walk ends
|
|
76
|
+
*/
|
|
77
|
+
export declare function getComposedParentElement<P extends TreeElement>(el: {
|
|
78
|
+
readonly parentElement: P | null;
|
|
79
|
+
getRootNode?(): unknown;
|
|
80
|
+
}): P | null;
|
|
81
|
+
/**
|
|
82
|
+
* Whether a root is a shadow root: a document fragment that has a host.
|
|
83
|
+
* The shadow root of `@markuplint/ml-core` is not a `ShadowRoot` of the DOM by class, and the
|
|
84
|
+
* content of an ordinary `template` element is a document fragment that has no host.
|
|
85
|
+
*
|
|
86
|
+
* @template P - The type of the host
|
|
87
|
+
* @param root - The root of a tree
|
|
88
|
+
* @returns `true` if the root has a host element
|
|
89
|
+
*/
|
|
90
|
+
export declare function isShadowRoot<P = unknown>(root: unknown): root is {
|
|
91
|
+
readonly host: P;
|
|
92
|
+
};
|
|
93
|
+
export {};
|