@markuplint/ml-spec 4.10.1 → 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.
- package/ARCHITECTURE.ja.md +267 -0
- package/ARCHITECTURE.md +267 -0
- package/CHANGELOG.md +20 -2
- package/README.md +6 -188
- package/SKILL.md +116 -0
- package/docs/aria-algorithms.ja.md +798 -0
- package/docs/aria-algorithms.md +800 -0
- package/docs/html-algorithms.ja.md +469 -0
- package/docs/html-algorithms.md +469 -0
- package/docs/maintenance.ja.md +331 -0
- package/docs/maintenance.md +331 -0
- package/docs/spec-resolution.ja.md +568 -0
- package/docs/spec-resolution.md +580 -0
- package/docs/type-definitions.ja.md +565 -0
- package/docs/type-definitions.md +565 -0
- package/lib/algorithm/aria/accname/__tests__/test-helpers.d.ts +45 -0
- package/lib/algorithm/aria/accname/__tests__/test-helpers.js +120 -0
- package/lib/algorithm/aria/accname/aria-steps.d.ts +51 -0
- package/lib/algorithm/aria/accname/aria-steps.js +104 -0
- package/lib/algorithm/aria/accname/compute.d.ts +51 -0
- package/lib/algorithm/aria/accname/compute.js +101 -0
- package/lib/algorithm/aria/accname/element-names.d.ts +36 -0
- package/lib/algorithm/aria/accname/element-names.js +342 -0
- package/lib/algorithm/aria/accname/helpers.d.ts +98 -0
- package/lib/algorithm/aria/accname/helpers.js +330 -0
- package/lib/algorithm/aria/accname/index.d.ts +4 -0
- package/lib/algorithm/aria/accname/index.js +3 -0
- package/lib/algorithm/aria/accname/label-steps.d.ts +25 -0
- package/lib/algorithm/aria/accname/label-steps.js +66 -0
- package/lib/algorithm/aria/accname/svg-helpers.d.ts +17 -0
- package/lib/algorithm/aria/accname/svg-helpers.js +30 -0
- package/lib/algorithm/aria/accname/types.d.ts +70 -0
- package/lib/algorithm/aria/accname/types.js +2 -0
- package/lib/algorithm/aria/accname-computation.d.ts +19 -1
- package/lib/algorithm/aria/accname-computation.js +136 -6
- package/lib/algorithm/aria/aria-specs.d.ts +8 -0
- package/lib/algorithm/aria/aria-specs.js +7 -0
- package/lib/algorithm/aria/get-aria.d.ts +12 -0
- package/lib/algorithm/aria/get-aria.js +42 -4
- package/lib/algorithm/aria/get-computed-aria-props.d.ts +22 -0
- package/lib/algorithm/aria/get-computed-aria-props.js +10 -0
- package/lib/algorithm/aria/get-computed-role.d.ts +12 -0
- package/lib/algorithm/aria/get-computed-role.js +118 -26
- package/lib/algorithm/aria/get-explicit-role.d.ts +12 -0
- package/lib/algorithm/aria/get-explicit-role.js +12 -0
- package/lib/algorithm/aria/get-implicit-role.d.ts +18 -0
- package/lib/algorithm/aria/get-implicit-role.js +18 -0
- package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +12 -0
- package/lib/algorithm/aria/get-non-presentational-ancestor.js +14 -2
- package/lib/algorithm/aria/get-permitted-roles-spec.d.ts +8 -4
- package/lib/algorithm/aria/get-permitted-roles-spec.js +19 -6
- package/lib/algorithm/aria/get-permitted-roles.d.ts +9 -0
- package/lib/algorithm/aria/get-permitted-roles.js +9 -0
- package/lib/algorithm/aria/get-role-spec.d.ts +11 -0
- package/lib/algorithm/aria/get-role-spec.js +21 -3
- package/lib/algorithm/aria/has-required-owned-elements.d.ts +24 -0
- package/lib/algorithm/aria/has-required-owned-elements.js +40 -14
- package/lib/algorithm/aria/is-exposed.d.ts +7 -4
- package/lib/algorithm/aria/is-exposed.js +7 -4
- package/lib/algorithm/aria/is-presentational.d.ts +32 -0
- package/lib/algorithm/aria/is-presentational.js +39 -0
- package/lib/algorithm/aria/matches-context-role.d.ts +19 -0
- package/lib/algorithm/aria/matches-context-role.js +46 -2
- package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +9 -0
- package/lib/algorithm/html/content-model-category-to-tag-names.js +10 -1
- package/lib/algorithm/html/get-content-model.d.ts +12 -1
- package/lib/algorithm/html/get-content-model.js +14 -6
- package/lib/algorithm/html/get-selectors-by-content-model-category.d.ts +8 -0
- package/lib/algorithm/html/get-selectors-by-content-model-category.js +8 -0
- package/lib/algorithm/html/is-nothing-content-model.d.ts +7 -0
- package/lib/algorithm/html/is-nothing-content-model.js +7 -0
- package/lib/algorithm/html/is-palpable-elements.d.ts +13 -0
- package/lib/algorithm/html/is-palpable-elements.js +13 -0
- package/lib/algorithm/html/is-void-element.d.ts +9 -0
- package/lib/algorithm/html/is-void-element.js +9 -0
- package/lib/algorithm/html/may-be-focusable.d.ts +10 -0
- package/lib/algorithm/html/may-be-focusable.js +10 -0
- package/lib/const/accname.d.ts +29 -0
- package/lib/const/accname.js +76 -0
- package/lib/const/dom.d.ts +8 -0
- package/lib/const/dom.js +8 -0
- package/lib/const/index.d.ts +2 -0
- package/lib/const/index.js +2 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +4 -0
- package/lib/types/index.d.ts +119 -0
- package/lib/utils/aria-version.d.ts +6 -0
- package/lib/utils/aria-version.js +6 -0
- package/lib/utils/directive-resolver.d.ts +23 -0
- package/lib/utils/directive-resolver.js +50 -0
- package/lib/utils/get-attr-specs-spec.d.ts +18 -0
- package/lib/utils/get-attr-specs-spec.js +22 -3
- package/lib/utils/get-attr-specs.d.ts +9 -0
- package/lib/utils/get-attr-specs.js +9 -0
- package/lib/utils/get-ns.d.ts +7 -0
- package/lib/utils/get-ns.js +7 -0
- package/lib/utils/get-spec-by-tag-name.d.ts +12 -1
- package/lib/utils/get-spec-by-tag-name.js +11 -0
- package/lib/utils/get-spec.d.ts +11 -1
- package/lib/utils/get-spec.js +10 -0
- package/lib/utils/merge-array.d.ts +10 -0
- package/lib/utils/merge-array.js +10 -0
- package/lib/utils/resolve-namespace.d.ts +13 -0
- package/lib/utils/resolve-namespace.js +10 -0
- package/lib/utils/resolve-version.d.ts +11 -0
- package/lib/utils/resolve-version.js +11 -0
- package/lib/utils/schema-to-spec.d.ts +7 -2
- package/lib/utils/schema-to-spec.js +20 -6
- package/lib/utils/validate-aria-version.d.ts +7 -0
- package/lib/utils/validate-aria-version.js +7 -0
- package/package.json +9 -7
|
@@ -1,11 +1,141 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
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';
|
|
5
|
+
/**
|
|
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.
|
|
28
|
+
*
|
|
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
|
|
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
|
|
34
|
+
*/
|
|
3
35
|
export function getAccname(
|
|
4
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
|
|
5
131
|
el) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
132
|
+
let root = el;
|
|
133
|
+
while (root.parentNode) {
|
|
134
|
+
root = root.parentNode;
|
|
135
|
+
}
|
|
136
|
+
if (root.nodeType === 9 || root.nodeType === 11) {
|
|
137
|
+
return root;
|
|
9
138
|
}
|
|
10
|
-
|
|
139
|
+
// Fallback: if we reach a disconnected element, use ownerDocument
|
|
140
|
+
return el.ownerDocument;
|
|
11
141
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Retrieves the ARIA specification data for a given ARIA version from the markup language spec.
|
|
4
|
+
*
|
|
5
|
+
* @param specs - The full markup language specification containing ARIA definitions
|
|
6
|
+
* @param version - The ARIA specification version to retrieve (e.g., '1.1', '1.2', '1.3')
|
|
7
|
+
* @returns The ARIA specification data including roles, properties, and graphics roles for the requested version
|
|
8
|
+
*/
|
|
2
9
|
export declare function ariaSpecs(specs: MLMLSpec, version: ARIAVersion): {
|
|
3
10
|
readonly roles: readonly import("../../types/index.js").ARIARoleInSchema[];
|
|
4
11
|
readonly graphicsRoles: readonly import("../../types/index.js").ARIARoleInSchema[];
|
|
12
|
+
readonly dpubRoles: readonly import("../../types/index.js").ARIARoleInSchema[];
|
|
5
13
|
readonly props: readonly import("../../types/index.js").ARIAProperty[];
|
|
6
14
|
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieves the ARIA specification data for a given ARIA version from the markup language spec.
|
|
3
|
+
*
|
|
4
|
+
* @param specs - The full markup language specification containing ARIA definitions
|
|
5
|
+
* @param version - The ARIA specification version to retrieve (e.g., '1.1', '1.2', '1.3')
|
|
6
|
+
* @returns The ARIA specification data including roles, properties, and graphics roles for the requested version
|
|
7
|
+
*/
|
|
1
8
|
export function ariaSpecs(specs, version) {
|
|
2
9
|
const aria = specs.def['#aria'];
|
|
3
10
|
return aria[version];
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { ARIA } from '../../types/aria.js';
|
|
2
2
|
import type { ARIAVersion, Matches, MLMLSpec } from '../../types/index.js';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
|
+
/**
|
|
5
|
+
* Retrieves the resolved ARIA specification for an element, taking into account
|
|
6
|
+
* ARIA version differences and conditional overrides based on the element's
|
|
7
|
+
* current attribute state (e.g., `input[type=checkbox]` vs `input[type=text]`).
|
|
8
|
+
*
|
|
9
|
+
* @param specs - The full markup language specification
|
|
10
|
+
* @param localName - The local tag name of the element
|
|
11
|
+
* @param namespace - The namespace URI of the element, or null
|
|
12
|
+
* @param version - The ARIA specification version to use
|
|
13
|
+
* @param matches - A function that tests whether the element matches a CSS selector
|
|
14
|
+
* @returns The resolved ARIA specification for the element, or null if no spec exists
|
|
15
|
+
*/
|
|
4
16
|
export declare function getARIA(specs: MLMLSpec, localName: string, namespace: string | null, version: ARIAVersion, matches: Matches): Omit<ReadonlyDeep<ARIA>, ARIAVersion | 'conditions'> | null;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { getSpecByTagName } from '../../utils/get-spec-by-tag-name.js';
|
|
2
2
|
import { resolveVersion } from '../../utils/resolve-version.js';
|
|
3
3
|
const cache = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Retrieves the resolved ARIA specification for an element, taking into account
|
|
6
|
+
* ARIA version differences and conditional overrides based on the element's
|
|
7
|
+
* current attribute state (e.g., `input[type=checkbox]` vs `input[type=text]`).
|
|
8
|
+
*
|
|
9
|
+
* @param specs - The full markup language specification
|
|
10
|
+
* @param localName - The local tag name of the element
|
|
11
|
+
* @param namespace - The namespace URI of the element, or null
|
|
12
|
+
* @param version - The ARIA specification version to use
|
|
13
|
+
* @param matches - A function that tests whether the element matches a CSS selector
|
|
14
|
+
* @returns The resolved ARIA specification for the element, or null if no spec exists
|
|
15
|
+
*/
|
|
4
16
|
export function getARIA(specs, localName, namespace, version, matches) {
|
|
5
17
|
const aria = getVersionResolvedARIA(specs, localName, namespace, version);
|
|
6
18
|
if (!aria) {
|
|
@@ -13,7 +25,23 @@ export function getARIA(specs, localName, namespace, version, matches) {
|
|
|
13
25
|
const conditionKeys = Object.keys(conditions);
|
|
14
26
|
let { implicitRole, permittedRoles, implicitProperties, properties, namingProhibited } = aria;
|
|
15
27
|
for (const cond of conditionKeys) {
|
|
16
|
-
|
|
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) {
|
|
17
45
|
continue;
|
|
18
46
|
}
|
|
19
47
|
const condARIA = conditions[cond];
|
|
@@ -49,13 +77,13 @@ function getVersionResolvedARIA(specs, localName, namespace, version) {
|
|
|
49
77
|
if (aria.permittedRoles !== false) {
|
|
50
78
|
aria = {
|
|
51
79
|
...aria,
|
|
52
|
-
permittedRoles: optimizePermittedRoles(aria.permittedRoles),
|
|
80
|
+
permittedRoles: optimizePermittedRoles(aria.permittedRoles, version),
|
|
53
81
|
};
|
|
54
82
|
}
|
|
55
83
|
cache.set(key, aria);
|
|
56
84
|
return aria;
|
|
57
85
|
}
|
|
58
|
-
function optimizePermittedRoles(permittedRoles) {
|
|
86
|
+
function optimizePermittedRoles(permittedRoles, version) {
|
|
59
87
|
if (!Array.isArray(permittedRoles)) {
|
|
60
88
|
return permittedRoles;
|
|
61
89
|
}
|
|
@@ -67,5 +95,15 @@ function optimizePermittedRoles(permittedRoles) {
|
|
|
67
95
|
if (unique.has('none')) {
|
|
68
96
|
unique.add('presentation');
|
|
69
97
|
}
|
|
70
|
-
|
|
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();
|
|
71
109
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* A record mapping ARIA property names to their computed property details.
|
|
4
|
+
*/
|
|
2
5
|
type ARIAProps = Record<string, ARIAProp>;
|
|
6
|
+
/**
|
|
7
|
+
* Represents a single computed ARIA property with its resolved value and metadata.
|
|
8
|
+
*/
|
|
3
9
|
type ARIAProp = {
|
|
4
10
|
name: string;
|
|
5
11
|
value: string | undefined;
|
|
@@ -7,6 +13,22 @@ type ARIAProp = {
|
|
|
7
13
|
deprecated: boolean;
|
|
8
14
|
from: ARIAPropReferenceType;
|
|
9
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Indicates the source from which an ARIA property value was derived:
|
|
18
|
+
* - `'default'`: The property's default value from the ARIA spec
|
|
19
|
+
* - `'html-attr'`: A value mapped from an equivalent HTML attribute
|
|
20
|
+
* - `'aria-attr'`: A value explicitly set via an `aria-*` attribute
|
|
21
|
+
*/
|
|
10
22
|
type ARIAPropReferenceType = 'default' | 'html-attr' | 'aria-attr';
|
|
23
|
+
/**
|
|
24
|
+
* Computes the resolved ARIA properties for an element based on its computed role.
|
|
25
|
+
* Resolves property values by checking explicit `aria-*` attributes first,
|
|
26
|
+
* then equivalent HTML attributes, and finally falling back to spec-defined defaults.
|
|
27
|
+
*
|
|
28
|
+
* @param specs - The full markup language specification
|
|
29
|
+
* @param el - The DOM element to compute ARIA properties for
|
|
30
|
+
* @param version - The ARIA specification version to use
|
|
31
|
+
* @returns A record of ARIA property names to their computed property details, or an empty record if the element has no computed role
|
|
32
|
+
*/
|
|
11
33
|
export declare function getComputedAriaProps(specs: MLMLSpec, el: Element, version: ARIAVersion): ARIAProps;
|
|
12
34
|
export {};
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { ariaSpecs as _ariaSpecs } from './aria-specs.js';
|
|
2
2
|
import { getComputedRole } from './get-computed-role.js';
|
|
3
|
+
/**
|
|
4
|
+
* Computes the resolved ARIA properties for an element based on its computed role.
|
|
5
|
+
* Resolves property values by checking explicit `aria-*` attributes first,
|
|
6
|
+
* then equivalent HTML attributes, and finally falling back to spec-defined defaults.
|
|
7
|
+
*
|
|
8
|
+
* @param specs - The full markup language specification
|
|
9
|
+
* @param el - The DOM element to compute ARIA properties for
|
|
10
|
+
* @param version - The ARIA specification version to use
|
|
11
|
+
* @returns A record of ARIA property names to their computed property details, or an empty record if the element has no computed role
|
|
12
|
+
*/
|
|
3
13
|
export function getComputedAriaProps(specs,
|
|
4
14
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
5
15
|
el, version) {
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes the final ARIA role for an element according to the WAI-ARIA specification,
|
|
4
|
+
* applying the Presentational Roles Conflict Resolution algorithm. This considers
|
|
5
|
+
* the explicit role, implicit role, required context roles, focusability,
|
|
6
|
+
* global ARIA properties, and SVG accessibility tree inclusion rules.
|
|
7
|
+
*
|
|
8
|
+
* @param specs - The full markup language specification
|
|
9
|
+
* @param el - The DOM element to compute the role for
|
|
10
|
+
* @param version - The ARIA specification version to use
|
|
11
|
+
* @param assumeSingleNode - When true, skips parent context validation and assumes the element is a standalone fragment
|
|
12
|
+
* @returns The computed role result including the resolved role spec (or null) and any error type
|
|
13
|
+
*/
|
|
2
14
|
export declare function getComputedRole(specs: MLMLSpec, el: Element, version: ARIAVersion, assumeSingleNode?: boolean): ComputedRole;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ariaSpecs } from './aria-specs.js';
|
|
2
2
|
import { isPresentational } from './is-presentational.js';
|
|
3
|
-
import {
|
|
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,9 +9,58 @@ 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();
|
|
30
|
+
/**
|
|
31
|
+
* Computes the final ARIA role for an element according to the WAI-ARIA specification,
|
|
32
|
+
* applying the Presentational Roles Conflict Resolution algorithm. This considers
|
|
33
|
+
* the explicit role, implicit role, required context roles, focusability,
|
|
34
|
+
* global ARIA properties, and SVG accessibility tree inclusion rules.
|
|
35
|
+
*
|
|
36
|
+
* @param specs - The full markup language specification
|
|
37
|
+
* @param el - The DOM element to compute the role for
|
|
38
|
+
* @param version - The ARIA specification version to use
|
|
39
|
+
* @param assumeSingleNode - When true, skips parent context validation and assumes the element is a standalone fragment
|
|
40
|
+
* @returns The computed role result including the resolved role spec (or null) and any error type
|
|
41
|
+
*/
|
|
11
42
|
export function getComputedRole(specs,
|
|
12
43
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
13
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) {
|
|
14
64
|
let lazyImplicitRole;
|
|
15
65
|
const explicitRole = getExplicitRole(specs, el, version);
|
|
16
66
|
const computedRole = explicitRole.role
|
|
@@ -36,24 +86,36 @@ el, version, assumeSingleNode = false) {
|
|
|
36
86
|
* > causes the accessibility tree to be malformed,
|
|
37
87
|
* > the expected results are undefined.
|
|
38
88
|
*
|
|
39
|
-
* Determines whether the
|
|
40
|
-
*
|
|
41
|
-
* 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).
|
|
42
91
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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`.
|
|
96
|
+
*
|
|
97
|
+
* ARIA 1.3 formally resolves this with the definitions of
|
|
98
|
+
* "accessibility child" and "accessibility parent", and
|
|
99
|
+
* `matchesContextRole` additionally skips `generic` elements.
|
|
47
100
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* according to the sample code in WAI-ARIA specification.
|
|
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
|
|
52
104
|
*/
|
|
53
|
-
if (computedRole.role &&
|
|
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)) {
|
|
54
116
|
/**
|
|
55
117
|
* An element fragment that serves as the root without a parent element
|
|
56
|
-
* cannot satisfy the "Required
|
|
118
|
+
* cannot satisfy the "Required Accessibility Parent Role" condition.
|
|
57
119
|
* Therefore, under normal circumstances, the `role` will disappear.
|
|
58
120
|
* However, in this specific case, it will fall back to both explicit
|
|
59
121
|
* and implicit roles. Note that the explicit role takes precedence.
|
|
@@ -72,7 +134,7 @@ el, version, assumeSingleNode = false) {
|
|
|
72
134
|
errorType: 'NO_OWNER',
|
|
73
135
|
};
|
|
74
136
|
}
|
|
75
|
-
if (!matchesContextRole(computedRole.role.
|
|
137
|
+
if (!matchesContextRole(computedRole.role.requiredAccessibilityParentRole, el, specs, version)) {
|
|
76
138
|
return {
|
|
77
139
|
el,
|
|
78
140
|
role: null,
|
|
@@ -114,15 +176,12 @@ el, version, assumeSingleNode = false) {
|
|
|
114
176
|
// It doesn't been specified a valid explicit role.
|
|
115
177
|
(explicitRole.role === null || explicitRole.errorType != null) &&
|
|
116
178
|
// It is an SVG element.
|
|
117
|
-
el.namespaceURI ===
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
role: null,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
179
|
+
el.namespaceURI === SVG_NAMESPACE &&
|
|
180
|
+
!hasSvgAccessibleNameSource(el)) {
|
|
181
|
+
return {
|
|
182
|
+
el,
|
|
183
|
+
role: null,
|
|
184
|
+
};
|
|
126
185
|
}
|
|
127
186
|
if (computedRole.role && !isPresentational(computedRole.role.name)) {
|
|
128
187
|
return computedRole;
|
|
@@ -181,8 +240,8 @@ el, version, assumeSingleNode = false) {
|
|
|
181
240
|
if (explicitRole.role) {
|
|
182
241
|
const nonPresentationalAncestor = getNonPresentationalAncestor(el, specs, version);
|
|
183
242
|
if (nonPresentationalAncestor.role &&
|
|
184
|
-
nonPresentationalAncestor.role?.
|
|
185
|
-
nonPresentationalAncestor.role.
|
|
243
|
+
nonPresentationalAncestor.role?.allowedAccessibilityChildRoles.length > 0 &&
|
|
244
|
+
nonPresentationalAncestor.role.allowedAccessibilityChildRoles.some(expected => {
|
|
186
245
|
// const ancestor = nonPresentationalAncestor.el;
|
|
187
246
|
// const ancestorImplicitRole = getImplicitRole(specs, ancestor, version);
|
|
188
247
|
// console.log({ nonPresentationalAncestor, ancestorImplicitRole });
|
|
@@ -240,3 +299,36 @@ el, predicate) {
|
|
|
240
299
|
}
|
|
241
300
|
return list.some(predicate);
|
|
242
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,22 @@
|
|
|
1
1
|
import type { ImplicitRole } from '../../types/aria.js';
|
|
2
2
|
import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Determines the implicit (native) ARIA role for an element based on its tag name,
|
|
5
|
+
* namespace, and any matching conditions defined in the HTML-ARIA spec.
|
|
6
|
+
*
|
|
7
|
+
* @param specs - The full markup language specification
|
|
8
|
+
* @param el - The DOM element to determine the implicit role for
|
|
9
|
+
* @param version - The ARIA specification version to use
|
|
10
|
+
* @returns The computed role result containing the implicit role spec, or null if no corresponding role exists
|
|
11
|
+
*/
|
|
3
12
|
export declare function getImplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole;
|
|
13
|
+
/**
|
|
14
|
+
* Retrieves the implicit role name string for an element without resolving
|
|
15
|
+
* the full role specification. Returns `false` when no corresponding role exists.
|
|
16
|
+
*
|
|
17
|
+
* @param el - The DOM element to look up the implicit role name for
|
|
18
|
+
* @param version - The ARIA specification version to use
|
|
19
|
+
* @param specs - The full markup language specification
|
|
20
|
+
* @returns The implicit role name string, or `false` if the element has no corresponding role
|
|
21
|
+
*/
|
|
4
22
|
export declare function getImplicitRoleName(el: Element, version: ARIAVersion, specs: MLMLSpec): ImplicitRole;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { getImplicitRole as _getImplicitRole } from './get-implicit-role-spec.js';
|
|
2
2
|
import { getRoleSpec } from './get-role-spec.js';
|
|
3
3
|
import { resolveNamespace } from '../../utils/resolve-namespace.js';
|
|
4
|
+
/**
|
|
5
|
+
* Determines the implicit (native) ARIA role for an element based on its tag name,
|
|
6
|
+
* namespace, and any matching conditions defined in the HTML-ARIA spec.
|
|
7
|
+
*
|
|
8
|
+
* @param specs - The full markup language specification
|
|
9
|
+
* @param el - The DOM element to determine the implicit role for
|
|
10
|
+
* @param version - The ARIA specification version to use
|
|
11
|
+
* @returns The computed role result containing the implicit role spec, or null if no corresponding role exists
|
|
12
|
+
*/
|
|
4
13
|
export function getImplicitRole(specs,
|
|
5
14
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
6
15
|
el, version) {
|
|
@@ -29,6 +38,15 @@ el, version) {
|
|
|
29
38
|
},
|
|
30
39
|
};
|
|
31
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Retrieves the implicit role name string for an element without resolving
|
|
43
|
+
* the full role specification. Returns `false` when no corresponding role exists.
|
|
44
|
+
*
|
|
45
|
+
* @param el - The DOM element to look up the implicit role name for
|
|
46
|
+
* @param version - The ARIA specification version to use
|
|
47
|
+
* @param specs - The full markup language specification
|
|
48
|
+
* @returns The implicit role name string, or `false` if the element has no corresponding role
|
|
49
|
+
*/
|
|
32
50
|
export function getImplicitRoleName(
|
|
33
51
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
34
52
|
el, version, specs) {
|
|
@@ -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;
|