@markuplint/ml-spec 5.4.0 → 5.6.0

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