@surea11y/core 1.5.0 → 1.7.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 +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- package/src/checks/automatic/contrast-computable.js +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +23 -4
- package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +563 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +4 -4
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/bin/surea11y-core.js +0 -20
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Container roles that require specific owned elements must contain at least one
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
|
-
* @sc
|
|
10
|
+
* @sc 1.3.1
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to elements with an explicit, valid, non-abstract role that is
|
|
13
13
|
* also one of the container roles with a documented "required owned
|
|
@@ -15,9 +15,19 @@
|
|
|
15
15
|
* table, grid, treegrid, tablist, tree, row).
|
|
16
16
|
* @expectation
|
|
17
17
|
* At least one descendant, or one aria-owns-referenced element, has one
|
|
18
|
-
* of the acceptable owned roles for that container role.
|
|
18
|
+
* of the acceptable owned roles for that container role. Reported at
|
|
19
|
+
* CANTTELL, never FAIL: this rule asks only whether the required content
|
|
20
|
+
* is PRESENT, and a container that owns nothing conveys nothing false --
|
|
21
|
+
* an empty role="list" is announced as a list with no items, which is what
|
|
22
|
+
* it is. Whether the content a container does own is VALID is
|
|
23
|
+
* aria-prohibited-children's decision, and that rule still fails, so a
|
|
24
|
+
* genuinely misdescribed structure (a role="button" among list items, a
|
|
25
|
+
* tablist of plain buttons) is caught with the same strength as before.
|
|
26
|
+
* The native-HTML equivalents already work this way: nothing in this
|
|
27
|
+
* ruleset fails an empty <ul>, and list-children-valid judges only the
|
|
28
|
+
* children that exist.
|
|
19
29
|
* @implementation-notes
|
|
20
|
-
* -
|
|
30
|
+
* - Scoped to REQUIRED_OWNED_ROLES in src/core/aria-helpers.js
|
|
21
31
|
* (see that file's header for the conservative-scope rationale).
|
|
22
32
|
* - Owned-role matching uses ariaHelpers.getContainmentRole, which combines
|
|
23
33
|
* explicit role="" attributes with a small, curated native-HTML-tag
|
|
@@ -28,12 +38,20 @@
|
|
|
28
38
|
* - Only one qualifying descendant/owned element is required (per
|
|
29
39
|
* WAI-ARIA "required owned elements": any one acceptable role satisfies
|
|
30
40
|
* the requirement); the full subtree is scanned without excluding nested
|
|
31
|
-
* containers with their own differing role, favoring simplicity
|
|
41
|
+
* containers with their own differing role, favoring simplicity, this
|
|
32
42
|
* can only under-report (recall), never over-report (fail integrity).
|
|
43
|
+
* - "At least one required child exists" is the whole of this rule's
|
|
44
|
+
* decision. Whether every owned child is ALLOWED is
|
|
45
|
+
* aria-prohibited-children's, and that rule does walk the owned graph
|
|
46
|
+
* exclusively, with group/rowgroup transparency. ACT bc4a75 asks both
|
|
47
|
+
* questions at once, which is why the mapping lists the two rules as a
|
|
48
|
+
* family: read on its own, this rule looks like it under-reports a
|
|
49
|
+
* container mixing valid and invalid children, and the sibling is what
|
|
50
|
+
* catches it.
|
|
33
51
|
* - Gated on isAccTreeEligible for the container itself: unlike this
|
|
34
52
|
* file's sibling attribute/role-validity checks (e.g. aria-roles-valid),
|
|
35
53
|
* "does this container currently have a required child" is not a fact
|
|
36
|
-
* that stays fixed once written
|
|
54
|
+
* that stays fixed once written, it is routinely filled in by the same
|
|
37
55
|
* script/interaction that reveals the container (a closed flyout menu
|
|
38
56
|
* or <dialog> populated on open). Flagging it while the container isn't
|
|
39
57
|
* currently exposed to the accessibility tree is a false positive; such
|
|
@@ -44,16 +62,16 @@
|
|
|
44
62
|
* widget is missing required owned elements due to script execution or
|
|
45
63
|
* loading, authors MUST mark a containing element with aria-busy equal
|
|
46
64
|
* to true." A container carrying aria-busy="true" is skipped the same
|
|
47
|
-
* way
|
|
65
|
+
* way, only the exact string "true" counts (absent/"false" do not).
|
|
48
66
|
* - Descendant search tries a fast native querySelectorAll(CANDIDATE_
|
|
49
67
|
* SELECTOR) first (covers the light-DOM-only common case with no added
|
|
50
68
|
* cost); only when that finds nothing AND the container has a <slot>
|
|
51
69
|
* anywhere in its subtree does it fall back to a composed-tree walk that
|
|
52
|
-
* expands <slot> elements via assignedElements({flatten:true})
|
|
70
|
+
* expands <slot> elements via assignedElements({flatten:true}). Plain
|
|
53
71
|
* querySelectorAll only sees a <slot>'s unrendered fallback content, never
|
|
54
|
-
* what's actually distributed into it.
|
|
72
|
+
* what's actually distributed into it. Scoped to slot
|
|
55
73
|
* expansion only, not a general "also descend into any nested custom
|
|
56
|
-
* element's own shadow root" walk
|
|
74
|
+
* element's own shadow root" walk, no known case needs that yet.
|
|
57
75
|
*/
|
|
58
76
|
|
|
59
77
|
const id = 'aria-required-children';
|
|
@@ -67,14 +85,14 @@ const meta = {
|
|
|
67
85
|
descriptionKey: 'ariaRequiredChildren_description'
|
|
68
86
|
},
|
|
69
87
|
helpUrl: null,
|
|
70
|
-
tags: ['wcag2a', '
|
|
71
|
-
wcagSc: ['
|
|
88
|
+
tags: ['wcag2a', 'wcag131', 'aria', 'structure', 'atomic', 'automatic'],
|
|
89
|
+
wcagSc: ['1.3.1'],
|
|
72
90
|
normativeMappings: [
|
|
73
91
|
{
|
|
74
92
|
standard: 'WCAG',
|
|
75
93
|
version: '2.2',
|
|
76
|
-
requirement: '
|
|
77
|
-
title: '
|
|
94
|
+
requirement: '1.3.1',
|
|
95
|
+
title: 'Info and Relationships',
|
|
78
96
|
conformanceLevel: 'A'
|
|
79
97
|
}
|
|
80
98
|
],
|
|
@@ -82,7 +100,7 @@ const meta = {
|
|
|
82
100
|
category: 'robust',
|
|
83
101
|
type: 'automatic',
|
|
84
102
|
defaultConfidence: 'medium',
|
|
85
|
-
coverage: { facetsBySc: { '
|
|
103
|
+
coverage: { facetsBySc: { '1.3.1': ['aria-role-required-owned-children'] } }
|
|
86
104
|
};
|
|
87
105
|
|
|
88
106
|
function runInPage(ctx) {
|
|
@@ -114,7 +132,7 @@ function runInPage(ctx) {
|
|
|
114
132
|
// Candidate selector for descendant scanning: explicit role attributes,
|
|
115
133
|
// plus every native tag ariaHelpers.getContainmentRole() recognizes
|
|
116
134
|
// (kept in sync with aria-helpers.js NATIVE_CONTAINMENT_ROLE_BY_ELEMENT).
|
|
117
|
-
// Declared inside runInPage
|
|
135
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
118
136
|
// ("runInPage MUST be self-contained").
|
|
119
137
|
const CANDIDATE_SELECTOR =
|
|
120
138
|
'[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
|
|
@@ -130,11 +148,11 @@ function runInPage(ctx) {
|
|
|
130
148
|
// light-DOM subtree, so a container whose real owned children are
|
|
131
149
|
// distributed via <slot> (e.g. a shadow-DOM role="list" wrapping
|
|
132
150
|
// <slot></slot>, with the actual role="listitem" elements living in the
|
|
133
|
-
// light DOM and projected in) would never find them there
|
|
151
|
+
// light DOM and projected in) would never find them there, same class
|
|
134
152
|
// of bug as aria-required-parent's ancestor search, just in the opposite
|
|
135
153
|
// (descendant) direction.
|
|
136
154
|
//
|
|
137
|
-
//
|
|
155
|
+
// Scoped to slot expansion only, does NOT separately
|
|
138
156
|
// descend into an unrelated nested custom element's own shadow root
|
|
139
157
|
// (e.g. a <my-widget> child with no <slot> involvement at all). That's a
|
|
140
158
|
// qualitatively different question (does an arbitrary component's own
|
|
@@ -192,9 +210,8 @@ function runInPage(ctx) {
|
|
|
192
210
|
let found = false;
|
|
193
211
|
|
|
194
212
|
// Fast path first: native querySelectorAll over the curated candidate
|
|
195
|
-
// selector
|
|
196
|
-
//
|
|
197
|
-
// added cost.
|
|
213
|
+
// selector. Covers the overwhelming majority of containers (no shadow
|
|
214
|
+
// DOM involved at all) with zero added cost.
|
|
198
215
|
let descendants;
|
|
199
216
|
try {
|
|
200
217
|
descendants = el.querySelectorAll(CANDIDATE_SELECTOR);
|
|
@@ -210,7 +227,7 @@ function runInPage(ctx) {
|
|
|
210
227
|
}
|
|
211
228
|
|
|
212
229
|
// Slow path only when the fast path found nothing AND there's an actual
|
|
213
|
-
// <slot> somewhere in the subtree to expand
|
|
230
|
+
// <slot> somewhere in the subtree to expand, bounds the extra cost to
|
|
214
231
|
// exactly the containers that could possibly need it.
|
|
215
232
|
if (!found) {
|
|
216
233
|
let hasSlot;
|
|
@@ -262,6 +279,16 @@ function runInPage(ctx) {
|
|
|
262
279
|
hintKey: 'ariaRequiredChildren_hint_fail',
|
|
263
280
|
params: { role, requiredRoles: requiredOwned.join(', ') }
|
|
264
281
|
},
|
|
282
|
+
uncertainty: {
|
|
283
|
+
code: 'spec-only',
|
|
284
|
+
needed:
|
|
285
|
+
'Whether this container is legitimately empty, or holds items that never got their role.',
|
|
286
|
+
evidence: {
|
|
287
|
+
role,
|
|
288
|
+
requiredOwnedRoles: requiredOwned,
|
|
289
|
+
childElementCount: el.children ? el.children.length : null
|
|
290
|
+
}
|
|
291
|
+
},
|
|
265
292
|
data: {
|
|
266
293
|
details: {
|
|
267
294
|
reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
|
|
@@ -276,15 +303,12 @@ function runInPage(ctx) {
|
|
|
276
303
|
if (applicableCount === 0) {
|
|
277
304
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
278
305
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
306
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
307
|
+
[],
|
|
308
|
+
occurrences,
|
|
309
|
+
rule.defaultSeverity || 'moderate'
|
|
310
|
+
);
|
|
311
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
288
312
|
}
|
|
289
313
|
|
|
290
314
|
module.exports = { id, meta, runInPage };
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Roles that require a specific ancestor/owner context role must have one
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
|
-
* @sc
|
|
10
|
+
* @sc 1.3.1
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to elements with an explicit, valid, non-abstract role that is
|
|
13
13
|
* also one of the roles with a documented, non-empty "required context
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ancestor/owner's aria-owns) whose effective role is one of the
|
|
20
20
|
* acceptable context roles for this element's role.
|
|
21
21
|
* @implementation-notes
|
|
22
|
-
* -
|
|
22
|
+
* - Scoped to REQUIRED_CONTEXT_ROLE in src/core/aria-helpers.js
|
|
23
23
|
* (see that file's header for the conservative-scope rationale); roles
|
|
24
24
|
* with an explicitly empty entry (e.g. tabpanel) are left unconstrained.
|
|
25
25
|
* - Context-role matching uses ariaHelpers.getContainmentRole, which
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* independent path via a reverse lookup over the search root.
|
|
35
35
|
* - Gated on isAccTreeEligible for the element itself. The ancestor-role
|
|
36
36
|
* walk itself doesn't care about visibility (a hidden ancestor's role is
|
|
37
|
-
* still found by plain DOM/composed-tree containment, so a
|
|
38
|
-
* correctly-nested-but-hidden widget was never at risk here)
|
|
37
|
+
* still found by plain DOM/composed-tree containment, so a
|
|
38
|
+
* correctly-nested-but-hidden widget was never at risk here), the
|
|
39
39
|
* remaining false-positive shape is an element whose required ancestor
|
|
40
40
|
* context doesn't exist YET because it (and its wrapping context) are
|
|
41
41
|
* assembled together at reveal time (e.g. a portal-rendered item staged
|
|
@@ -43,6 +43,16 @@
|
|
|
43
43
|
* aria-required-children/aria-prohibited-children, applied for
|
|
44
44
|
* consistency; an element that isn't currently exposed to the
|
|
45
45
|
* accessibility tree is skipped (notApplicable), not failed.
|
|
46
|
+
* - Also honors WAI-ARIA's escape hatch for a widget script has not
|
|
47
|
+
* finished assembling: "When a widget is missing required owned elements due to
|
|
48
|
+
* script execution or loading, authors MUST mark a containing element
|
|
49
|
+
* with aria-busy equal to true." aria-required-children reads that from
|
|
50
|
+
* the container it is checking; read from the item's side it is an
|
|
51
|
+
* ancestor, so the walk looks up rather than at the element itself, and
|
|
52
|
+
* only the exact string "true" counts. It also outranks the
|
|
53
|
+
* roleless-generic-parent rule below, since aria-busy is itself a global
|
|
54
|
+
* ARIA attribute and would otherwise block the context search and fail
|
|
55
|
+
* the very markup the spec says to mark.
|
|
46
56
|
*/
|
|
47
57
|
|
|
48
58
|
const id = 'aria-required-parent';
|
|
@@ -56,14 +66,14 @@ const meta = {
|
|
|
56
66
|
descriptionKey: 'ariaRequiredParent_description'
|
|
57
67
|
},
|
|
58
68
|
helpUrl: null,
|
|
59
|
-
tags: ['wcag2a', '
|
|
60
|
-
wcagSc: ['
|
|
69
|
+
tags: ['wcag2a', 'wcag131', 'aria', 'structure', 'atomic', 'automatic'],
|
|
70
|
+
wcagSc: ['1.3.1'],
|
|
61
71
|
normativeMappings: [
|
|
62
72
|
{
|
|
63
73
|
standard: 'WCAG',
|
|
64
74
|
version: '2.2',
|
|
65
|
-
requirement: '
|
|
66
|
-
title: '
|
|
75
|
+
requirement: '1.3.1',
|
|
76
|
+
title: 'Info and Relationships',
|
|
67
77
|
conformanceLevel: 'A'
|
|
68
78
|
}
|
|
69
79
|
],
|
|
@@ -71,7 +81,7 @@ const meta = {
|
|
|
71
81
|
category: 'robust',
|
|
72
82
|
type: 'automatic',
|
|
73
83
|
defaultConfidence: 'medium',
|
|
74
|
-
coverage: { facetsBySc: { '
|
|
84
|
+
coverage: { facetsBySc: { '1.3.1': ['aria-role-required-context-parent'] } }
|
|
75
85
|
};
|
|
76
86
|
|
|
77
87
|
function runInPage(ctx) {
|
|
@@ -96,13 +106,57 @@ function runInPage(ctx) {
|
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
// Roles that may host a nested listitem/treeitem group without breaking
|
|
99
|
-
// the required-context chain
|
|
109
|
+
// the required-context chain, the group role is transparent for exactly
|
|
100
110
|
// these two roles.
|
|
101
111
|
const GROUP_TRANSPARENT_FOR_ROLES = new Set(['listitem', 'treeitem']);
|
|
102
112
|
|
|
103
|
-
//
|
|
113
|
+
// The WAI-ARIA "Global States and Properties" set (same list as
|
|
114
|
+
// aria-prohibited-children.js's GLOBAL_ARIA_ATTRS, duplicated, not
|
|
115
|
+
// imported, since runInPage must be self-contained per
|
|
116
|
+
// scripts/build-core.js). A roleless ancestor carrying any of these is
|
|
117
|
+
// still "included in the accessibility tree" and is therefore a real
|
|
118
|
+
// (generic) parent, not a transparent one: ACT's ff89c9 test corpus
|
|
119
|
+
// covers exactly this with role="listitem" whose actual DOM parent is a
|
|
120
|
+
// roleless <div aria-live="polite">, itself inside a role="list", the
|
|
121
|
+
// required-context chain is broken by that included-but-roleless div,
|
|
122
|
+
// even though a role="list" ancestor does exist further up.
|
|
123
|
+
const GLOBAL_ARIA_ATTRS = [
|
|
124
|
+
'aria-atomic',
|
|
125
|
+
'aria-braillelabel',
|
|
126
|
+
'aria-brailleroledescription',
|
|
127
|
+
'aria-busy',
|
|
128
|
+
'aria-controls',
|
|
129
|
+
'aria-current',
|
|
130
|
+
'aria-describedby',
|
|
131
|
+
'aria-description',
|
|
132
|
+
'aria-details',
|
|
133
|
+
'aria-disabled',
|
|
134
|
+
'aria-dropeffect',
|
|
135
|
+
'aria-errormessage',
|
|
136
|
+
'aria-flowto',
|
|
137
|
+
'aria-grabbed',
|
|
138
|
+
'aria-haspopup',
|
|
139
|
+
'aria-hidden',
|
|
140
|
+
'aria-invalid',
|
|
141
|
+
'aria-keyshortcuts',
|
|
142
|
+
'aria-label',
|
|
143
|
+
'aria-labelledby',
|
|
144
|
+
'aria-live',
|
|
145
|
+
'aria-owns',
|
|
146
|
+
'aria-relevant',
|
|
147
|
+
'aria-roledescription'
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
function hasGlobalAriaAttr(el) {
|
|
151
|
+
for (const attr of GLOBAL_ARIA_ATTRS) {
|
|
152
|
+
if (el.getAttribute && el.getAttribute(attr) != null) return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A real ancestor role, not "no role at all" and not the two roles that
|
|
104
158
|
// strip an element from the accessibility tree's parent/child chain
|
|
105
|
-
// entirely (presentation/none)
|
|
159
|
+
// entirely (presentation/none), stops the search. This is stricter than
|
|
106
160
|
// "any ancestor with the right role anywhere up the tree": the
|
|
107
161
|
// required-context relationship is about the accessibility tree's actual
|
|
108
162
|
// PARENT, so an intervening ancestor with its OWN distinct real role
|
|
@@ -110,19 +164,27 @@ function runInPage(ctx) {
|
|
|
110
164
|
// a further-up ancestor has the correct role. E.g. a <button role="tab">
|
|
111
165
|
// inside a plain <li> (native listitem) inside <ul role="tablist"> fails:
|
|
112
166
|
// the tablist is never the tab's accessible-tree parent, the listitem is.
|
|
167
|
+
//
|
|
168
|
+
// A roleless ancestor is normally transparent (it isn't a node in the
|
|
169
|
+
// accessibility tree at all), UNLESS it carries a global ARIA attribute
|
|
170
|
+
// -- that alone includes it in the tree as a real, roleless (generic)
|
|
171
|
+
// parent, which still blocks the search the same way a distinct real
|
|
172
|
+
// role would.
|
|
113
173
|
function getRealContextRole(el) {
|
|
114
174
|
const role = ariaHelpers.getContainmentRole(el);
|
|
115
|
-
if (!role || role === 'presentation' || role === 'none')
|
|
175
|
+
if (!role || role === 'presentation' || role === 'none') {
|
|
176
|
+
return hasGlobalAriaAttr(el) ? 'generic' : '';
|
|
177
|
+
}
|
|
116
178
|
return role;
|
|
117
179
|
}
|
|
118
180
|
|
|
119
|
-
// Flat-tree ancestor walk (ctx.helpers.composedParent
|
|
181
|
+
// Flat-tree ancestor walk (ctx.helpers.composedParent, assignedSlot wins
|
|
120
182
|
// over parentNode, then shadow host). A slotted light-DOM element's real
|
|
121
183
|
// rendered ancestor is whatever the shadow tree wraps its <slot> in (e.g.
|
|
122
184
|
// a role="list" container), not its own light-DOM parentElement.
|
|
123
185
|
// composedParent can return a non-Element node (a ShadowRoot, nodeType
|
|
124
186
|
// 11) when climbing out of a shadow tree that has no further light-DOM
|
|
125
|
-
// parent
|
|
187
|
+
// parent, skip those and keep climbing rather than treating them as a
|
|
126
188
|
// (roleless) context.
|
|
127
189
|
const getComposedParent =
|
|
128
190
|
helpers && typeof helpers.composedParent === 'function'
|
|
@@ -131,6 +193,21 @@ function runInPage(ctx) {
|
|
|
131
193
|
return n && n.parentElement ? n.parentElement : null;
|
|
132
194
|
};
|
|
133
195
|
|
|
196
|
+
// The escape hatch marks the container being assembled, not the item inside
|
|
197
|
+
// it, so this walks up instead of reading the element's own attribute.
|
|
198
|
+
function hasBusyAncestor(el) {
|
|
199
|
+
let cur = getComposedParent(el);
|
|
200
|
+
let guard = 0;
|
|
201
|
+
while (cur && guard++ < 200) {
|
|
202
|
+
if (cur.nodeType === 1 && cur.getAttribute) {
|
|
203
|
+
const v = cur.getAttribute('aria-busy');
|
|
204
|
+
if (v != null && String(v).trim().toLowerCase() === 'true') return true;
|
|
205
|
+
}
|
|
206
|
+
cur = getComposedParent(cur);
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
134
211
|
function hasAcceptableAncestorContext(el, acceptableRoles, ownRole) {
|
|
135
212
|
const allowsGroup = acceptableRoles.has('group');
|
|
136
213
|
let cur = getComposedParent(el);
|
|
@@ -200,6 +277,7 @@ function runInPage(ctx) {
|
|
|
200
277
|
if (!requiredContext || !requiredContext.length) continue; // no entry, or explicitly unconstrained
|
|
201
278
|
|
|
202
279
|
if (!isEligibleAcc(el)) continue; // not currently exposed to the accessibility tree
|
|
280
|
+
if (hasBusyAncestor(el)) continue; // author has signaled transient incompleteness per WAI-ARIA
|
|
203
281
|
|
|
204
282
|
applicableCount += 1;
|
|
205
283
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Generic name-presence rule for
|
|
6
|
+
* Generic name-presence rule for the ARIA roles WAI-ARIA *requires* an
|
|
7
|
+
* accessible name for.
|
|
7
8
|
*
|
|
8
9
|
* Notes:
|
|
9
10
|
* - This rule intentionally focuses on author-provided naming mechanisms:
|
|
@@ -11,20 +12,40 @@
|
|
|
11
12
|
* - It does NOT treat descendant text content as a valid name source for these roles
|
|
12
13
|
* (to avoid false passes from labelled children inside composite widgets).
|
|
13
14
|
* - Eligibility is based on helpers.isAccTreeEligible(node, ctx) per engine checks.
|
|
15
|
+
* - The role set is generated from aria-query by
|
|
16
|
+
* scripts/generate-aria-tables.js; see that script for why each role is in
|
|
17
|
+
* or out, and for the name-required roles no rule covers yet.
|
|
18
|
+
*
|
|
19
|
+
* @applicability
|
|
20
|
+
* Applies to elements whose role attribute is exactly one of grid, meter,
|
|
21
|
+
* progressbar, radiogroup or tree, and that are included in the
|
|
22
|
+
* accessibility tree. Membership is decided by WAI-ARIA's own "Accessible
|
|
23
|
+
* Name Required: True" characteristic, not by whether a role merely permits
|
|
24
|
+
* a name: tablist, toolbar, menu, menubar and scrollbar are name-from-author
|
|
25
|
+
* roles the spec does not require a name for, and are out of
|
|
26
|
+
* scope. meter and progressbar are also covered by meter-name-present and
|
|
27
|
+
* progressbar-name-present, which map to SC 1.1.1; this rule is what gives
|
|
28
|
+
* those two roles their 4.1.2 coverage.
|
|
29
|
+
* @expectation
|
|
30
|
+
* The element has a non-empty aria-label, an aria-labelledby that resolves
|
|
31
|
+
* to non-empty text, or a non-empty title. Every role in the set is
|
|
32
|
+
* name-from-author-only, so descendant text is not accepted:
|
|
33
|
+
* a labelled child inside a composite widget would otherwise pass the
|
|
34
|
+
* container that has no name of its own.
|
|
14
35
|
*/
|
|
15
36
|
|
|
16
37
|
const id = 'aria-role-name-present';
|
|
17
38
|
|
|
18
39
|
const meta = {
|
|
19
|
-
title: 'ARIA
|
|
40
|
+
title: 'ARIA roles that require an accessible name have one',
|
|
20
41
|
description:
|
|
21
|
-
'Checks that
|
|
42
|
+
'Checks that the ARIA roles WAI-ARIA requires an accessible name for expose a non-empty one.',
|
|
22
43
|
i18n: {
|
|
23
44
|
titleKey: 'ariaRoleNamePresent_title',
|
|
24
45
|
descriptionKey: 'ariaRoleNamePresent_description'
|
|
25
46
|
},
|
|
26
47
|
helpUrl: null,
|
|
27
|
-
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
|
|
48
|
+
tags: ['wcag2a', 'wcag412', 'aria', 'navigation', 'atomic', 'automatic', 'name'],
|
|
28
49
|
wcagSc: ['4.1.2'],
|
|
29
50
|
normativeMappings: [
|
|
30
51
|
{
|
|
@@ -93,7 +114,7 @@ function runInPage(ctx) {
|
|
|
93
114
|
// button-name-present et al.), which correctly includes hidden/aria-hidden
|
|
94
115
|
// referenced nodes per the Accessible Name and Description Computation
|
|
95
116
|
// spec (a hidden node directly referenced by aria-labelledby still
|
|
96
|
-
// supplies its text
|
|
117
|
+
// supplies its text, this is a standard visually-hidden-label pattern).
|
|
97
118
|
const resolveLabelledby = (el, maxRefs) => {
|
|
98
119
|
if (!getAriaLabelledByInfo) return '';
|
|
99
120
|
try {
|
|
@@ -115,23 +136,16 @@ function runInPage(ctx) {
|
|
|
115
136
|
}
|
|
116
137
|
};
|
|
117
138
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'menubar',
|
|
129
|
-
'meter',
|
|
130
|
-
'progressbar'
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
const selector =
|
|
134
|
-
'[role="scrollbar"],[role="toolbar"],[role="tablist"],[role="radiogroup"],[role="tree"],[role="grid"],[role="menu"],[role="menubar"],[role="meter"],[role="progressbar"]';
|
|
139
|
+
// Roles WAI-ARIA marks "Accessible Name Required: True" and names from the
|
|
140
|
+
// author only. Generated from aria-query by scripts/generate-aria-tables.js:
|
|
141
|
+
// a role that merely *allows* an author name (tablist, toolbar, menu,
|
|
142
|
+
// menubar, scrollbar) is not a 4.1.2 failure when unnamed and is not listed.
|
|
143
|
+
// <generated:aria-name-required-roles>
|
|
144
|
+
const NAME_REQUIRED_ROLES = new Set(['grid', 'meter', 'progressbar', 'radiogroup', 'tree']);
|
|
145
|
+
// </generated:aria-name-required-roles>
|
|
146
|
+
|
|
147
|
+
// Derived from the set above so the two cannot drift apart.
|
|
148
|
+
const selector = [...NAME_REQUIRED_ROLES].map((r) => `[role="${r}"]`).join(',');
|
|
135
149
|
|
|
136
150
|
const nodes = (() => {
|
|
137
151
|
try {
|
|
@@ -161,7 +175,7 @@ function runInPage(ctx) {
|
|
|
161
175
|
return '';
|
|
162
176
|
}
|
|
163
177
|
})();
|
|
164
|
-
if (!
|
|
178
|
+
if (!NAME_REQUIRED_ROLES.has(role)) continue;
|
|
165
179
|
|
|
166
180
|
if (!isEligible(el)) continue;
|
|
167
181
|
|
|
@@ -11,16 +11,15 @@
|
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to any element with a non-empty role="" attribute in the composed DOM.
|
|
13
13
|
* @expectation
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* element happens to be hidden right now.
|
|
14
|
+
* At least one role token names a concrete, non-abstract ARIA role.
|
|
15
|
+
* Graded by what the element falls back to when none does:
|
|
16
|
+
* - FAIL on a roleless host (div, span, custom element), which is left
|
|
17
|
+
* exposed as generic, so the role the author meant reaches no one.
|
|
18
|
+
* - CANTTELL where the element has a native role (a <button>, <nav>,
|
|
19
|
+
* <a href>), which the accessibility tree keeps using. ACT 674b10 lists
|
|
20
|
+
* 4.1.2 as a secondary requirement only, "satisfied through the implicit
|
|
21
|
+
* role," so the bad token is worth reporting but is not itself the
|
|
22
|
+
* criterion failing.
|
|
24
23
|
*/
|
|
25
24
|
|
|
26
25
|
const id = 'aria-roles-valid';
|
|
@@ -64,7 +63,8 @@ function runInPage(ctx) {
|
|
|
64
63
|
? helpers.queryAllSmart('[role]')
|
|
65
64
|
: helpers.queryAll('[role]');
|
|
66
65
|
|
|
67
|
-
const
|
|
66
|
+
const failOccurrences = [];
|
|
67
|
+
const cantTellOccurrences = [];
|
|
68
68
|
let applicableCount = 0;
|
|
69
69
|
|
|
70
70
|
// Programmatically hidden per the ACT glossary: display:none, visibility not
|
|
@@ -121,8 +121,42 @@ function runInPage(ctx) {
|
|
|
121
121
|
const isKnown = tokens.some((t) => ariaHelpers.isKnownRole(t));
|
|
122
122
|
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
// An unusable role token leaves the element on its native role, when it
|
|
125
|
+
// has one: ACT 674b10 lists 4.1.2 as only a secondary requirement for
|
|
126
|
+
// exactly that reason. A roleless host (div, span, custom element) has
|
|
127
|
+
// nothing to fall back to and is exposed as generic instead.
|
|
128
|
+
const nativeRole =
|
|
129
|
+
typeof ariaHelpers.getNativeRoleForElement === 'function'
|
|
130
|
+
? ariaHelpers.getNativeRoleForElement(el) || ''
|
|
131
|
+
: '';
|
|
132
|
+
|
|
133
|
+
if (nativeRole) {
|
|
134
|
+
cantTellOccurrences.push(
|
|
135
|
+
helpers.reportOccurrence(el, {
|
|
136
|
+
occurrenceOutcome: 'cantTell',
|
|
137
|
+
summary: `The role attribute value is not usable, so this element is still exposed as its native role="${nativeRole}".`,
|
|
138
|
+
hint: 'Fix or remove the role token; assistive technology is using the native role in the meantime.',
|
|
139
|
+
i18n: {
|
|
140
|
+
summaryKey: 'ariaRolesValid_summary_cantTell',
|
|
141
|
+
hintKey: 'ariaRolesValid_hint_cantTell',
|
|
142
|
+
params: { role, nativeRole }
|
|
143
|
+
},
|
|
144
|
+
uncertainty: {
|
|
145
|
+
code: 'spec-only',
|
|
146
|
+
needed: 'Whether the native role this element falls back to is the one intended.',
|
|
147
|
+
evidence: { role, nativeRole, fallbackExposed: true }
|
|
148
|
+
},
|
|
149
|
+
data: {
|
|
150
|
+
details: { reasonCode, role, nativeRole }
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
failOccurrences.push(
|
|
125
158
|
helpers.reportOccurrence(el, {
|
|
159
|
+
occurrenceOutcome: 'fail',
|
|
126
160
|
summary: !isKnown
|
|
127
161
|
? 'The role attribute value is not a recognized ARIA role.'
|
|
128
162
|
: 'The role attribute value is an abstract ARIA role, which must not be used directly.',
|
|
@@ -146,15 +180,12 @@ function runInPage(ctx) {
|
|
|
146
180
|
if (applicableCount === 0) {
|
|
147
181
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
148
182
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
183
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
184
|
+
failOccurrences,
|
|
185
|
+
cantTellOccurrences,
|
|
186
|
+
rule.defaultSeverity || 'serious'
|
|
187
|
+
);
|
|
188
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
158
189
|
}
|
|
159
190
|
|
|
160
191
|
module.exports = { id, meta, runInPage };
|