@markuplint/ml-spec 4.10.2 → 5.0.0-alpha.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 (75) hide show
  1. package/ARCHITECTURE.ja.md +24 -10
  2. package/ARCHITECTURE.md +24 -10
  3. package/CHANGELOG.md +18 -0
  4. package/README.md +2 -2
  5. package/docs/aria-algorithms.ja.md +180 -33
  6. package/docs/aria-algorithms.md +183 -34
  7. package/docs/html-algorithms.ja.md +2 -2
  8. package/docs/html-algorithms.md +2 -2
  9. package/docs/maintenance.ja.md +13 -22
  10. package/docs/maintenance.md +13 -22
  11. package/docs/spec-resolution.ja.md +37 -9
  12. package/docs/spec-resolution.md +45 -16
  13. package/docs/type-definitions.ja.md +8 -4
  14. package/docs/type-definitions.md +8 -4
  15. package/lib/algorithm/aria/accname/__tests__/test-helpers.d.ts +45 -0
  16. package/lib/algorithm/aria/accname/__tests__/test-helpers.js +120 -0
  17. package/lib/algorithm/aria/accname/aria-steps.d.ts +51 -0
  18. package/lib/algorithm/aria/accname/aria-steps.js +104 -0
  19. package/lib/algorithm/aria/accname/compute.d.ts +51 -0
  20. package/lib/algorithm/aria/accname/compute.js +101 -0
  21. package/lib/algorithm/aria/accname/element-names.d.ts +36 -0
  22. package/lib/algorithm/aria/accname/element-names.js +342 -0
  23. package/lib/algorithm/aria/accname/helpers.d.ts +98 -0
  24. package/lib/algorithm/aria/accname/helpers.js +330 -0
  25. package/lib/algorithm/aria/accname/index.d.ts +4 -0
  26. package/lib/algorithm/aria/accname/index.js +3 -0
  27. package/lib/algorithm/aria/accname/label-steps.d.ts +25 -0
  28. package/lib/algorithm/aria/accname/label-steps.js +66 -0
  29. package/lib/algorithm/aria/accname/svg-helpers.d.ts +17 -0
  30. package/lib/algorithm/aria/accname/svg-helpers.js +30 -0
  31. package/lib/algorithm/aria/accname/types.d.ts +70 -0
  32. package/lib/algorithm/aria/accname/types.js +2 -0
  33. package/lib/algorithm/aria/accname-computation.d.ts +14 -3
  34. package/lib/algorithm/aria/accname-computation.js +131 -8
  35. package/lib/algorithm/aria/aria-specs.d.ts +1 -0
  36. package/lib/algorithm/aria/get-aria.js +30 -4
  37. package/lib/algorithm/aria/get-computed-role.js +106 -26
  38. package/lib/algorithm/aria/get-explicit-role.d.ts +12 -0
  39. package/lib/algorithm/aria/get-explicit-role.js +12 -0
  40. package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +12 -0
  41. package/lib/algorithm/aria/get-non-presentational-ancestor.js +14 -2
  42. package/lib/algorithm/aria/get-permitted-roles-spec.d.ts +8 -4
  43. package/lib/algorithm/aria/get-permitted-roles-spec.js +19 -6
  44. package/lib/algorithm/aria/get-role-spec.js +10 -3
  45. package/lib/algorithm/aria/has-required-owned-elements.d.ts +2 -1
  46. package/lib/algorithm/aria/has-required-owned-elements.js +18 -15
  47. package/lib/algorithm/aria/is-presentational.d.ts +24 -0
  48. package/lib/algorithm/aria/is-presentational.js +31 -0
  49. package/lib/algorithm/aria/matches-context-role.d.ts +12 -4
  50. package/lib/algorithm/aria/matches-context-role.js +39 -6
  51. package/lib/algorithm/html/content-model-category-to-tag-names.js +1 -1
  52. package/lib/algorithm/html/get-content-model.d.ts +4 -2
  53. package/lib/algorithm/html/get-content-model.js +6 -7
  54. package/lib/const/accname.d.ts +29 -0
  55. package/lib/const/accname.js +76 -0
  56. package/lib/const/dom.d.ts +8 -0
  57. package/lib/const/dom.js +8 -0
  58. package/lib/const/index.d.ts +2 -0
  59. package/lib/const/index.js +2 -0
  60. package/lib/index.d.ts +3 -0
  61. package/lib/index.js +4 -0
  62. package/lib/types/index.d.ts +65 -0
  63. package/lib/utils/directive-resolver.d.ts +23 -0
  64. package/lib/utils/directive-resolver.js +50 -0
  65. package/lib/utils/get-attr-specs-spec.js +4 -3
  66. package/lib/utils/get-ns.d.ts +7 -0
  67. package/lib/utils/get-ns.js +7 -0
  68. package/lib/utils/get-spec-by-tag-name.d.ts +1 -1
  69. package/lib/utils/merge-array.d.ts +10 -0
  70. package/lib/utils/merge-array.js +10 -0
  71. package/lib/utils/resolve-version.d.ts +11 -0
  72. package/lib/utils/resolve-version.js +11 -0
  73. package/lib/utils/schema-to-spec.d.ts +2 -0
  74. package/lib/utils/schema-to-spec.js +15 -4
  75. package/package.json +9 -7
