@markuplint/ml-spec 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/lib/algorithm/aria/accname/aria-steps.d.ts +14 -2
- package/lib/algorithm/aria/accname/aria-steps.js +22 -2
- package/lib/algorithm/aria/accname/compute.d.ts +35 -3
- package/lib/algorithm/aria/accname/compute.js +57 -9
- package/lib/algorithm/aria/accname/element-names.d.ts +2 -2
- package/lib/algorithm/aria/accname/element-names.js +2 -1
- package/lib/algorithm/aria/accname/helpers.d.ts +23 -4
- package/lib/algorithm/aria/accname/helpers.js +45 -12
- package/lib/algorithm/aria/accname/index.d.ts +2 -2
- package/lib/algorithm/aria/accname/index.js +1 -1
- package/lib/algorithm/aria/accname/label-steps.d.ts +2 -2
- package/lib/algorithm/aria/accname/label-steps.js +12 -3
- package/lib/algorithm/aria/accname/result.d.ts +21 -0
- package/lib/algorithm/aria/accname/result.js +28 -0
- package/lib/algorithm/aria/accname/types.d.ts +39 -0
- package/lib/algorithm/aria/accname-computation.js +6 -0
- package/lib/algorithm/aria/get-computed-role.js +11 -12
- package/lib/algorithm/aria/get-explicit-role.d.ts +6 -0
- package/lib/algorithm/aria/get-explicit-role.js +73 -7
- package/lib/algorithm/aria/is-presentational.d.ts +13 -1
- package/lib/algorithm/aria/is-presentational.js +14 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/utils/directive-resolver.js +1 -1
- package/lib/utils/get-spec-by-tag-name.d.ts +2 -1
- package/lib/utils/get-spec-by-tag-name.js +14 -2
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **ml-spec:** keep roles that are not permitted and align the accessible name with AccName ([6c38c60](https://github.com/markuplint/markuplint/commit/6c38c60971690edec8b6453e50c76146eb10deb2)), closes [#4408](https://github.com/markuplint/markuplint/issues/4408)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
|
|
18
|
+
* **ml-spec:** ask the element whether a name is the attribute, and keep the spelling of a directive ([e414513](https://github.com/markuplint/markuplint/commit/e414513598bf068c323dc335b4177c63941d5775))
|
|
19
|
+
* **ml-spec:** keep the results of getSpecByTagName for each list of specs ([3322329](https://github.com/markuplint/markuplint/commit/33223295d7990561a8686a80029706c096e65666))
|
|
20
|
+
|
|
21
|
+
### Reverts
|
|
22
|
+
|
|
23
|
+
* **ml-spec:** leave is-exposed as it was on dev ([ce4c5c8](https://github.com/markuplint/markuplint/commit/ce4c5c833ca542bcfef8460d1a8228c2a3144f87))
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
# [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
|
|
7
27
|
|
|
8
28
|
### Bug Fixes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
1
|
+
import type { AccnameComputeFn, AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Implements AccName 1.2 §4.3.2 Step 2B:
|
|
4
4
|
* "If the current node has an `aria-labelledby` attribute that contains
|
|
@@ -16,7 +16,19 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
|
|
|
16
16
|
*
|
|
17
17
|
* @see https://www.w3.org/TR/accname-1.2/#comp_labelledby_traversal — AccName 1.2 §4.3.2 Step 2B
|
|
18
18
|
*/
|
|
19
|
-
export declare function resolveAriaLabelledby(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn:
|
|
19
|
+
export declare function resolveAriaLabelledby(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn): AccnameResult | null;
|
|
20
|
+
/**
|
|
21
|
+
* Computes the text alternative of a node directly referenced by
|
|
22
|
+
* `aria-labelledby` (Step 2B, LabelledBy Traversal).
|
|
23
|
+
*
|
|
24
|
+
* Step 2A is skipped for the whole subtree only when the referenced node is
|
|
25
|
+
* hidden; otherwise its hidden descendants contribute nothing.
|
|
26
|
+
*
|
|
27
|
+
* @see https://w3c.github.io/accname/#comp_hidden_not_referenced
|
|
28
|
+
*
|
|
29
|
+
* > If the current node is hidden and is: Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeReferenced(referenced: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn): AccnameResult;
|
|
20
32
|
/**
|
|
21
33
|
* Implements AccName 1.2 §4.3.2 Step 2D:
|
|
22
34
|
* "If the current node has an `aria-label` attribute whose value is
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
|
-
import { flattenText, makeResult } from './
|
|
2
|
+
import { flattenText, makeResult } from './result.js';
|
|
3
3
|
/**
|
|
4
4
|
* Implements AccName 1.2 §4.3.2 Step 2B:
|
|
5
5
|
* "If the current node has an `aria-labelledby` attribute that contains
|
|
@@ -52,7 +52,7 @@ export function resolveAriaLabelledby(el, resolver, visited, computeFn) {
|
|
|
52
52
|
// Each branch gets its own visited set so that one IDREF's
|
|
53
53
|
// traversal doesn't block resolution of a later IDREF.
|
|
54
54
|
const innerVisited = new Set([...newVisited, id]);
|
|
55
|
-
const result =
|
|
55
|
+
const result = computeReferenced(referenced, resolver, innerVisited, computeFn);
|
|
56
56
|
if (result.name) {
|
|
57
57
|
parts.push(result.name);
|
|
58
58
|
}
|
|
@@ -63,6 +63,26 @@ export function resolveAriaLabelledby(el, resolver, visited, computeFn) {
|
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Computes the text alternative of a node directly referenced by
|
|
68
|
+
* `aria-labelledby` (Step 2B, LabelledBy Traversal).
|
|
69
|
+
*
|
|
70
|
+
* Step 2A is skipped for the whole subtree only when the referenced node is
|
|
71
|
+
* hidden; otherwise its hidden descendants contribute nothing.
|
|
72
|
+
*
|
|
73
|
+
* @see https://w3c.github.io/accname/#comp_hidden_not_referenced
|
|
74
|
+
*
|
|
75
|
+
* > If the current node is hidden and is: Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.
|
|
76
|
+
*/
|
|
77
|
+
export function computeReferenced(referenced, resolver, visited, computeFn) {
|
|
78
|
+
const traversalResolver = resolver.isHidden(referenced)
|
|
79
|
+
? {
|
|
80
|
+
...resolver,
|
|
81
|
+
isHidden: () => false,
|
|
82
|
+
}
|
|
83
|
+
: resolver;
|
|
84
|
+
return computeFn(referenced, traversalResolver, true, visited);
|
|
85
|
+
}
|
|
66
86
|
/**
|
|
67
87
|
* Implements AccName 1.2 §4.3.2 Step 2D:
|
|
68
88
|
* "If the current node has an `aria-label` attribute whose value is
|
|
@@ -8,8 +8,9 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
|
|
|
8
8
|
*
|
|
9
9
|
* Control flow (Steps map to AccName 1.2 §4.3.2):
|
|
10
10
|
*
|
|
11
|
-
* 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`)
|
|
12
|
-
*
|
|
11
|
+
* 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`), return
|
|
12
|
+
* empty immediately, unless it is in an `aria-labelledby` traversal whose
|
|
13
|
+
* directly referenced node is hidden.
|
|
13
14
|
* 2. **Pre-computed name** — (Implementation-specific extension) If the
|
|
14
15
|
* resolver provides `getPrecomputedName`, check it before standard steps.
|
|
15
16
|
* Used by ml-core's Pretender integration for framework components.
|
|
@@ -18,7 +19,9 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
|
|
|
18
19
|
* labelledby traversal (`inLabelledbyTraversal`) to prevent re-entry.
|
|
19
20
|
* 4. **Step 2D — `aria-label`** — Use the `aria-label` attribute value.
|
|
20
21
|
* 5. **Step 2E — Element-specific name** — Dispatch to HTML-AAM §4.1
|
|
21
|
-
* element-specific rules (label association, alt, value, legend, caption, SVG title)
|
|
22
|
+
* element-specific rules (label association, alt, value, legend, caption, SVG title),
|
|
23
|
+
* skipped for an element the resolver reports as exposed as presentational
|
|
24
|
+
* (`isExposedAsPresentational`).
|
|
22
25
|
* 6. **Step 2F — Name from content** — If the element's role allows
|
|
23
26
|
* `nameFrom: ["content"]`, or the element is referenced by `aria-labelledby`
|
|
24
27
|
* (`inLabelledbyTraversal`), recursively collect child text (including
|
|
@@ -32,6 +35,35 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
|
|
|
32
35
|
* @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
|
|
33
36
|
*/
|
|
34
37
|
export declare function computeAccessibleName(el: AccnameElement, resolver: AccnameResolver): AccnameResult;
|
|
38
|
+
/**
|
|
39
|
+
* Computes the text alternative of an element as a node referenced by
|
|
40
|
+
* `aria-labelledby` or `aria-describedby` (the LabelledBy Traversal of Step 2B,
|
|
41
|
+
* which the description computation also uses).
|
|
42
|
+
*
|
|
43
|
+
* @param el - The referenced element
|
|
44
|
+
* @param resolver - Environment-dependent resolver for DOM traversal and role queries
|
|
45
|
+
* @returns The computed text alternative and its source
|
|
46
|
+
* @see https://w3c.github.io/accname/#comp_labelledby_traversal
|
|
47
|
+
*/
|
|
48
|
+
export declare function computeReferencedTextAlternative(el: AccnameElement, resolver: AccnameResolver): AccnameResult;
|
|
49
|
+
/**
|
|
50
|
+
* Computes the host language text alternative of an element (AccName §4.3.2
|
|
51
|
+
* Step 2E, HTML-AAM §4.1), such as the `alt` of an image or the `title` of an
|
|
52
|
+
* SVG element, whether or not the element is exposed as presentational.
|
|
53
|
+
*
|
|
54
|
+
* Tells what an explicit `none` or `presentation` role drops from the name:
|
|
55
|
+
* every source the HTML-AAM rules of Step 2E give (`label`, `alt`, `value`,
|
|
56
|
+
* `placeholder`, `legend`, `caption`, `svg-title`, and the `default` label of a
|
|
57
|
+
* submit, reset or image button). The sources that Step 2E shares with other
|
|
58
|
+
* steps (`content`, the `title` attribute) are not dropped by the role, and give
|
|
59
|
+
* `null`.
|
|
60
|
+
*
|
|
61
|
+
* @param el - The element
|
|
62
|
+
* @param resolver - Environment-dependent resolver for DOM traversal and role queries
|
|
63
|
+
* @returns The text alternative and its source, or `null` if the element has none
|
|
64
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
65
|
+
*/
|
|
66
|
+
export declare function computeHostLanguageLabel(el: AccnameElement, resolver: AccnameResolver): AccnameResult | null;
|
|
35
67
|
/**
|
|
36
68
|
* @param inLabelledbyTraversal - When true, Step 2B is skipped to prevent re-entry
|
|
37
69
|
* into `resolveAriaLabelledby`, and Step 2F name-from-content is enabled regardless
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
|
-
import { resolveAriaLabel, resolveAriaLabelledby } from './aria-steps.js';
|
|
2
|
+
import { computeReferenced, resolveAriaLabel, resolveAriaLabelledby } from './aria-steps.js';
|
|
3
3
|
import { getElementSpecificName } from './element-names.js';
|
|
4
|
-
import {
|
|
4
|
+
import { resolveNameFromContent } from './helpers.js';
|
|
5
|
+
import { makeResult } from './result.js';
|
|
5
6
|
const EMPTY_RESULT = { name: '', source: null };
|
|
6
7
|
/**
|
|
7
8
|
* Computes the accessible name for an element.
|
|
@@ -12,8 +13,9 @@ const EMPTY_RESULT = { name: '', source: null };
|
|
|
12
13
|
*
|
|
13
14
|
* Control flow (Steps map to AccName 1.2 §4.3.2):
|
|
14
15
|
*
|
|
15
|
-
* 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`)
|
|
16
|
-
*
|
|
16
|
+
* 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`), return
|
|
17
|
+
* empty immediately, unless it is in an `aria-labelledby` traversal whose
|
|
18
|
+
* directly referenced node is hidden.
|
|
17
19
|
* 2. **Pre-computed name** — (Implementation-specific extension) If the
|
|
18
20
|
* resolver provides `getPrecomputedName`, check it before standard steps.
|
|
19
21
|
* Used by ml-core's Pretender integration for framework components.
|
|
@@ -22,7 +24,9 @@ const EMPTY_RESULT = { name: '', source: null };
|
|
|
22
24
|
* labelledby traversal (`inLabelledbyTraversal`) to prevent re-entry.
|
|
23
25
|
* 4. **Step 2D — `aria-label`** — Use the `aria-label` attribute value.
|
|
24
26
|
* 5. **Step 2E — Element-specific name** — Dispatch to HTML-AAM §4.1
|
|
25
|
-
* element-specific rules (label association, alt, value, legend, caption, SVG title)
|
|
27
|
+
* element-specific rules (label association, alt, value, legend, caption, SVG title),
|
|
28
|
+
* skipped for an element the resolver reports as exposed as presentational
|
|
29
|
+
* (`isExposedAsPresentational`).
|
|
26
30
|
* 6. **Step 2F — Name from content** — If the element's role allows
|
|
27
31
|
* `nameFrom: ["content"]`, or the element is referenced by `aria-labelledby`
|
|
28
32
|
* (`inLabelledbyTraversal`), recursively collect child text (including
|
|
@@ -38,6 +42,43 @@ const EMPTY_RESULT = { name: '', source: null };
|
|
|
38
42
|
export function computeAccessibleName(el, resolver) {
|
|
39
43
|
return computeAccessibleNameInternal(el, resolver, false, new Set());
|
|
40
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Computes the text alternative of an element as a node referenced by
|
|
47
|
+
* `aria-labelledby` or `aria-describedby` (the LabelledBy Traversal of Step 2B,
|
|
48
|
+
* which the description computation also uses).
|
|
49
|
+
*
|
|
50
|
+
* @param el - The referenced element
|
|
51
|
+
* @param resolver - Environment-dependent resolver for DOM traversal and role queries
|
|
52
|
+
* @returns The computed text alternative and its source
|
|
53
|
+
* @see https://w3c.github.io/accname/#comp_labelledby_traversal
|
|
54
|
+
*/
|
|
55
|
+
export function computeReferencedTextAlternative(el, resolver) {
|
|
56
|
+
return computeReferenced(el, resolver, new Set(el.id ? [el.id] : []), computeAccessibleNameInternal);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Computes the host language text alternative of an element (AccName §4.3.2
|
|
60
|
+
* Step 2E, HTML-AAM §4.1), such as the `alt` of an image or the `title` of an
|
|
61
|
+
* SVG element, whether or not the element is exposed as presentational.
|
|
62
|
+
*
|
|
63
|
+
* Tells what an explicit `none` or `presentation` role drops from the name:
|
|
64
|
+
* every source the HTML-AAM rules of Step 2E give (`label`, `alt`, `value`,
|
|
65
|
+
* `placeholder`, `legend`, `caption`, `svg-title`, and the `default` label of a
|
|
66
|
+
* submit, reset or image button). The sources that Step 2E shares with other
|
|
67
|
+
* steps (`content`, the `title` attribute) are not dropped by the role, and give
|
|
68
|
+
* `null`.
|
|
69
|
+
*
|
|
70
|
+
* @param el - The element
|
|
71
|
+
* @param resolver - Environment-dependent resolver for DOM traversal and role queries
|
|
72
|
+
* @returns The text alternative and its source, or `null` if the element has none
|
|
73
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
74
|
+
*/
|
|
75
|
+
export function computeHostLanguageLabel(el, resolver) {
|
|
76
|
+
const result = getElementSpecificName(el, resolver, new Set(), computeAccessibleNameInternal, false);
|
|
77
|
+
if (!result?.source || result.source === 'content' || result.source === 'title') {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
41
82
|
/**
|
|
42
83
|
* @param inLabelledbyTraversal - When true, Step 2B is skipped to prevent re-entry
|
|
43
84
|
* into `resolveAriaLabelledby`, and Step 2F name-from-content is enabled regardless
|
|
@@ -45,8 +86,12 @@ export function computeAccessibleName(el, resolver) {
|
|
|
45
86
|
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
|
|
46
87
|
*/
|
|
47
88
|
export function computeAccessibleNameInternal(el, resolver, inLabelledbyTraversal, visited) {
|
|
48
|
-
// AccName 1.2 §4.3.2 Step 2A: Hidden elements return empty
|
|
49
|
-
|
|
89
|
+
// AccName 1.2 §4.3.2 Step 2A: Hidden elements return empty. In an aria-labelledby
|
|
90
|
+
// traversal whose referenced node is hidden, `computeReferenced` passes a
|
|
91
|
+
// resolver that finds nothing hidden.
|
|
92
|
+
const hidden = resolver.isHidden(el);
|
|
93
|
+
resolver.onVisit?.(el, { hidden, embeddedControl: false, inLabelledbyTraversal });
|
|
94
|
+
if (hidden) {
|
|
50
95
|
return EMPTY_RESULT;
|
|
51
96
|
}
|
|
52
97
|
// [Implementation-specific] Pre-computed name (e.g., Pretender integration) — checked after hidden check
|
|
@@ -68,8 +113,11 @@ export function computeAccessibleNameInternal(el, resolver, inLabelledbyTraversa
|
|
|
68
113
|
if (ariaLabelResult) {
|
|
69
114
|
return ariaLabelResult;
|
|
70
115
|
}
|
|
71
|
-
// AccName 1.2 §4.3.2 Step 2E: Element-specific name (HTML-AAM §4.1)
|
|
72
|
-
|
|
116
|
+
// AccName 1.2 §4.3.2 Step 2E: Element-specific name (HTML-AAM §4.1),
|
|
117
|
+
// "unless the current node is exposed as presentational"
|
|
118
|
+
const elementResult = resolver.isExposedAsPresentational?.(el)
|
|
119
|
+
? null
|
|
120
|
+
: getElementSpecificName(el, resolver, visited, computeAccessibleNameInternal, inLabelledbyTraversal);
|
|
73
121
|
if (elementResult) {
|
|
74
122
|
return elementResult;
|
|
75
123
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
1
|
+
import type { AccnameComputeFn, AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Implements AccName 1.2 §4.3.2 Step 2E: for elements that have a native
|
|
4
4
|
* host language text alternative, use that alternative. The specific rules
|
|
@@ -10,4 +10,4 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
|
|
|
10
10
|
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2E
|
|
11
11
|
* @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
|
|
12
12
|
*/
|
|
13
|
-
export declare function getElementSpecificName(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn:
|
|
13
|
+
export declare function getElementSpecificName(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): AccnameResult | null;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
2
|
import { DEFAULT_IMAGE_LABEL, DEFAULT_RESET_LABEL, DEFAULT_SUBMIT_LABEL, TEXT_INPUT_TYPES, } from '../../../const/index.js';
|
|
3
|
-
import { findChildByLocalName, getInputType, getTextContent, isSvgElement,
|
|
3
|
+
import { findChildByLocalName, getInputType, getTextContent, isSvgElement, resolveNameFromContent } from './helpers.js';
|
|
4
4
|
import { resolveLabelText } from './label-steps.js';
|
|
5
|
+
import { makeResult } from './result.js';
|
|
5
6
|
/**
|
|
6
7
|
* Implements AccName 1.2 §4.3.2 Step 2E: for elements that have a native
|
|
7
8
|
* host language text alternative, use that alternative. The specific rules
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type { AccnameElement, AccnameNode, AccnameResolver
|
|
1
|
+
import type { AccnameComputeFn, AccnameElement, AccnameNode, AccnameResolver } from './types.js';
|
|
2
2
|
export { EMBEDDED_CONTROL_ROLES } from '../../../const/index.js';
|
|
3
|
-
export declare function makeResult(name: string, source: AccnameSource | null): AccnameResult;
|
|
4
|
-
export declare function flattenText(text: string): string;
|
|
5
3
|
/**
|
|
6
4
|
* Implements AccName 1.2 §4.3.2 Steps 2F and 2C:
|
|
7
5
|
*
|
|
@@ -21,7 +19,19 @@ export declare function flattenText(text: string): string;
|
|
|
21
19
|
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2F
|
|
22
20
|
* @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
|
|
23
21
|
*/
|
|
24
|
-
export declare function resolveNameFromContent(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn:
|
|
22
|
+
export declare function resolveNameFromContent(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): string;
|
|
23
|
+
/**
|
|
24
|
+
* The text an embedded control reached by name-from-content recursion
|
|
25
|
+
* contributes, in the order of AccName §4.3.2: nothing when it is hidden
|
|
26
|
+
* (Step 2A), the text of its `aria-labelledby` outside an ongoing
|
|
27
|
+
* `aria-labelledby` traversal (Step 2B), otherwise its value (Step 2C), as its
|
|
28
|
+
* `aria-label` is ignored (Step 2D).
|
|
29
|
+
*
|
|
30
|
+
* @see https://w3c.github.io/accname/#comp_label
|
|
31
|
+
*
|
|
32
|
+
* > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an embedded control, ignore aria-label and skip to rule Embedded Control.
|
|
33
|
+
*/
|
|
34
|
+
export declare function embeddedControlText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): string;
|
|
25
35
|
/**
|
|
26
36
|
* The child nodes to traverse for `el`: what the resolver says it renders,
|
|
27
37
|
* or `el.childNodes` when it says nothing.
|
|
@@ -38,6 +48,15 @@ export declare function findAncestorLabel(el: AccnameElement): AccnameElement |
|
|
|
38
48
|
export declare function resolveLabel(el: AccnameElement, resolver: AccnameResolver): readonly AccnameElement[];
|
|
39
49
|
export declare function getInputType(el: AccnameElement): string;
|
|
40
50
|
export declare function isSvgElement(el: AccnameElement): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the element is an SVG `title` or `desc`. They are never rendered,
|
|
53
|
+
* so their text is not content of the parent: a `title` gives the name only
|
|
54
|
+
* through Step 2E of its parent (`handleSvgElement`), which an element exposed
|
|
55
|
+
* as presentational skips.
|
|
56
|
+
*
|
|
57
|
+
* @see https://w3c.github.io/svgwg/svg2-draft/struct.html#TermNeverRenderedElement
|
|
58
|
+
*/
|
|
59
|
+
export declare function isSvgDescriptiveElement(el: AccnameElement): boolean;
|
|
41
60
|
/**
|
|
42
61
|
* Escapes a string for safe use inside a CSS selector.
|
|
43
62
|
*
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
2
|
import { ELEMENT_NODE, SVG_NAMESPACE, TEXT_NODE, TEXT_LIKE_INPUT_TYPES } from '../../../const/index.js';
|
|
3
|
+
import { resolveAriaLabelledby } from './aria-steps.js';
|
|
3
4
|
export { EMBEDDED_CONTROL_ROLES } from '../../../const/index.js';
|
|
4
|
-
export function makeResult(name, source) {
|
|
5
|
-
const trimmed = flattenText(name);
|
|
6
|
-
return {
|
|
7
|
-
name: trimmed,
|
|
8
|
-
source: trimmed ? source : null,
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
export function flattenText(text) {
|
|
12
|
-
return text.replaceAll(/\s+/g, ' ').trim();
|
|
13
|
-
}
|
|
14
5
|
/**
|
|
15
6
|
* Implements AccName 1.2 §4.3.2 Steps 2F and 2C:
|
|
16
7
|
*
|
|
@@ -38,9 +29,12 @@ export function resolveNameFromContent(el, resolver, visited, computeFn, inLabel
|
|
|
38
29
|
}
|
|
39
30
|
else if (child.nodeType === ELEMENT_NODE) {
|
|
40
31
|
const childEl = child;
|
|
32
|
+
if (isSvgDescriptiveElement(childEl)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
41
35
|
if (resolver.isEmbeddedControl(childEl)) {
|
|
42
36
|
// Step 2C: Embedded controls — use value directly, aria-label is ignored
|
|
43
|
-
parts.push(
|
|
37
|
+
parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
|
|
44
38
|
}
|
|
45
39
|
else {
|
|
46
40
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
@@ -84,8 +78,11 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
|
|
|
84
78
|
}
|
|
85
79
|
else if (child.nodeType === ELEMENT_NODE) {
|
|
86
80
|
const childEl = child;
|
|
81
|
+
if (isSvgDescriptiveElement(childEl)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
87
84
|
if (resolver.isEmbeddedControl(childEl)) {
|
|
88
|
-
parts.push(
|
|
85
|
+
parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
|
|
89
86
|
}
|
|
90
87
|
else {
|
|
91
88
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
@@ -100,6 +97,31 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
|
|
|
100
97
|
}
|
|
101
98
|
return parts.join(' ');
|
|
102
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The text an embedded control reached by name-from-content recursion
|
|
102
|
+
* contributes, in the order of AccName §4.3.2: nothing when it is hidden
|
|
103
|
+
* (Step 2A), the text of its `aria-labelledby` outside an ongoing
|
|
104
|
+
* `aria-labelledby` traversal (Step 2B), otherwise its value (Step 2C), as its
|
|
105
|
+
* `aria-label` is ignored (Step 2D).
|
|
106
|
+
*
|
|
107
|
+
* @see https://w3c.github.io/accname/#comp_label
|
|
108
|
+
*
|
|
109
|
+
* > If traversal of the current node is due to Name From Content descendant node recursion and the current node is an embedded control, ignore aria-label and skip to rule Embedded Control.
|
|
110
|
+
*/
|
|
111
|
+
export function embeddedControlText(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
112
|
+
const hidden = resolver.isHidden(el);
|
|
113
|
+
resolver.onVisit?.(el, { hidden, embeddedControl: true, inLabelledbyTraversal });
|
|
114
|
+
if (hidden) {
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
if (!inLabelledbyTraversal) {
|
|
118
|
+
const labelledby = resolveAriaLabelledby(el, resolver, visited, computeFn);
|
|
119
|
+
if (labelledby) {
|
|
120
|
+
return labelledby.name;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return getEmbeddedControlValue(el, resolver);
|
|
124
|
+
}
|
|
103
125
|
/**
|
|
104
126
|
* Implements AccName 1.2 §4.3.2 Step 2C embedded control value resolution:
|
|
105
127
|
* - **Range controls** (slider/spinbutton/input[range]):
|
|
@@ -287,6 +309,17 @@ export function getInputType(el) {
|
|
|
287
309
|
export function isSvgElement(el) {
|
|
288
310
|
return el.namespaceURI === SVG_NAMESPACE;
|
|
289
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Whether the element is an SVG `title` or `desc`. They are never rendered,
|
|
314
|
+
* so their text is not content of the parent: a `title` gives the name only
|
|
315
|
+
* through Step 2E of its parent (`handleSvgElement`), which an element exposed
|
|
316
|
+
* as presentational skips.
|
|
317
|
+
*
|
|
318
|
+
* @see https://w3c.github.io/svgwg/svg2-draft/struct.html#TermNeverRenderedElement
|
|
319
|
+
*/
|
|
320
|
+
export function isSvgDescriptiveElement(el) {
|
|
321
|
+
return isSvgElement(el) && (el.localName === 'title' || el.localName === 'desc');
|
|
322
|
+
}
|
|
290
323
|
/**
|
|
291
324
|
* Escapes a string for safe use inside a CSS selector.
|
|
292
325
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { computeAccessibleName } from './compute.js';
|
|
1
|
+
export { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative } from './compute.js';
|
|
2
2
|
export { escapeCSS } from './helpers.js';
|
|
3
3
|
export { hasSvgAccessibleNameSource } from './svg-helpers.js';
|
|
4
|
-
export type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource } from './types.js';
|
|
4
|
+
export type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource, AccnameVisit, } from './types.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { computeAccessibleName } from './compute.js';
|
|
1
|
+
export { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative } from './compute.js';
|
|
2
2
|
export { escapeCSS } from './helpers.js';
|
|
3
3
|
export { hasSvgAccessibleNameSource } from './svg-helpers.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
1
|
+
import type { AccnameComputeFn, AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Part of AccName 1.2 §4.3.2 Step 2E — for labelable elements, the HTML label
|
|
4
4
|
* association is checked before other element-specific rules.
|
|
@@ -6,4 +6,4 @@ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js'
|
|
|
6
6
|
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2E
|
|
7
7
|
* @see https://www.w3.org/TR/html-aam-1.0/#el-input-text — HTML-AAM label association
|
|
8
8
|
*/
|
|
9
|
-
export declare function resolveLabelText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn:
|
|
9
|
+
export declare function resolveLabelText(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: AccnameComputeFn, inLabelledbyTraversal: boolean): AccnameResult | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
2
|
import { ELEMENT_NODE, TEXT_NODE } from '../../../const/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { embeddedControlText, getChildNodes, isSvgDescriptiveElement, resolveLabel } from './helpers.js';
|
|
4
|
+
import { flattenText, makeResult } from './result.js';
|
|
4
5
|
/**
|
|
5
6
|
* Part of AccName 1.2 §4.3.2 Step 2E — for labelable elements, the HTML label
|
|
6
7
|
* association is checked before other element-specific rules.
|
|
@@ -30,6 +31,10 @@ export function resolveLabelText(el, resolver, visited, computeFn, inLabelledbyT
|
|
|
30
31
|
* Excludes the labeled element itself to prevent circular inclusion. Parts are
|
|
31
32
|
* joined with a space separator, matching the AccName 1.2 concatenation
|
|
32
33
|
* behavior for label text.
|
|
34
|
+
*
|
|
35
|
+
* The children are treated as name-from-content recursion treats them
|
|
36
|
+
* (`resolveNameFromContent`): a hidden one and an SVG `title` or `desc`
|
|
37
|
+
* contribute nothing, and an embedded control contributes its value.
|
|
33
38
|
*/
|
|
34
39
|
function collectLabelText(label, labeledElement, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
35
40
|
const parts = [];
|
|
@@ -39,14 +44,18 @@ function collectLabelText(label, labeledElement, resolver, visited, computeFn, i
|
|
|
39
44
|
}
|
|
40
45
|
else if (child.nodeType === ELEMENT_NODE) {
|
|
41
46
|
const childEl = child;
|
|
42
|
-
if (childEl === labeledElement) {
|
|
47
|
+
if (childEl === labeledElement || isSvgDescriptiveElement(childEl)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (resolver.isEmbeddedControl(childEl)) {
|
|
51
|
+
parts.push(embeddedControlText(childEl, resolver, visited, computeFn, inLabelledbyTraversal));
|
|
43
52
|
continue;
|
|
44
53
|
}
|
|
45
54
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
46
55
|
if (result.name) {
|
|
47
56
|
parts.push(result.name);
|
|
48
57
|
}
|
|
49
|
-
else {
|
|
58
|
+
else if (!resolver.isHidden(childEl)) {
|
|
50
59
|
// Transparent traversal: when the child element has no accessible name
|
|
51
60
|
// (e.g., <span> with role="generic"), recursively collect its descendant text.
|
|
52
61
|
// This matches the behavior of resolveNameFromContent's fallback.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result builders of the accessible name computation.
|
|
3
|
+
*
|
|
4
|
+
* A module of their own, not part of `helpers.ts`: `aria-steps.ts` builds
|
|
5
|
+
* results, and `helpers.ts` runs Step 2B for an embedded control through
|
|
6
|
+
* `aria-steps.ts`, so keeping them in `helpers.ts` would make the two import
|
|
7
|
+
* each other.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { AccnameResult, AccnameSource } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Builds a result from a name, flattened; an empty name has no source.
|
|
14
|
+
*/
|
|
15
|
+
export declare function makeResult(name: string, source: AccnameSource | null): AccnameResult;
|
|
16
|
+
/**
|
|
17
|
+
* Makes a flat string: whitespace runs become a single space, and the ends are trimmed.
|
|
18
|
+
*
|
|
19
|
+
* @see https://w3c.github.io/accname/#terminology
|
|
20
|
+
*/
|
|
21
|
+
export declare function flattenText(text: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result builders of the accessible name computation.
|
|
3
|
+
*
|
|
4
|
+
* A module of their own, not part of `helpers.ts`: `aria-steps.ts` builds
|
|
5
|
+
* results, and `helpers.ts` runs Step 2B for an embedded control through
|
|
6
|
+
* `aria-steps.ts`, so keeping them in `helpers.ts` would make the two import
|
|
7
|
+
* each other.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Builds a result from a name, flattened; an empty name has no source.
|
|
13
|
+
*/
|
|
14
|
+
export function makeResult(name, source) {
|
|
15
|
+
const trimmed = flattenText(name);
|
|
16
|
+
return {
|
|
17
|
+
name: trimmed,
|
|
18
|
+
source: trimmed ? source : null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Makes a flat string: whitespace runs become a single space, and the ends are trimmed.
|
|
23
|
+
*
|
|
24
|
+
* @see https://w3c.github.io/accname/#terminology
|
|
25
|
+
*/
|
|
26
|
+
export function flattenText(text) {
|
|
27
|
+
return text.replaceAll(/\s+/g, ' ').trim();
|
|
28
|
+
}
|
|
@@ -42,6 +42,19 @@ export interface AccnameResolver {
|
|
|
42
42
|
* in name-from-content computation (textbox, combobox, listbox, spinbutton, slider, searchbox).
|
|
43
43
|
*/
|
|
44
44
|
isEmbeddedControl(el: AccnameElement): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Checks if an element is exposed with an explicit `none` or `presentation`
|
|
47
|
+
* role, so that Step 2E does not use its host language text alternative.
|
|
48
|
+
* When absent, no element is treated so.
|
|
49
|
+
*/
|
|
50
|
+
isExposedAsPresentational?(el: AccnameElement): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Called for every element the computation reaches, before Step 2A, so that
|
|
53
|
+
* a caller can tell which attributes of a descendant took part in a name.
|
|
54
|
+
* An element that the computation does not reach (the subtree of a hidden
|
|
55
|
+
* element, or of one named before content) is never reported.
|
|
56
|
+
*/
|
|
57
|
+
onVisit?(el: AccnameElement, visit: AccnameVisit): void;
|
|
45
58
|
/**
|
|
46
59
|
* Returns a pre-computed name for an element, bypassing the standard algorithm.
|
|
47
60
|
* Used by ml-core for Pretender integration where framework components provide
|
|
@@ -60,6 +73,32 @@ export interface AccnameResolver {
|
|
|
60
73
|
*/
|
|
61
74
|
getChildNodes?(el: AccnameElement): Iterable<AccnameNode>;
|
|
62
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The recursive entry of the computation (`computeAccessibleNameInternal`),
|
|
78
|
+
* handed to the steps that compute the text alternative of another node.
|
|
79
|
+
*/
|
|
80
|
+
export type AccnameComputeFn = (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult;
|
|
81
|
+
/**
|
|
82
|
+
* How the computation reached an element (see `AccnameResolver.onVisit`).
|
|
83
|
+
*/
|
|
84
|
+
export interface AccnameVisit {
|
|
85
|
+
/**
|
|
86
|
+
* Whether Step 2A found the element hidden, which ends the computation of
|
|
87
|
+
* it. Step 2A does not apply inside an `aria-labelledby` traversal whose
|
|
88
|
+
* directly referenced node is hidden, so no element is found hidden there.
|
|
89
|
+
*/
|
|
90
|
+
readonly hidden: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the element is an embedded control reached by name-from-content
|
|
93
|
+
* recursion (Step 2C), whose value is used and whose `aria-label` is ignored.
|
|
94
|
+
*/
|
|
95
|
+
readonly embeddedControl: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Whether the element is part of an ongoing `aria-labelledby` traversal, in
|
|
98
|
+
* which Step 2B is not applied.
|
|
99
|
+
*/
|
|
100
|
+
readonly inLabelledbyTraversal: boolean;
|
|
101
|
+
}
|
|
63
102
|
/**
|
|
64
103
|
* Identifies the source of an element's accessible name per HTML-AAM §4.1.
|
|
65
104
|
* Used for diagnostic purposes (e.g., reporting which source provided the name).
|
|
@@ -2,6 +2,7 @@ import { computeAccessibleName } from './accname/compute.js';
|
|
|
2
2
|
import { EMBEDDED_CONTROL_ROLES, escapeCSS } from './accname/helpers.js';
|
|
3
3
|
import { isNativeEmbeddedControl } from '../../const/index.js';
|
|
4
4
|
import { getComputedRole } from './get-computed-role.js';
|
|
5
|
+
import { isExposedAsPresentational } from './is-presentational.js';
|
|
5
6
|
/**
|
|
6
7
|
* [Implementation-specific] Reentrant guard to prevent infinite recursion when
|
|
7
8
|
* getComputedRole evaluates `:aria(has name)` selectors, which call back into getAccname.
|
|
@@ -121,6 +122,11 @@ el, specs, version) {
|
|
|
121
122
|
// Native HTML embedded controls without explicit role
|
|
122
123
|
return isNativeEmbeddedControl(target);
|
|
123
124
|
},
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
126
|
+
isExposedAsPresentational(target) {
|
|
127
|
+
// TODO: Remove cast when getComputedRole accepts AccnameElement (#3178)
|
|
128
|
+
return isExposedAsPresentational(getComputedRole(specs, target, version));
|
|
129
|
+
},
|
|
124
130
|
};
|
|
125
131
|
}
|
|
126
132
|
/**
|
|
@@ -174,7 +174,7 @@ el, version, assumeSingleNode) {
|
|
|
174
174
|
*/
|
|
175
175
|
if (
|
|
176
176
|
// It doesn't been specified a valid explicit role.
|
|
177
|
-
|
|
177
|
+
explicitRole.role === null &&
|
|
178
178
|
// It is an SVG element.
|
|
179
179
|
el.namespaceURI === SVG_NAMESPACE &&
|
|
180
180
|
!hasSvgAccessibleNameSource(el)) {
|
|
@@ -204,10 +204,9 @@ el, version, assumeSingleNode) {
|
|
|
204
204
|
*
|
|
205
205
|
* 1. It may be focusable
|
|
206
206
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* in the previous processing that computes the permitted role (`getExplicitRole`).
|
|
207
|
+
* ARIA in HTML does not permit the presentational roles on most
|
|
208
|
+
* interactive elements, but `getExplicitRole` keeps such a role with
|
|
209
|
+
* `NO_PERMITTED`, as user agents expose it, so this resolves it.
|
|
211
210
|
*/
|
|
212
211
|
mayBeFocusable(el, specs) &&
|
|
213
212
|
/**
|
|
@@ -264,13 +263,13 @@ el, version, assumeSingleNode) {
|
|
|
264
263
|
* > and an explicit non-presentational role is applied.
|
|
265
264
|
*/
|
|
266
265
|
const { props } = ariaSpecs(specs, version);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
266
|
+
// Asks the element, not its attribute list: whether `ARIA-LABEL` is `aria-label` is up to the element
|
|
267
|
+
// (a document of the HTML syntax folds the name, one that sets it under the name written does not).
|
|
268
|
+
if (props.some(p => p.isGlobal && el.hasAttribute(p.name))) {
|
|
269
|
+
return {
|
|
270
|
+
...implicitRole,
|
|
271
|
+
errorType: 'GLOBAL_PROP_MUST_NOT_BE_PRESENTATIONAL',
|
|
272
|
+
};
|
|
274
273
|
}
|
|
275
274
|
return computedRole;
|
|
276
275
|
}
|
|
@@ -2,6 +2,12 @@ import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Implements the WAI-ARIA "Handling Author Errors" algorithm.
|
|
4
4
|
*
|
|
5
|
+
* The roles that ARIA in HTML does not permit on the element are authoring
|
|
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
|
|
8
|
+
* never rendered.
|
|
9
|
+
*
|
|
5
10
|
* @see https://w3c.github.io/aria/#document-handling_author-errors
|
|
11
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts
|
|
6
12
|
*/
|
|
7
13
|
export declare function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import { XHTML_NAMESPACE } from '../../const/index.js';
|
|
1
2
|
import { getRoleSpec } from './get-role-spec.js';
|
|
2
3
|
import { resolveNamespace } from '../../utils/resolve-namespace.js';
|
|
3
4
|
import { getPermittedRoles } from './get-permitted-roles.js';
|
|
4
5
|
/**
|
|
5
6
|
* Implements the WAI-ARIA "Handling Author Errors" algorithm.
|
|
6
7
|
*
|
|
8
|
+
* The roles that ARIA in HTML does not permit on the element are authoring
|
|
9
|
+
* 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
|
+
* never rendered.
|
|
12
|
+
*
|
|
7
13
|
* @see https://w3c.github.io/aria/#document-handling_author-errors
|
|
14
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts
|
|
8
15
|
*/
|
|
9
16
|
export function getExplicitRole(specs,
|
|
10
17
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -46,13 +53,6 @@ el, version) {
|
|
|
46
53
|
error = 'ABSTRACT';
|
|
47
54
|
continue;
|
|
48
55
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Whether the role is the permitted role according to ARIA in HTML.
|
|
51
|
-
*/
|
|
52
|
-
if (!permittedRoles.some(r => r.name === roleName)) {
|
|
53
|
-
error = 'NO_PERMITTED';
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
56
|
/**
|
|
57
57
|
* > Certain landmark roles require names from authors.
|
|
58
58
|
* > In situations where an author has not specified names for these landmarks,
|
|
@@ -66,6 +66,35 @@ el, version) {
|
|
|
66
66
|
error = 'INVALID_LANDMARK';
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A role that ARIA in HTML does not permit on the element is an
|
|
71
|
+
* authoring error, but user agents still expose it, so the role is
|
|
72
|
+
* kept and the error is reported alongside. An element that is never
|
|
73
|
+
* rendered is not in the accessibility tree at all (WAI-ARIA §7.1),
|
|
74
|
+
* so the role is dropped there.
|
|
75
|
+
*
|
|
76
|
+
* @see https://w3c.github.io/aria/#tree_exclusion
|
|
77
|
+
*
|
|
78
|
+
* > Elements, including their descendent elements, that have host language semantics specifying that the element is not displayed, such as CSS display:none, visibility:hidden, or the HTML hidden attribute.
|
|
79
|
+
*
|
|
80
|
+
* @see https://w3c.github.io/core-aam/#mapping_conflicts
|
|
81
|
+
*
|
|
82
|
+
* > While conformance checkers might signal an error or warning when a WAI-ARIA role is used on elements with strong native semantics, user agents MUST still use the value of the semantic of the WAI-ARIA role when exposing the element to accessibility APIs.
|
|
83
|
+
*/
|
|
84
|
+
if (!permittedRoles.some(r => r.name === roleName)) {
|
|
85
|
+
if (isNeverRendered(el, namespaceURI)) {
|
|
86
|
+
error = 'NO_PERMITTED';
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
el,
|
|
91
|
+
role: {
|
|
92
|
+
...spec,
|
|
93
|
+
isImplicit: false,
|
|
94
|
+
},
|
|
95
|
+
errorType: 'NO_PERMITTED',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
69
98
|
/**
|
|
70
99
|
* Otherwise
|
|
71
100
|
*/
|
|
@@ -83,6 +112,43 @@ el, version) {
|
|
|
83
112
|
errorType: error,
|
|
84
113
|
};
|
|
85
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* HTML elements that the user agent style sheet never renders (`display: none`),
|
|
117
|
+
* together with `input[type=hidden]` from the same rules (`isNeverRendered`).
|
|
118
|
+
* `area` is left out: it is rendered through its image map. The rules that
|
|
119
|
+
* depend on state (`[hidden]`, `noscript` with scripting) are left out too: the
|
|
120
|
+
* role stays on such an element, as its `hidden` attribute is a matter of the
|
|
121
|
+
* accessible name computation (Step 2A), not of the role.
|
|
122
|
+
*
|
|
123
|
+
* @see https://html.spec.whatwg.org/multipage/rendering.html#hidden-elements
|
|
124
|
+
*/
|
|
125
|
+
const NEVER_RENDERED_HTML_ELEMENTS = new Set([
|
|
126
|
+
'base',
|
|
127
|
+
'basefont',
|
|
128
|
+
'datalist',
|
|
129
|
+
'head',
|
|
130
|
+
'link',
|
|
131
|
+
'meta',
|
|
132
|
+
'noembed',
|
|
133
|
+
'noframes',
|
|
134
|
+
'param',
|
|
135
|
+
'rp',
|
|
136
|
+
'script',
|
|
137
|
+
'style',
|
|
138
|
+
'template',
|
|
139
|
+
'title',
|
|
140
|
+
]);
|
|
141
|
+
function isNeverRendered(
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
143
|
+
el, namespaceURI) {
|
|
144
|
+
if (namespaceURI !== XHTML_NAMESPACE) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (NEVER_RENDERED_HTML_ELEMENTS.has(el.localName)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return el.localName === 'input' && el.getAttribute('type')?.trim().toLowerCase() === 'hidden';
|
|
151
|
+
}
|
|
86
152
|
function isLandmarkRole(role) {
|
|
87
153
|
return role?.superClassRoles.some(su => su.name === 'landmark');
|
|
88
154
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ARIAVersion } from '../../types/index.js';
|
|
1
|
+
import type { ARIAVersion, ComputedRole } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Determines whether a given role name corresponds to a presentational role
|
|
4
4
|
* (`"presentation"` or `"none"`), which indicates the element should be
|
|
@@ -8,6 +8,18 @@ import type { ARIAVersion } from '../../types/index.js';
|
|
|
8
8
|
* @returns `true` if the role name is `"presentation"` or `"none"`, `false` otherwise
|
|
9
9
|
*/
|
|
10
10
|
export declare function isPresentational(roleName?: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Determines whether an element is exposed with the `none` or `presentation`
|
|
13
|
+
* role that its author specified, after the Presentational Roles Conflict
|
|
14
|
+
* Resolution. The implicit `presentation` role of `<img alt="">` does not count:
|
|
15
|
+
* HTML-AAM gives that image no name by its own rule.
|
|
16
|
+
*
|
|
17
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
18
|
+
*
|
|
19
|
+
* @param computed - The computed role of the element
|
|
20
|
+
* @returns `true` if the computed role is an explicit presentational role
|
|
21
|
+
*/
|
|
22
|
+
export declare function isExposedAsPresentational(computed: ComputedRole): boolean;
|
|
11
23
|
/**
|
|
12
24
|
* Determines whether a given role is transparent for ownership traversal.
|
|
13
25
|
*
|
|
@@ -12,6 +12,20 @@ export function isPresentational(roleName) {
|
|
|
12
12
|
}
|
|
13
13
|
return ['presentation', 'none'].includes(roleName);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Determines whether an element is exposed with the `none` or `presentation`
|
|
17
|
+
* role that its author specified, after the Presentational Roles Conflict
|
|
18
|
+
* Resolution. The implicit `presentation` role of `<img alt="">` does not count:
|
|
19
|
+
* HTML-AAM gives that image no name by its own rule.
|
|
20
|
+
*
|
|
21
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
22
|
+
*
|
|
23
|
+
* @param computed - The computed role of the element
|
|
24
|
+
* @returns `true` if the computed role is an explicit presentational role
|
|
25
|
+
*/
|
|
26
|
+
export function isExposedAsPresentational(computed) {
|
|
27
|
+
return !!computed.role && !computed.role.isImplicit && isPresentational(computed.role.name);
|
|
28
|
+
}
|
|
15
29
|
/**
|
|
16
30
|
* Determines whether a given role is transparent for ownership traversal.
|
|
17
31
|
*
|
package/lib/types/index.d.ts
CHANGED
|
@@ -329,6 +329,10 @@ export type Matches = (selector: string) => boolean;
|
|
|
329
329
|
* The result of computing an element's ARIA role, containing the element reference,
|
|
330
330
|
* the resolved role specification (or null if no role applies), and an optional
|
|
331
331
|
* error type indicating issues during role computation.
|
|
332
|
+
*
|
|
333
|
+
* An error type does not mean the role was dropped: with `NO_PERMITTED` (a role
|
|
334
|
+
* ARIA in HTML does not permit on the element) the `role` is the one the author
|
|
335
|
+
* specified, as user agents still expose it.
|
|
332
336
|
*/
|
|
333
337
|
export type ComputedRole = {
|
|
334
338
|
readonly el: Element;
|
|
@@ -29,7 +29,7 @@ compiledPatterns) {
|
|
|
29
29
|
}
|
|
30
30
|
let potentialName;
|
|
31
31
|
if (pattern.potentialName != null) {
|
|
32
|
-
potentialName = pattern.potentialName.replaceAll(/\$(\d+)/g, (_, idx) =>
|
|
32
|
+
potentialName = pattern.potentialName.replaceAll(/\$(\d+)/g, (_, idx) => match[Number(idx)] ?? '');
|
|
33
33
|
}
|
|
34
34
|
let isDuplicatable;
|
|
35
35
|
if (pattern.isDuplicatable === true) {
|
|
@@ -2,7 +2,8 @@ import type { ElementSpec } from '../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Looks up an element specification by its local tag name and namespace. The tag name
|
|
4
4
|
* is resolved to a namespace-qualified form (e.g., `"svg:circle"`) before searching.
|
|
5
|
-
* Results are cached by the namespace-qualified name for subsequent lookups
|
|
5
|
+
* Results are cached by the namespace-qualified name for subsequent lookups in the same
|
|
6
|
+
* list of specs.
|
|
6
7
|
*
|
|
7
8
|
* @template K - The keys of `ElementSpec` to include in the returned spec object (defaults to all keys)
|
|
8
9
|
* @param specs - The array of element specifications to search
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { resolveNamespace } from './resolve-namespace.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The results, for each list of specs it was searched in. A list is not told what another has:
|
|
4
|
+
* `permitted-contents` searches the specs of the document plus the `tag` rules of its option, which
|
|
5
|
+
* a lookup made in the specs of the document alone must not hide (nor the other way round). A list
|
|
6
|
+
* that nothing refers to any more takes its results with it.
|
|
7
|
+
*/
|
|
8
|
+
const caches = new WeakMap();
|
|
3
9
|
/**
|
|
4
10
|
* Looks up an element specification by its local tag name and namespace. The tag name
|
|
5
11
|
* is resolved to a namespace-qualified form (e.g., `"svg:circle"`) before searching.
|
|
6
|
-
* Results are cached by the namespace-qualified name for subsequent lookups
|
|
12
|
+
* Results are cached by the namespace-qualified name for subsequent lookups in the same
|
|
13
|
+
* list of specs.
|
|
7
14
|
*
|
|
8
15
|
* @template K - The keys of `ElementSpec` to include in the returned spec object (defaults to all keys)
|
|
9
16
|
* @param specs - The array of element specifications to search
|
|
@@ -13,6 +20,11 @@ const cache = new Map();
|
|
|
13
20
|
*/
|
|
14
21
|
export function getSpecByTagName(specs, localName, namespace) {
|
|
15
22
|
const { localNameWithNS } = resolveNamespace(localName, namespace ?? undefined);
|
|
23
|
+
let cache = caches.get(specs);
|
|
24
|
+
if (!cache) {
|
|
25
|
+
cache = new Map();
|
|
26
|
+
caches.set(specs, cache);
|
|
27
|
+
}
|
|
16
28
|
let spec = cache.get(localNameWithNS);
|
|
17
29
|
if (spec !== undefined) {
|
|
18
30
|
return spec;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/ml-spec",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Types and schema that specs of the Markup languages for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"schema:aria": "npx json2ts ./schemas/aria.schema.json --cwd ./schemas > ./src/types/aria.ts"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@markuplint/ml-ast": "5.
|
|
40
|
-
"@markuplint/types": "5.
|
|
39
|
+
"@markuplint/ml-ast": "5.3.0",
|
|
40
|
+
"@markuplint/types": "5.4.0",
|
|
41
41
|
"is-plain-object": "^5.1.0",
|
|
42
42
|
"type-fest": "^5.10.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@markuplint/test-tools": "5.
|
|
46
|
-
"json-schema-to-typescript": "
|
|
45
|
+
"@markuplint/test-tools": "5.4.0",
|
|
46
|
+
"json-schema-to-typescript": "16.0.0"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "ab56aa8e12b67fbf793ddf1267aaba9fda90269d"
|
|
49
49
|
}
|