@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +4 -0
  3. package/lib/algorithm/aria/accname/aria-steps.d.ts +26 -9
  4. package/lib/algorithm/aria/accname/aria-steps.js +38 -9
  5. package/lib/algorithm/aria/accname/compute.d.ts +38 -6
  6. package/lib/algorithm/aria/accname/compute.js +60 -12
  7. package/lib/algorithm/aria/accname/element-names.d.ts +4 -4
  8. package/lib/algorithm/aria/accname/element-names.js +31 -30
  9. package/lib/algorithm/aria/accname/helpers.d.ts +52 -12
  10. package/lib/algorithm/aria/accname/helpers.js +78 -23
  11. package/lib/algorithm/aria/accname/index.d.ts +2 -2
  12. package/lib/algorithm/aria/accname/index.js +1 -1
  13. package/lib/algorithm/aria/accname/label-steps.d.ts +4 -4
  14. package/lib/algorithm/aria/accname/label-steps.js +14 -5
  15. package/lib/algorithm/aria/accname/result.d.ts +21 -0
  16. package/lib/algorithm/aria/accname/result.js +28 -0
  17. package/lib/algorithm/aria/accname/svg-helpers.d.ts +2 -2
  18. package/lib/algorithm/aria/accname/svg-helpers.js +2 -2
  19. package/lib/algorithm/aria/accname/types.d.ts +51 -0
  20. package/lib/algorithm/aria/accname-computation.d.ts +1 -1
  21. package/lib/algorithm/aria/accname-computation.js +12 -6
  22. package/lib/algorithm/aria/get-aria.js +2 -2
  23. package/lib/algorithm/aria/get-computed-role.js +18 -14
  24. package/lib/algorithm/aria/get-explicit-role.d.ts +6 -0
  25. package/lib/algorithm/aria/get-explicit-role.js +82 -9
  26. package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +2 -0
  27. package/lib/algorithm/aria/get-non-presentational-ancestor.js +5 -2
  28. package/lib/algorithm/aria/is-exposed.d.ts +3 -1
  29. package/lib/algorithm/aria/is-exposed.js +15 -7
  30. package/lib/algorithm/aria/is-presentational.d.ts +15 -4
  31. package/lib/algorithm/aria/is-presentational.js +16 -3
  32. package/lib/algorithm/aria/matches-context-role.d.ts +3 -1
  33. package/lib/algorithm/aria/matches-context-role.js +10 -7
  34. package/lib/algorithm/dom/tree-boundary.d.ts +93 -0
  35. package/lib/algorithm/dom/tree-boundary.js +103 -0
  36. package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +7 -3
  37. package/lib/algorithm/html/content-model-category-to-tag-names.js +7 -3
  38. package/lib/algorithm/html/select-display-size.d.ts +29 -0
  39. package/lib/algorithm/html/select-display-size.js +38 -0
  40. package/lib/const/accname.d.ts +1 -1
  41. package/lib/const/accname.js +1 -1
  42. package/lib/index.d.ts +2 -0
  43. package/lib/index.js +3 -0
  44. package/lib/types/index.d.ts +67 -2
  45. package/lib/types/permitted-structures.d.ts +5 -1
  46. package/lib/utils/directive-resolver.d.ts +8 -0
  47. package/lib/utils/directive-resolver.js +1 -0
  48. package/lib/utils/schema-to-spec.d.ts +4 -0
  49. package/lib/utils/schema-to-spec.js +42 -0
  50. package/package.json +6 -5
@@ -1,16 +1,24 @@
1
- import type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource } from './types.js';
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
- * - **Step 2F**: "If the current node's role allows name from content [...],
9
- * return the accumulated text of the current node's descendant nodes."
10
- * - **Step 2C (Embedded Controls)**: "If the current node is a descendant of
11
- * an `aria-labelledby` or `aria-label` reference AND the current node is
12
- * an embedded control, return the embedded control's value." Per spec,
13
- * `aria-label` is ignored for embedded controls during this traversal.
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://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2F
22
- * @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
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 resolveNameFromContent(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult, inLabelledbyTraversal: boolean): string;
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
- * - **Step 2F**: "If the current node's role allows name from content [...],
18
- * return the accumulated text of the current node's descendant nodes."
19
- * - **Step 2C (Embedded Controls)**: "If the current node is a descendant of
20
- * an `aria-labelledby` or `aria-label` reference AND the current node is
21
- * an embedded control, return the embedded control's value." Per spec,
22
- * `aria-label` is ignored for embedded controls during this traversal.
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://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2F
31
- * @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
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(getEmbeddedControlValue(childEl, resolver));
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(getEmbeddedControlValue(childEl, resolver));
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://www.w3.org/TR/accname-1.2/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
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.parentElement;
302
+ let current = getTreeParentElement(el);
259
303
  while (current) {
260
304
  if (current.localName === 'label') {
261
305
  return current;
262
306
  }
263
- current = current.parentElement;
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://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2E
7
- * @see https://www.w3.org/TR/html-aam-1.0/#el-input-text — HTML-AAM label association
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: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult, inLabelledbyTraversal: boolean): AccnameResult | null;
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 { flattenText, getChildNodes, makeResult, resolveLabel } from './helpers.js';
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://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2E
9
- * @see https://www.w3.org/TR/html-aam-1.0/#el-input-text — HTML-AAM label association
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 §5.1.1. This is a lightweight check that avoids the full AccName
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-1.0/#include_elements — SVG-AAM §5.1.1
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 §5.1.1. This is a lightweight check that avoids the full AccName
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-1.0/#include_elements — SVG-AAM §5.1.1
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://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
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://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
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
- * Walks up the DOM tree to find the root node (Document or DocumentFragment).
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
- let root = el;
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://www.w3.org/TR/wai-aria-1.2/#note-regarding-the-aria-1-1-none-role
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/#ref-for-image
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')) {