@@ -1,18 +1,141 @@
1
- // @ts-ignore
2
- import { computeAccessibleName } from 'dom-accessibility-api';
1
+ import { computeAccessibleName } from './accname/compute.js';
2
+ import { EMBEDDED_CONTROL_ROLES, escapeCSS } from './accname/helpers.js';
3
+ import { isNativeEmbeddedControl } from '../../const/index.js';
4
+ import { getComputedRole } from './get-computed-role.js';
3
5
  /**
4
- * Computes the accessible name for an element using the WAI-ARIA accessible name computation algorithm.
5
- * Falls back to the placeholder attribute value for input elements when no accessible name is found.
6
+ * [Implementation-specific] Reentrant guard to prevent infinite recursion when
7
+ * getComputedRole evaluates `:aria(has name)` selectors, which call back into getAccname.
8
+ *
9
+ * This breaks the cycle: `getAccname → createDomResolver → allowsNameFromContent
10
+ * → getComputedRole → getARIA → matches(":aria(has name)") → getAccname`.
11
+ *
12
+ * Module-level state is intentional: the guard must span the entire call stack
13
+ * within a single synchronous computation. `WeakSet` ensures no memory leaks
14
+ * (entries are GC'd with their Element), and `try/finally` in `getAccname`
15
+ * guarantees cleanup even on exceptions.
16
+ */
17
+ const computingElements = new WeakSet();
18
+ /**
19
+ * Computes the accessible name for a DOM `Element` using the AccName 1.2 algorithm.
20
+ *
21
+ * This is the public facade that bridges the pure AccName algorithm (in `accname/compute.ts`)
22
+ * with the DOM environment. It creates a DOM-based `AccnameResolver` and delegates to
23
+ * `computeAccessibleName`.
24
+ *
25
+ * **Reentrant guard**: Uses a `WeakSet<Element>` to detect and short-circuit
26
+ * recursive calls caused by the `:aria(has name)` pseudo-class selector in
27
+ * `getComputedRole` → `getARIA` → `matches` chain.
6
28
  *
7
29
  * @param el - The DOM element to compute the accessible name for
30
+ * @param specs - The ML specification data for role resolution
31
+ * @param version - The ARIA version to use for role computation
8
32
  * @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
9
34
  */
