@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
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import type { AccnameElement, AccnameNode, AccnameResolver
|
|
1
|
+
import type { AccnameComputeFn, AccnameElement, AccnameNode, AccnameResolver } from './types.js';
|
|
2
2
|
export { EMBEDDED_CONTROL_ROLES } from '../../../const/index.js';
|
|
3
|
-
export declare function makeResult(name: string, source: AccnameSource | null): AccnameResult;
|
|
4
|
-
export declare function flattenText(text: string): string;
|
|
5
3
|
/**
|
|
6
4
|
* Implements AccName 1.2 §4.3.2 Steps 2F and 2C:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* **Step 2F**, the name from content:
|
|
7
|
+
*
|
|
8
|
+
* > Otherwise, if the current node's role allows name from content, or if the current node is referenced by
|
|
9
|
+
* > aria-labelledby, aria-describedby, or is a native host language text alternative element (e.g. label in HTML),
|
|
10
|
+
* > or is a descendant of a native host language text alternative element:
|
|
11
|
+
*
|
|
12
|
+
* **Step 2C**, the embedded controls:
|
|
13
|
+
*
|
|
14
|
+
* > Otherwise, if the current node is a control embedded within the label (e.g. any element directly referenced by
|
|
15
|
+
* > aria-labelledby) for another widget, where the user can adjust the embedded control's value, then return the
|
|
16
|
+
* > embedded control as part of the text alternative in the following manner:
|
|
17
|
+
*
|
|
18
|
+
* `aria-label` is ignored for an embedded control during this traversal:
|
|
19
|
+
*
|
|
20
|
+
* > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an
|
|
21
|
+
* > embedded control, ignore aria-label and skip to rule Embedded Control.
|
|
14
22
|
*
|
|
15
23
|
* **Limitation:** CSS-generated content (`::before`/`::after` with the `content`
|
|
16
24
|
* property) is not included. AccName 1.2 §4.3.2 Step 2G specifies that CSS
|
|
@@ -18,10 +26,22 @@ export declare function flattenText(text: string): string;
|
|
|
18
26
|
* performs static HTML analysis without CSS processing, so this content is
|
|
19
27
|
* unavailable at lint time.
|
|
20
28
|
*
|
|
21
|
-
* @see https://
|
|
22
|
-
* @see https://
|
|
29
|
+
* @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2 Step 2F
|
|
30
|
+
* @see https://w3c.github.io/accname/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveNameFromContent(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): string;
|
|
33
|
+
/**
|
|
34
|
+
* The text an embedded control reached by name-from-content recursion
|
|
35
|
+
* contributes, in the order of AccName §4.3.2: nothing when it is hidden
|
|
36
|
+
* (Step 2A), the text of its `aria-labelledby` outside an ongoing
|
|
37
|
+
* `aria-labelledby` traversal (Step 2B), otherwise its value (Step 2C), as its
|
|
38
|
+
* `aria-label` is ignored (Step 2D).
|
|
39
|
+
*
|
|
40
|
+
* @see https://w3c.github.io/accname/#comp_label
|
|
41
|
+
*
|
|
42
|
+
* > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an embedded control, ignore aria-label and skip to rule Embedded Control.
|
|
23
43
|
*/
|
|
24
|
-
export declare function
|
|
44
|
+
export declare function embeddedControlText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): string;
|
|
25
45
|
/**
|
|
26
46
|
* The child nodes to traverse for `el`: what the resolver says it renders,
|
|
27
47
|
* or `el.childNodes` when it says nothing.
|
|
@@ -34,10 +54,30 @@ export declare function getChildNodes(el: AccnameElement, resolver: AccnameResol
|
|
|
34
54
|
*/
|
|
35
55
|
export declare function getTextContent(el: AccnameElement, resolver: AccnameResolver): string;
|
|
36
56
|
export declare function findChildByLocalName(el: AccnameElement, localName: string, resolver?: AccnameResolver): AccnameElement | null;
|
|
57
|
+
/**
|
|
58
|
+
* The closest `label` element among the ancestors of the element, which names the element when it is
|
|
59
|
+
* its labeled control. The ancestors are those of its own tree, as the HTML Standard defines the
|
|
60
|
+
* relation between a `label` and its labeled control by ancestors in the tree, so a `label` that
|
|
61
|
+
* wraps a `template` element does not label a control in its content.
|
|
62
|
+
*
|
|
63
|
+
* @see https://html.spec.whatwg.org/multipage/forms.html#labeled-control
|
|
64
|
+
*
|
|
65
|
+
* @param el - The element to start from
|
|
66
|
+
* @returns The closest `label` ancestor, or `null`
|
|
67
|
+
*/
|
|
37
68
|
export declare function findAncestorLabel(el: AccnameElement): AccnameElement | null;
|
|
38
69
|
export declare function resolveLabel(el: AccnameElement, resolver: AccnameResolver): readonly AccnameElement[];
|
|
39
70
|
export declare function getInputType(el: AccnameElement): string;
|
|
40
71
|
export declare function isSvgElement(el: AccnameElement): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the element is an SVG `title` or `desc`. They are never rendered,
|
|
74
|
+
* so their text is not content of the parent: a `title` gives the name only
|
|
75
|
+
* through Step 2E of its parent (`handleSvgElement`), which an element exposed
|
|
76
|
+
* as presentational skips.
|
|
77
|
+
*
|
|
78
|
+
* @see https://svgwg.org/svg2-draft/render.html#TermNeverRenderedElement
|
|
79
|
+
*/
|
|
80
|
+
export declare function isSvgDescriptiveElement(el: AccnameElement): boolean;
|
|
41
81
|
/**
|
|
42
82
|
* Escapes a string for safe use inside a CSS selector.
|
|
43
83
|
*
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
2
|
import { ELEMENT_NODE, SVG_NAMESPACE, TEXT_NODE, TEXT_LIKE_INPUT_TYPES } from '../../../const/index.js';
|
|
3
|
+
import { getTreeParentElement } from '../../dom/tree-boundary.js';
|
|
4
|
+
import { resolveAriaLabelledby } from './aria-steps.js';
|
|
3
5
|
export { EMBEDDED_CONTROL_ROLES } from '../../../const/index.js';
|
|
4
|
-
export function makeResult(name, source) {
|
|
5
|
-
const trimmed = flattenText(name);
|
|
6
|
-
return {
|
|
7
|
-
name: trimmed,
|
|
8
|
-
source: trimmed ? source : null,
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
export function flattenText(text) {
|
|
12
|
-
return text.replaceAll(/\s+/g, ' ').trim();
|
|
13
|
-
}
|
|
14
6
|
/**
|
|
15
7
|
* Implements AccName 1.2 §4.3.2 Steps 2F and 2C:
|
|
16
8
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
9
|
+
* **Step 2F**, the name from content:
|
|
10
|
+
*
|
|
11
|
+
* > Otherwise, if the current node's role allows name from content, or if the current node is referenced by
|
|
12
|
+
* > aria-labelledby, aria-describedby, or is a native host language text alternative element (e.g. label in HTML),
|
|
13
|
+
* > or is a descendant of a native host language text alternative element:
|
|
14
|
+
*
|
|
15
|
+
* **Step 2C**, the embedded controls:
|
|
16
|
+
*
|
|
17
|
+
* > Otherwise, if the current node is a control embedded within the label (e.g. any element directly referenced by
|
|
18
|
+
* > aria-labelledby) for another widget, where the user can adjust the embedded control's value, then return the
|
|
19
|
+
* > embedded control as part of the text alternative in the following manner:
|
|
20
|
+
*
|
|
21
|
+
* `aria-label` is ignored for an embedded control during this traversal:
|
|
22
|
+
*
|
|
23
|
+
* > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an
|
|
24
|
+
* > embedded control, ignore aria-label and skip to rule Embedded Control.
|
|
23
25
|
*
|
|
24
26
|
* **Limitation:** CSS-generated content (`::before`/`::after` with the `content`
|
|
25
27
|
* property) is not included. AccName 1.2 §4.3.2 Step 2G specifies that CSS
|
|
@@ -27,8 +29,8 @@ export function flattenText(text) {
|
|
|
27
29
|
* performs static HTML analysis without CSS processing, so this content is
|
|
28
30
|
* unavailable at lint time.
|
|
29
31
|
*
|
|
30
|
-
* @see https://
|
|
31
|
-
* @see https://
|
|
32
|
+
* @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2 Step 2F
|
|
33
|
+
* @see https://w3c.github.io/accname/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
|
|
32
34
|
*/
|
|
33
35
|
export function resolveNameFromContent(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
34
36
|
const parts = [];
|
|
@@ -38,9 +40,12 @@ export function resolveNameFromContent(el, resolver, visited, computeFn, inLabel
|
|
|
38
40
|
}
|
|
39
41
|
else if (child.nodeType === ELEMENT_NODE) {
|
|
40
42
|
const childEl = child;
|
|
43
|
+
if (isSvgDescriptiveElement(childEl)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
41
46
|
if (resolver.isEmbeddedControl(childEl)) {
|
|
42
47
|
// Step 2C: Embedded controls — use value directly, aria-label is ignored
|
|
43
|
-
parts.push(
|
|
48
|
+
parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
|
|
44
49
|
}
|
|
45
50
|
else {
|
|
46
51
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
@@ -84,8 +89,11 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
|
|
|
84
89
|
}
|
|
85
90
|
else if (child.nodeType === ELEMENT_NODE) {
|
|
86
91
|
const childEl = child;
|
|
92
|
+
if (isSvgDescriptiveElement(childEl)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
87
95
|
if (resolver.isEmbeddedControl(childEl)) {
|
|
88
|
-
parts.push(
|
|
96
|
+
parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
|
|
89
97
|
}
|
|
90
98
|
else {
|
|
91
99
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
@@ -100,6 +108,31 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
|
|
|
100
108
|
}
|
|
101
109
|
return parts.join(' ');
|
|
102
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The text an embedded control reached by name-from-content recursion
|
|
113
|
+
* contributes, in the order of AccName §4.3.2: nothing when it is hidden
|
|
114
|
+
* (Step 2A), the text of its `aria-labelledby` outside an ongoing
|
|
115
|
+
* `aria-labelledby` traversal (Step 2B), otherwise its value (Step 2C), as its
|
|
116
|
+
* `aria-label` is ignored (Step 2D).
|
|
117
|
+
*
|
|
118
|
+
* @see https://w3c.github.io/accname/#comp_label
|
|
119
|
+
*
|
|
120
|
+
* > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an embedded control, ignore aria-label and skip to rule Embedded Control.
|
|
121
|
+
*/
|
|
122
|
+
export function embeddedControlText(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
123
|
+
const hidden = resolver.isHidden(el);
|
|
124
|
+
resolver.onVisit?.(el, { hidden, embeddedControl: true, inLabelledbyTraversal });
|
|
125
|
+
if (hidden) {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
if (!inLabelledbyTraversal) {
|
|
129
|
+
const labelledby = resolveAriaLabelledby(el, resolver, visited, computeFn);
|
|
130
|
+
if (labelledby) {
|
|
131
|
+
return labelledby.name;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return getEmbeddedControlValue(el, resolver);
|
|
135
|
+
}
|
|
103
136
|
/**
|
|
104
137
|
* Implements AccName 1.2 §4.3.2 Step 2C embedded control value resolution:
|
|
105
138
|
* - **Range controls** (slider/spinbutton/input[range]):
|
|
@@ -108,7 +141,7 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
|
|
|
108
141
|
* value attr → textContent
|
|
109
142
|
* - **Listbox** (select): selected option text (static analysis approximation)
|
|
110
143
|
*
|
|
111
|
-
* @see https://
|
|
144
|
+
* @see https://w3c.github.io/accname/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
|
|
112
145
|
*/
|
|
113
146
|
function getEmbeddedControlValue(el, resolver) {
|
|
114
147
|
const role = el.getAttribute('role')?.trim().split(/\s+/)[0];
|
|
@@ -254,13 +287,24 @@ export function findChildByLocalName(el, localName, resolver) {
|
|
|
254
287
|
}
|
|
255
288
|
return null;
|
|
256
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* The closest `label` element among the ancestors of the element, which names the element when it is
|
|
292
|
+
* its labeled control. The ancestors are those of its own tree, as the HTML Standard defines the
|
|
293
|
+
* relation between a `label` and its labeled control by ancestors in the tree, so a `label` that
|
|
294
|
+
* wraps a `template` element does not label a control in its content.
|
|
295
|
+
*
|
|
296
|
+
* @see https://html.spec.whatwg.org/multipage/forms.html#labeled-control
|
|
297
|
+
*
|
|
298
|
+
* @param el - The element to start from
|
|
299
|
+
* @returns The closest `label` ancestor, or `null`
|
|
300
|
+
*/
|
|
257
301
|
export function findAncestorLabel(el) {
|
|
258
|
-
let current = el
|
|
302
|
+
let current = getTreeParentElement(el);
|
|
259
303
|
while (current) {
|
|
260
304
|
if (current.localName === 'label') {
|
|
261
305
|
return current;
|
|
262
306
|
}
|
|
263
|
-
current = current
|
|
307
|
+
current = getTreeParentElement(current);
|
|
264
308
|
}
|
|
265
309
|
return null;
|
|
266
310
|
}
|
|
@@ -287,6 +331,17 @@ export function getInputType(el) {
|
|
|
287
331
|
export function isSvgElement(el) {
|
|
288
332
|
return el.namespaceURI === SVG_NAMESPACE;
|
|
289
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Whether the element is an SVG `title` or `desc`. They are never rendered,
|
|
336
|
+
* so their text is not content of the parent: a `title` gives the name only
|
|
337
|
+
* through Step 2E of its parent (`handleSvgElement`), which an element exposed
|
|
338
|
+
* as presentational skips.
|
|
339
|
+
*
|
|
340
|
+
* @see https://svgwg.org/svg2-draft/render.html#TermNeverRenderedElement
|
|
341
|
+
*/
|
|
342
|
+
export function isSvgDescriptiveElement(el) {
|
|
343
|
+
return isSvgElement(el) && (el.localName === 'title' || el.localName === 'desc');
|
|
344
|
+
}
|
|
290
345
|
/**
|
|
291
346
|
* Escapes a string for safe use inside a CSS selector.
|
|
292
347
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { computeAccessibleName } from './compute.js';
|
|
1
|
+
export { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative } from './compute.js';
|
|
2
2
|
export { escapeCSS } from './helpers.js';
|
|
3
3
|
export { hasSvgAccessibleNameSource } from './svg-helpers.js';
|
|
4
|
-
export type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource } from './types.js';
|
|
4
|
+
export type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource, AccnameVisit, } from './types.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { computeAccessibleName } from './compute.js';
|
|
1
|
+
export { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative } from './compute.js';
|
|
2
2
|
export { escapeCSS } from './helpers.js';
|
|
3
3
|
export { hasSvgAccessibleNameSource } from './svg-helpers.js';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
1
|
+
import type { AccnameComputeFn, AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Part of AccName 1.2 §4.3.2 Step 2E — for labelable elements, the HTML label
|
|
4
4
|
* association is checked before other element-specific rules.
|
|
5
5
|
*
|
|
6
|
-
* @see https://
|
|
7
|
-
* @see https://www.w3.org/TR/html-aam
|
|
6
|
+
* @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2 Step 2E
|
|
7
|
+
* @see https://www.w3.org/TR/html-aam/#el-input-text — HTML-AAM label association
|
|
8
8
|
*/
|
|
9
|
-
export declare function resolveLabelText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn:
|
|
9
|
+
export declare function resolveLabelText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): AccnameResult | null;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
2
|
import { ELEMENT_NODE, TEXT_NODE } from '../../../const/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { embeddedControlText, getChildNodes, isSvgDescriptiveElement, resolveLabel } from './helpers.js';
|
|
4
|
+
import { flattenText, makeResult } from './result.js';
|
|
4
5
|
/**
|
|
5
6
|
* Part of AccName 1.2 §4.3.2 Step 2E — for labelable elements, the HTML label
|
|
6
7
|
* association is checked before other element-specific rules.
|
|
7
8
|
*
|
|
8
|
-
* @see https://
|
|
9
|
-
* @see https://www.w3.org/TR/html-aam
|
|
9
|
+
* @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2 Step 2E
|
|
10
|
+
* @see https://www.w3.org/TR/html-aam/#el-input-text — HTML-AAM label association
|
|
10
11
|
*/
|
|
11
12
|
export function resolveLabelText(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
12
13
|
const labels = resolveLabel(el, resolver);
|
|
@@ -30,6 +31,10 @@ export function resolveLabelText(el, resolver, visited, computeFn, inLabelledbyT
|
|
|
30
31
|
* Excludes the labeled element itself to prevent circular inclusion. Parts are
|
|
31
32
|
* joined with a space separator, matching the AccName 1.2 concatenation
|
|
32
33
|
* behavior for label text.
|
|
34
|
+
*
|
|
35
|
+
* The children are treated as name-from-content recursion treats them
|
|
36
|
+
* (`resolveNameFromContent`): a hidden one and an SVG `title` or `desc`
|
|
37
|
+
* contribute nothing, and an embedded control contributes its value.
|
|
33
38
|
*/
|
|
34
39
|
function collectLabelText(label, labeledElement, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
35
40
|
const parts = [];
|
|
@@ -39,14 +44,18 @@ function collectLabelText(label, labeledElement, resolver, visited, computeFn, i
|
|
|
39
44
|
}
|
|
40
45
|
else if (child.nodeType === ELEMENT_NODE) {
|
|
41
46
|
const childEl = child;
|
|
42
|
-
if (childEl === labeledElement) {
|
|
47
|
+
if (childEl === labeledElement || isSvgDescriptiveElement(childEl)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (resolver.isEmbeddedControl(childEl)) {
|
|
51
|
+
parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
|
|
43
52
|
continue;
|
|
44
53
|
}
|
|
45
54
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
46
55
|
if (result.name) {
|
|
47
56
|
parts.push(result.name);
|
|
48
57
|
}
|
|
49
|
-
else {
|
|
58
|
+
else if (!resolver.isHidden(childEl)) {
|
|
50
59
|
// Transparent traversal: when the child element has no accessible name
|
|
51
60
|
// (e.g., <span> with role="generic"), recursively collect its descendant text.
|
|
52
61
|
// This matches the behavior of resolveNameFromContent's fallback.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result builders of the accessible name computation.
|
|
3
|
+
*
|
|
4
|
+
* A module of their own, not part of `helpers.ts`: `aria-steps.ts` builds
|
|
5
|
+
* results, and `helpers.ts` runs Step 2B for an embedded control through
|
|
6
|
+
* `aria-steps.ts`, so keeping them in `helpers.ts` would make the two import
|
|
7
|
+
* each other.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { AccnameResult, AccnameSource } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Builds a result from a name, flattened; an empty name has no source.
|
|
14
|
+
*/
|
|
15
|
+
export declare function makeResult(name: string, source: AccnameSource | null): AccnameResult;
|
|
16
|
+
/**
|
|
17
|
+
* Makes a flat string: whitespace runs become a single space, and the ends are trimmed.
|
|
18
|
+
*
|
|
19
|
+
* @see https://w3c.github.io/accname/#terminology
|
|
20
|
+
*/
|
|
21
|
+
export declare function flattenText(text: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result builders of the accessible name computation.
|
|
3
|
+
*
|
|
4
|
+
* A module of their own, not part of `helpers.ts`: `aria-steps.ts` builds
|
|
5
|
+
* results, and `helpers.ts` runs Step 2B for an embedded control through
|
|
6
|
+
* `aria-steps.ts`, so keeping them in `helpers.ts` would make the two import
|
|
7
|
+
* each other.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Builds a result from a name, flattened; an empty name has no source.
|
|
13
|
+
*/
|
|
14
|
+
export function makeResult(name, source) {
|
|
15
|
+
const trimmed = flattenText(name);
|
|
16
|
+
return {
|
|
17
|
+
name: trimmed,
|
|
18
|
+
source: trimmed ? source : null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Makes a flat string: whitespace runs become a single space, and the ends are trimmed.
|
|
23
|
+
*
|
|
24
|
+
* @see https://w3c.github.io/accname/#terminology
|
|
25
|
+
*/
|
|
26
|
+
export function flattenText(text) {
|
|
27
|
+
return text.replaceAll(/\s+/g, ' ').trim();
|
|
28
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { AccnameElement } from './types.js';
|
|
|
4
4
|
* the full name computation.
|
|
5
5
|
*
|
|
6
6
|
* Used by `get-computed-role.ts` for SVG accessibility tree inclusion decisions
|
|
7
|
-
* per SVG-AAM §
|
|
7
|
+
* per SVG-AAM §4.1.2. This is a lightweight check that avoids the full AccName
|
|
8
8
|
* algorithm, breaking the circular dependency between role computation and
|
|
9
9
|
* name computation.
|
|
10
10
|
*
|
|
@@ -19,6 +19,6 @@ import type { AccnameElement } from './types.js';
|
|
|
19
19
|
*
|
|
20
20
|
* @param el - The SVG element to check for accessible name sources
|
|
21
21
|
* @returns True if the element has aria-label, aria-labelledby, or a title/desc child
|
|
22
|
-
* @see https://www.w3.org/TR/svg-aam
|
|
22
|
+
* @see https://www.w3.org/TR/svg-aam/#include_elements — SVG-AAM §4.1.2
|
|
23
23
|
*/
|
|
24
24
|
export declare function hasSvgAccessibleNameSource(el: AccnameElement): boolean;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* the full name computation.
|
|
5
5
|
*
|
|
6
6
|
* Used by `get-computed-role.ts` for SVG accessibility tree inclusion decisions
|
|
7
|
-
* per SVG-AAM §
|
|
7
|
+
* per SVG-AAM §4.1.2. This is a lightweight check that avoids the full AccName
|
|
8
8
|
* algorithm, breaking the circular dependency between role computation and
|
|
9
9
|
* name computation.
|
|
10
10
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* @param el - The SVG element to check for accessible name sources
|
|
21
21
|
* @returns True if the element has aria-label, aria-labelledby, or a title/desc child
|
|
22
|
-
* @see https://www.w3.org/TR/svg-aam
|
|
22
|
+
* @see https://www.w3.org/TR/svg-aam/#include_elements — SVG-AAM §4.1.2
|
|
23
23
|
*/
|
|
24
24
|
export function hasSvgAccessibleNameSource(el) {
|
|
25
25
|
if (el.getAttribute('aria-label')?.trim()) {
|
|
@@ -20,6 +20,13 @@ export interface AccnameElement extends AccnameNode {
|
|
|
20
20
|
getAttribute(name: string): string | null;
|
|
21
21
|
hasAttribute(name: string): boolean;
|
|
22
22
|
readonly parentElement: AccnameElement | null;
|
|
23
|
+
/**
|
|
24
|
+
* The root of the tree of the element. An element and its parent element are in the same tree,
|
|
25
|
+
* unless the parent element is only the syntactical parent, as `MLElement#parentElement` is for the
|
|
26
|
+
* top of the content of a `template` element: see `getTreeParentElement`. An implementation without
|
|
27
|
+
* it has no tree boundary.
|
|
28
|
+
*/
|
|
29
|
+
getRootNode?(): unknown;
|
|
23
30
|
readonly children: Iterable<AccnameElement>;
|
|
24
31
|
readonly childNodes: Iterable<AccnameNode>;
|
|
25
32
|
}
|
|
@@ -42,6 +49,19 @@ export interface AccnameResolver {
|
|
|
42
49
|
* in name-from-content computation (textbox, combobox, listbox, spinbutton, slider, searchbox).
|
|
43
50
|
*/
|
|
44
51
|
isEmbeddedControl(el: AccnameElement): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Checks if an element is exposed with an explicit `none` or `presentation`
|
|
54
|
+
* role, so that Step 2E does not use its host language text alternative.
|
|
55
|
+
* When absent, no element is treated so.
|
|
56
|
+
*/
|
|
57
|
+
isExposedAsPresentational?(el: AccnameElement): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Called for every element the computation reaches, before Step 2A, so that
|
|
60
|
+
* a caller can tell which attributes of a descendant took part in a name.
|
|
61
|
+
* An element that the computation does not reach (the subtree of a hidden
|
|
62
|
+
* element, or of one named before content) is never reported.
|
|
63
|
+
*/
|
|
64
|
+
onVisit?(el: AccnameElement, visit: AccnameVisit): void;
|
|
45
65
|
/**
|
|
46
66
|
* Returns a pre-computed name for an element, bypassing the standard algorithm.
|
|
47
67
|
* Used by ml-core for Pretender integration where framework components provide
|
|
@@ -53,6 +73,11 @@ export interface AccnameResolver {
|
|
|
53
73
|
* Used by ml-core for a pretended component: it renders its own elements around
|
|
54
74
|
* the children given at the usage site (`contents` of the pretender), and those
|
|
55
75
|
* can carry the accessible name (`<button><img alt="Save"></button>`).
|
|
76
|
+
* Also the rendered children, when they are not the syntactical ones: the
|
|
77
|
+
* child nodes of an attached shadow root, and none for an ordinary
|
|
78
|
+
* `template` element, whose content is not rendered where it is written.
|
|
79
|
+
* An element whose shadow root is only maybe attached is given none as
|
|
80
|
+
* well, because that root is not there to read.
|
|
56
81
|
* Consulted by name-from-content, label text, the legend/caption/SVG `title`
|
|
57
82
|
* lookups, the `<select>` options, and wherever the computation reads an
|
|
58
83
|
* element's text (`getTextContent`): an option, an SVG `title`, the value of
|
|
@@ -60,6 +85,32 @@ export interface AccnameResolver {
|
|
|
60
85
|
*/
|
|
61
86
|
getChildNodes?(el: AccnameElement): Iterable<AccnameNode>;
|
|
62
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The recursive entry of the computation (`computeAccessibleNameInternal`),
|
|
90
|
+
* handed to the steps that compute the text alternative of another node.
|
|
91
|
+
*/
|
|
92
|
+
export type AccnameComputeFn = (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult;
|
|
93
|
+
/**
|
|
94
|
+
* How the computation reached an element (see `AccnameResolver.onVisit`).
|
|
95
|
+
*/
|
|
96
|
+
export interface AccnameVisit {
|
|
97
|
+
/**
|
|
98
|
+
* Whether Step 2A found the element hidden, which ends the computation of
|
|
99
|
+
* it. Step 2A does not apply inside an `aria-labelledby` traversal whose
|
|
100
|
+
* directly referenced node is hidden, so no element is found hidden there.
|
|
101
|
+
*/
|
|
102
|
+
readonly hidden: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the element is an embedded control reached by name-from-content
|
|
105
|
+
* recursion (Step 2C), whose value is used and whose `aria-label` is ignored.
|
|
106
|
+
*/
|
|
107
|
+
readonly embeddedControl: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Whether the element is part of an ongoing `aria-labelledby` traversal, in
|
|
110
|
+
* which Step 2B is not applied.
|
|
111
|
+
*/
|
|
112
|
+
readonly inLabelledbyTraversal: boolean;
|
|
113
|
+
}
|
|
63
114
|
/**
|
|
64
115
|
* Identifies the source of an element's accessible name per HTML-AAM §4.1.
|
|
65
116
|
* Used for diagnostic purposes (e.g., reporting which source provided the name).
|
|
@@ -14,6 +14,6 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
|
14
14
|
* @param specs - The ML specification data for role resolution
|
|
15
15
|
* @param version - The ARIA version to use for role computation
|
|
16
16
|
* @returns The computed accessible name string, or an empty string if none is found
|
|
17
|
-
* @see https://
|
|
17
|
+
* @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2
|
|
18
18
|
*/
|
|
19
19
|
export declare function getAccname(el: Element, specs: MLMLSpec, version: ARIAVersion): string;
|
|
@@ -2,6 +2,7 @@ import { computeAccessibleName } from './accname/compute.js';
|
|
|
2
2
|
import { EMBEDDED_CONTROL_ROLES, escapeCSS } from './accname/helpers.js';
|
|
3
3
|
import { isNativeEmbeddedControl } from '../../const/index.js';
|
|
4
4
|
import { getComputedRole } from './get-computed-role.js';
|
|
5
|
+
import { isExposedAsPresentational } from './is-presentational.js';
|
|
5
6
|
/**
|
|
6
7
|
* [Implementation-specific] Reentrant guard to prevent infinite recursion when
|
|
7
8
|
* getComputedRole evaluates `:aria(has name)` selectors, which call back into getAccname.
|
|
@@ -30,7 +31,7 @@ const computingElements = new WeakSet();
|
|
|
30
31
|
* @param specs - The ML specification data for role resolution
|
|
31
32
|
* @param version - The ARIA version to use for role computation
|
|
32
33
|
* @returns The computed accessible name string, or an empty string if none is found
|
|
33
|
-
* @see https://
|
|
34
|
+
* @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2
|
|
34
35
|
*/
|
|
35
36
|
export function getAccname(
|
|
36
37
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -121,18 +122,23 @@ el, specs, version) {
|
|
|
121
122
|
// Native HTML embedded controls without explicit role
|
|
122
123
|
return isNativeEmbeddedControl(target);
|
|
123
124
|
},
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
126
|
+
isExposedAsPresentational(target) {
|
|
127
|
+
// TODO: Remove cast when getComputedRole accepts AccnameElement (#3178)
|
|
128
|
+
return isExposedAsPresentational(getComputedRole(specs, target, version));
|
|
129
|
+
},
|
|
124
130
|
};
|
|
125
131
|
}
|
|
126
132
|
/**
|
|
127
|
-
*
|
|
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.
|
|
128
137
|
*/
|
|
129
138
|
function getRootNode(
|
|
130
139
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
131
140
|
el) {
|
|
132
|
-
|
|
133
|
-
while (root.parentNode) {
|
|
134
|
-
root = root.parentNode;
|
|
135
|
-
}
|
|
141
|
+
const root = el.getRootNode();
|
|
136
142
|
if (root.nodeType === 9 || root.nodeType === 11) {
|
|
137
143
|
return root;
|
|
138
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')) {
|