10
35
  export function getAccname(
11
36
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
37
+ el, specs, version) {
38
+ if (computingElements.has(el)) {
39
+ return '';
40
+ }
41
+ computingElements.add(el);
42
+ const resolver = createDomResolver(el, specs, version);
43
+ // try/finally ensures the reentrant guard (WeakSet) is cleaned up even
44
+ // when computeAccessibleName throws. This is NOT a try/catch — exceptions
45
+ // propagate to the caller (ml-core's getAccname catches them).
46
+ try {
47
+ const result = computeAccessibleName(el, resolver);
48
+ return result.name;
49
+ }
50
+ finally {
51
+ computingElements.delete(el);
52
+ }
53
+ }
54
+ /**
55
+ * Creates an `AccnameResolver` from a DOM Element's owner document.
56
+ *
57
+ * [Implementation-specific] This resolver bridges the pure AccName algorithm
58
+ * with the DOM API. It uses `getComputedRole` for spec-driven role resolution
59
+ * (determining `allowsNameFromContent` and `isEmbeddedControl`) instead of
60
+ * hardcoded role lists, ensuring accuracy across ARIA versions.
61
+ */
62
+ function createDomResolver(
63
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
64
+ el, specs, version) {
65
+ const root = getRootNode(el);
66
+ return {
67
+ getElementById(id) {
68
+ if ('getElementById' in root && typeof root.getElementById === 'function') {
69
+ return root.getElementById(id);
70
+ }
71
+ return root.querySelector(`[id="${escapeCSS(id)}"]`) ?? null;
72
+ },
73
+ getLabelsForId(id) {
74
+ const labels = root.querySelectorAll(`label[for="${escapeCSS(id)}"]`);
75
+ return [...labels];
76
+ },
77
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
78
+ allowsNameFromContent(target) {
79
+ // TODO: Remove cast when getComputedRole accepts AccnameElement (#3178)
80
+ const role = getComputedRole(specs, target, version);
81
+ return !!role.role?.accessibleNameFromContent;
82
+ },
83
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
84
+ isHidden(target) {
85
+ if (target.getAttribute('aria-hidden') === 'true') {
86
+ return true;
87
+ }
88
+ if (target.hasAttribute('hidden')) {
89
+ return true;
90
+ }
91
+ // Best-effort CSS visibility check (available in browser/JSDOM).
92
+ // getComputedStyle may throw on:
93
+ // - SecurityError: cross-origin iframes
94
+ // - TypeError: detached nodes not connected to a document
95
+ // These are expected operational failures. On error, fall through
96
+ // to return false (treat as visible). Attribute-based checks above
97
+ // (aria-hidden, hidden) are the primary detection for static analysis.
98
+ if (typeof getComputedStyle === 'function') {
99
+ try {
100
+ // TODO: Remove cast when getComputedStyle dependency is decoupled (#3178)
101
+ const style = getComputedStyle(target);
102
+ if (style.display === 'none' || style.visibility === 'hidden') {
103
+ return true;
104
+ }
105
+ }
106
+ catch {
107
+ // Swallowed intentionally: SecurityError or TypeError from
108
+ // getComputedStyle is non-fatal. CSS hidden detection is
109
+ // supplementary; attribute checks above cover static analysis.
110
+ }
111
+ }
112
+ return false;
113
+ },
114
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
115
+ isEmbeddedControl(target) {
116
+ // TODO: Remove cast when getComputedRole accepts AccnameElement (#3178)
117
+ const role = getComputedRole(specs, target, version);
118
+ if (role.role?.name && EMBEDDED_CONTROL_ROLES.has(role.role.name)) {
119
+ return true;
120
+ }
121
+ // Native HTML embedded controls without explicit role
122
+ return isNativeEmbeddedControl(target);
123
+ },
124
+ };
125
+ }
126
+ /**
127
+ * Walks up the DOM tree to find the root node (Document or DocumentFragment).
128
+ */
129
+ function getRootNode(
130
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
12
131
  el) {
13
- const name = computeAccessibleName(el);
14
- if (!name.trim() && el.nodeName === 'INPUT') {
15
- return el.getAttribute('placeholder')?.trim() ?? '';
132
+ let root = el;
133
+ while (root.parentNode) {
134
+ root = root.parentNode;
135
+ }
136
+ if (root.nodeType === 9 || root.nodeType === 11) {
137
+ return root;
16
138
  }
17
- return name;
139
+ // Fallback: if we reach a disconnected element, use ownerDocument
140
+ return el.ownerDocument;
18
141
  }
@@ -9,5 +9,6 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
9
9
  export declare function ariaSpecs(specs: MLMLSpec, version: ARIAVersion): {
10
10
  readonly roles: readonly import("../../types/index.js").ARIARoleInSchema[];
11
11
  readonly graphicsRoles: readonly import("../../types/index.js").ARIARoleInSchema[];
12
+ readonly dpubRoles: readonly import("../../types/index.js").ARIARoleInSchema[];
12
13
  readonly props: readonly import("../../types/index.js").ARIAProperty[];
13
14
  };
@@ -25,7 +25,23 @@ export function getARIA(specs, localName, namespace, version, matches) {
25
25
  const conditionKeys = Object.keys(conditions);
26
26
  let { implicitRole, permittedRoles, implicitProperties, properties, namingProhibited } = aria;
27
27
  for (const cond of conditionKeys) {
28
- if (!matches(cond)) {
28
+ let matched;
29
+ try {
30
+ matched = matches(cond);
31
+ }
32
+ catch (error) {
33
+ // Native Element.prototype.matches() cannot evaluate :aria()
34
+ // pseudo-class. When called from getImplicitRoleName() or
35
+ // getPermittedRoles() via accname-computation, native matches
36
+ // is used, which throws a DOMException (name: "SyntaxError").
37
+ // Note: JSDOM's DOMException is context-local, so instanceof
38
+ // checks against the global DOMException/SyntaxError fail.
39
+ if (error instanceof Error && error.name === 'SyntaxError') {
40
+ continue;
41
+ }
42
+ throw error;
43
+ }
44
+ if (!matched) {
29
45
  continue;
30
46
  }
31
47
  const condARIA = conditions[cond];
@@ -61,13 +77,13 @@ function getVersionResolvedARIA(specs, localName, namespace, version) {
61
77
  if (aria.permittedRoles !== false) {
62
78
  aria = {
63
79
  ...aria,
64
- permittedRoles: optimizePermittedRoles(aria.permittedRoles),
80
+ permittedRoles: optimizePermittedRoles(aria.permittedRoles, version),
65
81
  };
66
82
  }
67
83
  cache.set(key, aria);
68
84
  return aria;
69
85
  }
70
- function optimizePermittedRoles(permittedRoles) {
86
+ function optimizePermittedRoles(permittedRoles, version) {
71
87
  if (!Array.isArray(permittedRoles)) {
72
88
  return permittedRoles;
73
89
  }
@@ -79,5 +95,15 @@ function optimizePermittedRoles(permittedRoles) {
79
95
  if (unique.has('none')) {
80
96
  unique.add('presentation');
81
97
  }
82
- return [...unique].sort();
98
+ // https://w3c.github.io/aria/#ref-for-image
99
+ // In ARIA 1.3, `image` is the primary role name and `img` is a synonym.
100
+ if (version === '1.3') {
101
+ if (unique.has('image')) {
102
+ unique.add('img');
103
+ }
104
+ if (unique.has('img')) {
105
+ unique.add('image');
106
+ }
107
+ }
108
+ return [...unique].toSorted();
83
109
  }
@@ -1,6 +1,7 @@
1
1
  import { ariaSpecs } from './aria-specs.js';
2
2
  import { isPresentational } from './is-presentational.js';
3
- import { getAccname } from './accname-computation.js';
3
+ import { SVG_NAMESPACE } from '../../const/index.js';
4
+ import { hasSvgAccessibleNameSource } from './accname/svg-helpers.js';
4
5
  import { getAttrSpecs } from '../../utils/get-attr-specs.js';
5
6
  import { getExplicitRole } from './get-explicit-role.js';
6
7
  import { getImplicitRole } from './get-implicit-role.js';
@@ -8,6 +9,24 @@ import { getNonPresentationalAncestor } from './get-non-presentational-ancestor.
8
9
  import { isRequiredOwnedElement } from './has-required-owned-elements.js';
9
10
  import { matchesContextRole } from './matches-context-role.js';
10
11
  import { mayBeFocusable } from '../html/may-be-focusable.js';
12
+ /**
13
+ * Module-level cache for computed role results.
14
+ *
15
+ * Uses WeakMap so entries are automatically garbage-collected
16
+ * when the Element is released. Cache key format: `${version}:${assumeSingleNode}`.
17
+ *
18
+ * Invariants:
19
+ * - `specs` is always the same `MLMLSpec` instance within a document traversal,
20
+ * so it is not included in the cache key.
21
+ * - Recursive calls from `computeRole` (via `getNonPresentationalAncestor`,
22
+ * `matchesContextRole`, `isNativeContextIntact`) always traverse upward
23
+ * in the DOM tree, so circular references cannot occur.
24
+ * - Cache entries become stale if an element's attributes are mutated after
25
+ * computation. This is acceptable because markuplint operates on a static
26
+ * document snapshot. External consumers that use the algorithm on a live
27
+ * DOM with dynamic attribute mutations should be aware of this limitation.
28
+ */
29
+ const computedRoleCache = new WeakMap();
11
30
  /**
12
31
  * Computes the final ARIA role for an element according to the WAI-ARIA specification,
13
32
  * applying the Presentational Roles Conflict Resolution algorithm. This considers
@@ -23,6 +42,25 @@ import { mayBeFocusable } from '../html/may-be-focusable.js';
23
42
  export function getComputedRole(specs,
24
43
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
25
44
  el, version, assumeSingleNode = false) {
45
+ const cacheKey = `${version}:${assumeSingleNode}`;
46
+ const elCache = computedRoleCache.get(el);
47
+ if (elCache) {
48
+ const cached = elCache.get(cacheKey);
49
+ if (cached) {
50
+ return cached;
51
+ }
52
+ }
53
+ const result = computeRole(specs, el, version, assumeSingleNode);
54
+ const cache = elCache ?? new Map();
55
+ cache.set(cacheKey, result);
56
+ if (!elCache) {
57
+ computedRoleCache.set(el, cache);
58
+ }
59
+ return result;
60
+ }
61
+ function computeRole(specs,
62
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
63
+ el, version, assumeSingleNode) {
26
64
  let lazyImplicitRole;
27
65
  const explicitRole = getExplicitRole(specs, el, version);
28
66
  const computedRole = explicitRole.role
@@ -48,24 +86,36 @@ el, version, assumeSingleNode = false) {
48
86
  * > causes the accessibility tree to be malformed,
49
87
  * > the expected results are undefined.
50
88
  *
51
- * Determines whether the context is valid.
52
- * THE SPECIFICATION HAS AN ISSUE
53
- * that has not decided whether the context is a parent or an ancestor.
89
+ * Determines whether the "Required Accessibility Parent Role" is valid
90
+ * (called "Required Context Role" in ARIA 1.2).
54
91
  *
55
- * @see https://github.com/w3c/aria/issues/1033
56
- * @see https://github.com/w3c/aria/issues/748
57
- * @see https://github.com/w3c/aria/pull/1162
58
- * @see https://github.com/w3c/aria/pull/1213
92
+ * In ARIA 1.1/1.2, the spec had not decided whether the context
93
+ * is a parent or an ancestor. This implementation interprets that
94
+ * as A PARENT, but `presentation`/`none` elements are treated as
95
+ * transparent by `getNonPresentationalAncestor`.
59
96
  *
60
- * Currently, this process interprets that as A PARENT
61
- * because it wants to be near to HTML semantics.
62
- * However, the presentational role behaves transparently
63
- * according to the sample code in WAI-ARIA specification.
97
+ * ARIA 1.3 formally resolves this with the definitions of
98
+ * "accessibility child" and "accessibility parent", and
99
+ * `matchesContextRole` additionally skips `generic` elements.
100
+ *
101
+ * @see https://w3c.github.io/aria/#scope
102
+ * @see https://github.com/w3c/aria/issues/1033
103
+ * @see https://github.com/w3c/aria/pull/1454
64
104
  */
65
- if (computedRole.role && computedRole.role.requiredContextRole.length > 0) {
105
+ if (computedRole.role &&
106
+ computedRole.role.requiredAccessibilityParentRole.length > 0 &&
107
+ // For implicit roles in native HTML contexts, skip the context role check
108
+ // when the direct parent retains its native semantics (no explicit role override
109
+ // and its own computed role is non-null). This handles spec data mismatches
110
+ // such as <option> inside <select>, where the ARIA spec requires "listbox"
111
+ // context but the HTML-ARIA mapping gives <select> the "combobox" role.
112
+ // When the parent HAS an explicit role or its computed role is null (cascaded
113
+ // from an ancestor override like <table role="none">), the check proceeds
114
+ // normally so that role nullification cascades correctly.
115
+ !isNativeContextIntact(computedRole, el, specs, version)) {
66
116
  /**
67
117
  * An element fragment that serves as the root without a parent element
68
- * cannot satisfy the "Required Context Role" condition.
118
+ * cannot satisfy the "Required Accessibility Parent Role" condition.
69
119
  * Therefore, under normal circumstances, the `role` will disappear.
70
120
  * However, in this specific case, it will fall back to both explicit
71
121
  * and implicit roles. Note that the explicit role takes precedence.
@@ -84,7 +134,7 @@ el, version, assumeSingleNode = false) {
84
134
  errorType: 'NO_OWNER',
85
135
  };
86
136
  }
87
- if (!matchesContextRole(computedRole.role.requiredContextRole, el, specs, version)) {
137
+ if (!matchesContextRole(computedRole.role.requiredAccessibilityParentRole, el, specs, version)) {
88
138
  return {
89
139
  el,
90
140
  role: null,
@@ -126,15 +176,12 @@ el, version, assumeSingleNode = false) {
126
176
  // It doesn't been specified a valid explicit role.
127
177
  (explicitRole.role === null || explicitRole.errorType != null) &&
128
178
  // It is an SVG element.
129
- el.namespaceURI === 'http://www.w3.org/2000/svg') {
130
- const accname = getAccname(el).trim() ||
131
- [...el.children].find(child => ['title', 'desc'].includes(child.localName))?.textContent?.trim();
132
- if (!accname) {
133
- return {
134
- el,
135
- role: null,
136
- };
137
- }
179
+ el.namespaceURI === SVG_NAMESPACE &&
180
+ !hasSvgAccessibleNameSource(el)) {
181
+ return {
182
+ el,
183
+ role: null,
184
+ };
138
185
  }
139
186
  if (computedRole.role && !isPresentational(computedRole.role.name)) {
140
187
  return computedRole;
@@ -193,8 +240,8 @@ el, version, assumeSingleNode = false) {
193
240
  if (explicitRole.role) {
194
241
  const nonPresentationalAncestor = getNonPresentationalAncestor(el, specs, version);
195
242
  if (nonPresentationalAncestor.role &&
196
- nonPresentationalAncestor.role?.requiredOwnedElements.length > 0 &&
197
- nonPresentationalAncestor.role.requiredOwnedElements.some(expected => {
243
+ nonPresentationalAncestor.role?.allowedAccessibilityChildRoles.length > 0 &&
244
+ nonPresentationalAncestor.role.allowedAccessibilityChildRoles.some(expected => {
198
245
  // const ancestor = nonPresentationalAncestor.el;
199
246
  // const ancestorImplicitRole = getImplicitRole(specs, ancestor, version);
200
247
  // console.log({ nonPresentationalAncestor, ancestorImplicitRole });
@@ -252,3 +299,36 @@ el, predicate) {
252
299
  }
253
300
  return list.some(predicate);
254
301
  }
302
+ /**
303
+ * Checks whether an element with an implicit role is in its native HTML
304
+ * context — i.e., the direct parent has no explicit role override and
305
+ * its own computed role is non-null.
306
+ *
307
+ * When this returns `true`, the context role check can be safely skipped
308
+ * because the native HTML parent-child relationship is intact, even if
309
+ * the ARIA spec data has a mismatch (e.g., `<option>` requires "listbox"
310
+ * but `<select>` maps to "combobox").
311
+ *
312
+ * When the parent has an explicit role (e.g., `<table role="none">`) or
313
+ * its computed role is `null` (cascaded from an ancestor override), the
314
+ * context is NOT considered intact and the check should proceed.
315
+ */
316
+ function isNativeContextIntact(
317
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
318
+ computedRole,
319
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
320
+ el, specs, version) {
321
+ if (!computedRole.role?.isImplicit) {
322
+ return false;
323
+ }
324
+ const parent = el.parentElement;
325
+ if (!parent) {
326
+ return false;
327
+ }
328
+ const parentExplicit = getExplicitRole(specs, parent, version);
329
+ if (parentExplicit.role) {
330
+ return false;
331
+ }
332
+ const parentComputed = getComputedRole(specs, parent, version);
333
+ return parentComputed.role !== null;
334
+ }
@@ -1,2 +1,14 @@
1
1
  import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
2
+ /**
3
+ * Resolves the ARIA role from the `role` attribute value, implementing the
4
+ * WAI-ARIA "Handling Author Errors" algorithm. Iterates through whitespace-separated
5
+ * role tokens and returns the first one that passes all validation checks
6
+ * (exists, non-abstract, permitted, valid landmark).
7
+ *
8
+ * @see https://w3c.github.io/aria/#document-handling_author-errors
9
+ * @param specs - The full markup language specification
10
+ * @param el - The DOM element to resolve the explicit role for
11
+ * @param version - The ARIA specification version to use
12
+ * @returns The first valid role found as a `ComputedRole`, or `role: null` with the last encountered error type
13
+ */
2
14
  export declare function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole;
@@ -1,6 +1,18 @@
1
1
  import { getRoleSpec } from './get-role-spec.js';
2
2
  import { resolveNamespace } from '../../utils/resolve-namespace.js';
3
3
  import { getPermittedRoles } from './get-permitted-roles.js';
4
+ /**
5
+ * Resolves the ARIA role from the `role` attribute value, implementing the
6
+ * WAI-ARIA "Handling Author Errors" algorithm. Iterates through whitespace-separated
7
+ * role tokens and returns the first one that passes all validation checks
8
+ * (exists, non-abstract, permitted, valid landmark).
9
+ *
10
+ * @see https://w3c.github.io/aria/#document-handling_author-errors
11
+ * @param specs - The full markup language specification
12
+ * @param el - The DOM element to resolve the explicit role for
13
+ * @param version - The ARIA specification version to use
14
+ * @returns The first valid role found as a `ComputedRole`, or `role: null` with the last encountered error type
15
+ */
4
16
  export function getExplicitRole(specs,
5
17
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
6
18
  el, version) {
@@ -1,4 +1,16 @@
1
1
  import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
2
+ /**
3
+ * Traverses the parent element chain to find the nearest ancestor with a
4
+ * non-presentational role, skipping elements that are transparent for
5
+ * ownership traversal (via `isTransparentForOwnership`).
6
+ *
7
+ * In ARIA 1.3, `generic` role elements are additionally transparent.
8
+ *
9
+ * @param el - The DOM element whose ancestors to traverse
10
+ * @param specs - The full markup language specification
11
+ * @param version - The ARIA specification version to use
12
+ * @returns The nearest non-presentational ancestor's `ComputedRole`, or `{ el: null, role: null }` if none exists
13
+ */
2
14
  export declare function getNonPresentationalAncestor(el: Element, specs: MLMLSpec, version: ARIAVersion): import("../../types/index.js").ComputedRole | {
3
15
  el: null;
4
16
  role: null;
@@ -1,5 +1,17 @@
1
- import { isPresentational } from './is-presentational.js';
1
+ import { isTransparentForOwnership } from './is-presentational.js';
2
2
  import { getComputedRole } from './get-computed-role.js';
3
+ /**
4
+ * Traverses the parent element chain to find the nearest ancestor with a
5
+ * non-presentational role, skipping elements that are transparent for
6
+ * ownership traversal (via `isTransparentForOwnership`).
7
+ *
8
+ * In ARIA 1.3, `generic` role elements are additionally transparent.
9
+ *
10
+ * @param el - The DOM element whose ancestors to traverse
11
+ * @param specs - The full markup language specification
12
+ * @param version - The ARIA specification version to use
13
+ * @returns The nearest non-presentational ancestor's `ComputedRole`, or `{ el: null, role: null }` if none exists
14
+ */
3
15
  export function getNonPresentationalAncestor(
4
16
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
5
17
  el, specs, version) {
@@ -12,7 +24,7 @@ el, specs, version) {
12
24
  const assumeSingleNode = version !== '1.1' && version !== '1.2';
13
25
  while (ancestor) {
14
26
  const ancestorRole = getComputedRole(specs, ancestor, version, assumeSingleNode);
15
- if (!isPresentational(ancestorRole.role?.name)) {
27
+ if (!isTransparentForOwnership(ancestorRole.role?.name, version)) {
16
28
  return ancestorRole;
17
29
  }
18
30
  ancestor = ancestor.parentElement;
@@ -1,10 +1,14 @@
1
1
  import type { ARIAVersion, Matches, MLMLSpec } from '../../types/index.js';
2
2
  /**
3
- * Getting permitted ARIA roles.
3
+ * Computes the list of permitted ARIA roles for an element at the spec level,
4
+ * operating on tag name and namespace rather than a DOM element.
4
5
  *
5
- * - If an array, it is role list.
6
- * - If `true`, this mean is "Any".
7
- * - If `false`, this mean is "No".
6
+ * @param specs - The full markup language specification
7
+ * @param localName - The element's local tag name
8
+ * @param namespace - The element's namespace URI
9
+ * @param version - The ARIA specification version to use
10
+ * @param matches - A function that tests CSS selector matches for conditional role resolution
11
+ * @returns A list of permitted roles, each with a `name` and optional `deprecated` flag
8
12
  */
9
13
  export declare function getPermittedRoles(specs: MLMLSpec, localName: string, namespace: string | null, version: ARIAVersion, matches: Matches): readonly {
10
14
  readonly name: string;
@@ -4,11 +4,15 @@ import { mergeArray } from '../../utils/merge-array.js';
4
4
  import { ariaSpecs } from './aria-specs.js';
5
5
  import { getARIA } from './get-aria.js';
6
6
  /**
7
- * Getting permitted ARIA roles.
7
+ * Computes the list of permitted ARIA roles for an element at the spec level,
8
+ * operating on tag name and namespace rather than a DOM element.
8
9
  *
9
- * - If an array, it is role list.
10
- * - If `true`, this mean is "Any".
11
- * - If `false`, this mean is "No".
10
+ * @param specs - The full markup language specification
11
+ * @param localName - The element's local tag name
12
+ * @param namespace - The element's namespace URI
13
+ * @param version - The ARIA specification version to use
14
+ * @param matches - A function that tests CSS selector matches for conditional role resolution
15
+ * @returns A list of permitted roles, each with a `name` and optional `deprecated` flag
12
16
  */
13
17
  export function getPermittedRoles(specs, localName, namespace, version, matches) {
14
18
  const aria = getARIA(specs, localName, namespace, version, matches);
@@ -16,7 +20,7 @@ export function getPermittedRoles(specs, localName, namespace, version, matches)
16
20
  return [];
17
21
  }
18
22
  const { implicitRole, permittedRoles } = aria;
19
- const { roles, graphicsRoles } = ariaSpecs(specs, version);
23
+ const { roles, graphicsRoles, dpubRoles } = ariaSpecs(specs, version);
20
24
  let permittedRoleList = [];
21
25
  if (permittedRoles === true) {
22
26
  permittedRoleList = mergeArray(permittedRoleList, roles
@@ -24,6 +28,11 @@ export function getPermittedRoles(specs, localName, namespace, version, matches)
24
28
  .map(role => ({
25
29
  name: role.name,
26
30
  })));
31
+ permittedRoleList = mergeArray(permittedRoleList, dpubRoles
32
+ .filter(role => !role.isAbstract)
33
+ .map(role => ({
34
+ name: role.name,
35
+ })));
27
36
  }
28
37
  if (isAAMInfo(permittedRoles)) {
29
38
  if (permittedRoles['core-aam']) {
@@ -54,7 +63,11 @@ export function getPermittedRoles(specs, localName, namespace, version, matches)
54
63
  if (implicitRole === false) {
55
64
  return permittedRoleList;
56
65
  }
57
- const implicitRoles = implicitRole === 'presentation' || implicitRole === 'none' ? ['none', 'presentation'] : [implicitRole];
66
+ const implicitRoles = implicitRole === 'presentation' || implicitRole === 'none'
67
+ ? ['none', 'presentation']
68
+ : version === '1.3' && (implicitRole === 'img' || implicitRole === 'image')
69
+ ? ['image', 'img']
70
+ : [implicitRole];
58
71
  return mergeArray(implicitRoles.map(r => ({ name: r })), permittedRoleList);
59
72
  }
60
73
  function isAAMInfo(permittedRoles) {
@@ -16,12 +16,16 @@ export function getRoleSpec(specs, roleName, namespace, version) {
16
16
  return null;
17
17
  }
18
18
  const superClassRoles = recursiveTraverseSuperClassRoles(specs, roleName, namespace, version);
19
+ const requiredAccessibilityParentRole = role.requiredContextRole ?? role.requiredAccessibilityParentRole ?? [];
20
+ const allowedAccessibilityChildRoles = role.requiredOwnedElements ?? role.allowedAccessibilityChildRoles ?? [];
19
21
  return {
20
22
  name: role.name,
21
23
  isAbstract: !!role.isAbstract,
22
24
  deprecated: !!role.deprecated,
23
- requiredContextRole: role.requiredContextRole ?? [],
24
- requiredOwnedElements: role.requiredOwnedElements ?? [],
25
+ requiredAccessibilityParentRole,
26
+ allowedAccessibilityChildRoles,
27
+ requiredContextRole: requiredAccessibilityParentRole,
28
+ requiredOwnedElements: allowedAccessibilityChildRoles,
25
29
  accessibleNameRequired: !!role.accessibleNameRequired,
26
30
  accessibleNameFromAuthor: !!role.accessibleNameFromAuthor,
27
31
  accessibleNameFromContent: !!role.accessibleNameFromContent,
@@ -51,10 +55,13 @@ function getSuperClassRoles(specs, roleName, namespace, version) {
51
55
  .filter((role) => !!role) ?? null);
52
56
  }
53
57
  function getRoleByName(specs, roleName, namespace, version) {
54
- const { roles, graphicsRoles } = ariaSpecs(specs, version);
58
+ const { roles, graphicsRoles, dpubRoles } = ariaSpecs(specs, version);
55
59
  let role = roles.find(r => r.name === roleName);
56
60
  if (!role && namespace === 'http://www.w3.org/2000/svg') {
57
61
  role = graphicsRoles.find(r => r.name === roleName);
58
62
  }
63
+ if (!role) {
64
+ role = dpubRoles.find(r => r.name === roleName);
65
+ }
59
66
  return role;
60
67
  }
@@ -1,6 +1,7 @@
1
1
  import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
2
2
  /**
3
- * Checks whether an element satisfies the "Required Owned Elements" constraint
3
+ * Checks whether an element satisfies the "Allowed Accessibility Child Roles"
4
+ * constraint (called "Required Owned Elements" in ARIA 1.2)
4
5
  * defined by its computed ARIA role. An element satisfies this constraint if it
5
6
  * has `aria-owns`, or if any of its closest non-presentational descendants match
6
7
  * the required owned element roles.