@surea11y/core 1.3.0 → 1.4.1
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 +87 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -0
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/CI_INTEGRATIONS.md +7 -7
- package/docs/ENGINE_OPTIONS.md +1 -1
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +6 -6
- package/package.json +52 -16
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +4 -6
- package/src/checks/automatic/aria-allowed-attr.js +663 -134
- package/src/checks/automatic/aria-allowed-role.js +2 -0
- package/src/checks/automatic/aria-braille-equivalent.js +2 -0
- package/src/checks/automatic/aria-conditional-attr.js +8 -7
- package/src/checks/automatic/aria-deprecated-role.js +107 -38
- package/src/checks/automatic/aria-hidden-body.js +6 -4
- package/src/checks/automatic/aria-hidden-focus.js +12 -13
- package/src/checks/automatic/aria-prohibited-attr.js +98 -105
- package/src/checks/automatic/aria-prohibited-children.js +56 -87
- package/src/checks/automatic/aria-required-attr.js +6 -7
- package/src/checks/automatic/aria-required-children.js +7 -10
- package/src/checks/automatic/aria-required-parent.js +20 -25
- package/src/checks/automatic/aria-role-name-present.js +2 -0
- package/src/checks/automatic/aria-roles-valid.js +33 -6
- package/src/checks/automatic/aria-valid-attr-value.js +18 -15
- package/src/checks/automatic/aria-valid-attr.js +2 -0
- package/src/checks/automatic/autocomplete-valid.js +39 -1
- package/src/checks/automatic/avoid-inline-spacing.js +181 -20
- package/src/checks/automatic/binary-control-name-present.js +13 -3
- package/src/checks/automatic/button-name-present.js +54 -21
- package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
- package/src/checks/automatic/combobox-name-present.js +10 -1
- package/src/checks/automatic/contrast-computable.js +2 -0
- package/src/checks/automatic/contrast-enhanced.js +2 -0
- package/src/checks/automatic/contrast-minimum.js +2 -0
- package/src/checks/automatic/css-orientation-lock.js +21 -27
- package/src/checks/automatic/definition-list-children-valid.js +6 -6
- package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
- package/src/checks/automatic/dialog-name-present.js +18 -11
- package/src/checks/automatic/dlitem-parent-valid.js +2 -0
- package/src/checks/automatic/duplicate-id-aria.js +4 -3
- package/src/checks/automatic/embed-text-alternative-present.js +2 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
- package/src/checks/automatic/form-control-single-label.js +110 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
- package/src/checks/automatic/iframe-focusable-content.js +246 -18
- package/src/checks/automatic/iframe-name-present.js +2 -0
- package/src/checks/automatic/iframe-title-unique.js +3 -1
- package/src/checks/automatic/img-alt-present.js +23 -18
- package/src/checks/automatic/input-image-alt-present.js +101 -52
- package/src/checks/automatic/label-in-name.js +97 -27
- package/src/checks/automatic/language-page-present.js +7 -1
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +52 -17
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +10 -1
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +10 -1
- package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
- package/src/checks/automatic/meter-name-present.js +12 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
- package/src/checks/automatic/object-text-alternative-present.js +16 -7
- package/src/checks/automatic/option-name-present.js +10 -1
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +16 -11
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +10 -1
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +13 -2
- package/src/checks/automatic/spinbutton-name-present.js +10 -1
- package/src/checks/automatic/summary-name-present.js +10 -1
- package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
- package/src/checks/automatic/svg-text-alternative-present.js +17 -5
- package/src/checks/automatic/tab-name-present.js +10 -1
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +69 -6
- package/src/checks/automatic/target-size-minimum.js +5 -0
- package/src/checks/automatic/td-has-header.js +24 -1
- package/src/checks/automatic/textbox-name-present.js +10 -1
- package/src/checks/automatic/tooltip-name-present.js +10 -1
- package/src/checks/automatic/treeitem-name-present.js +10 -1
- package/src/checks/automatic/valid-lang.js +18 -3
- package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
- package/src/checks/manual/accesskeys-manual.js +3 -1
- package/src/checks/manual/area-alt-decorative-manual.js +2 -0
- package/src/checks/manual/area-alt-quality-manual.js +2 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
- package/src/checks/manual/aria-text-manual.js +6 -5
- package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/css-hidden-focus.js +184 -9
- package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
- package/src/checks/manual/empty-heading-manual.js +17 -17
- package/src/checks/manual/empty-table-header-manual.js +52 -25
- package/src/checks/manual/focus-order-semantics-manual.js +16 -4
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
- package/src/checks/manual/heading-order-manual.js +28 -1
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
- package/src/checks/manual/image-redundant-alt-manual.js +21 -1
- package/src/checks/manual/img-alt-decorative-manual.js +2 -0
- package/src/checks/manual/img-alt-quality-manual.js +2 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
- package/src/checks/manual/label-title-only-manual.js +29 -22
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
- package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
- package/src/checks/manual/landmark-one-main-manual.js +12 -23
- package/src/checks/manual/landmark-unique-manual.js +37 -52
- package/src/checks/manual/link-name-quality-manual.js +2 -0
- package/src/checks/manual/media-transcript-present-manual.js +2 -0
- package/src/checks/manual/meta-viewport-large-manual.js +3 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/p-as-heading-manual.js +2 -0
- package/src/checks/manual/page-has-heading-one-manual.js +12 -11
- package/src/checks/manual/page-title-patterns-manual.js +2 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
- package/src/checks/manual/region-manual.js +27 -36
- package/src/checks/manual/scope-attr-valid-manual.js +3 -1
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
- package/src/checks/manual/skip-link-manual.js +7 -6
- package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/tabindex-manual.js +3 -1
- package/src/checks/manual/table-duplicate-name-manual.js +5 -4
- package/src/checks/manual/table-fake-caption-manual.js +24 -3
- package/src/checks/manual/video-caption-manual.js +2 -0
- package/src/checks/manual-review.js +2 -0
- package/src/core.js +11820 -3317
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +20 -5
- package/surea11y.browser.js +4943 -1388
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- package/src/checks/automatic/bypass-blocks-present.js +0 -215
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -1211
- package/src/core/contrast-helpers.js +0 -1302
- package/src/core/dom-helpers.js +0 -4493
- package/src/core/dom-runner.js +0 -787
- package/src/core/frame-messaging.js +0 -261
- package/src/core/frame-scan.js +0 -190
- package/src/core/rollup-composites.js +0 -127
- package/src/core/rule-meta.js +0 -176
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -1228
- package/src/i18n/fr.js +0 -1185
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -59
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -16,13 +18,12 @@
|
|
|
16
18
|
* `"false"` silently drops the error message from the accessibility
|
|
17
19
|
* tree — authors almost always intend it to be exposed.
|
|
18
20
|
* @implementation-notes
|
|
19
|
-
* - This is deliberately narrow: a
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* for the same "narrower than that reference engine, but zero false positives"
|
|
21
|
+
* - This is deliberately narrow: the broader space is a table of many
|
|
22
|
+
* attribute/condition pairs. This rule implements only the one pairing
|
|
23
|
+
* (`aria-errormessage` / `aria-invalid`) that is unambiguous and
|
|
24
|
+
* explicitly stated in the ARIA spec, to keep `fail` high-confidence —
|
|
25
|
+
* matches this repo's established pattern (see `aria-required-attr`/
|
|
26
|
+
* `aria-prohibited-attr` for the same "narrow but zero false positives"
|
|
26
27
|
* trade-off).
|
|
27
28
|
* - Does not check whether the `aria-errormessage` ID reference itself
|
|
28
29
|
* resolves to an existing element — that is `aria-valid-attr-value`'s
|
|
@@ -1,33 +1,40 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check aria-deprecated-role
|
|
5
7
|
* @atomic true
|
|
6
|
-
* @summary An explicit role attribute
|
|
8
|
+
* @summary An explicit role attribute should not use a deprecated or author-discouraged ARIA role
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 4.1.2
|
|
9
11
|
* @applicability
|
|
10
12
|
* Applies to any element whose role attribute's first (used) token is a
|
|
11
|
-
* valid, non-abstract ARIA role that authors
|
|
13
|
+
* valid, non-abstract ARIA role that authors should not explicitly
|
|
12
14
|
* declare — either because WAI-ARIA has deprecated it (e.g. "directory",
|
|
13
|
-
* superseded by role="list") or because it
|
|
14
|
-
* internal use
|
|
15
|
-
*
|
|
16
|
-
* against both the WAI-ARIA 1.2 spec text and MDN's explicit "It should
|
|
17
|
-
* not be used by web authors" guidance).
|
|
15
|
+
* superseded by role="list") or because it is reserved for user-agent-
|
|
16
|
+
* internal use (role="generic", which ARIA 1.2 §5.4 says authors SHOULD
|
|
17
|
+
* NOT use in content).
|
|
18
18
|
* @expectation
|
|
19
|
-
* The role in use is neither deprecated nor
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* The role in use is neither deprecated nor reserved. Graded by the
|
|
20
|
+
* strength of the rule ARIA states:
|
|
21
|
+
* - CANTTELL at SHOULD NOT, which leaves the usage conforming, so the
|
|
22
|
+
* author decides whether it matters: a deprecated role ("directory") or
|
|
23
|
+
* one reserved for user agents ("generic").
|
|
24
|
+
* - FAIL at MUST NOT. No ARIA 1.2 or 1.3 role carries an author MUST NOT
|
|
25
|
+
* outside the abstract roles, so this outcome is reserved for a later
|
|
26
|
+
* revision promoting a role to that strength.
|
|
27
|
+
* Distinct, atomic decision from aria-roles-valid (existence/
|
|
28
|
+
* abstractness): a role can be valid and non-abstract while still being
|
|
29
|
+
* discouraged in explicit author use.
|
|
23
30
|
*/
|
|
24
31
|
|
|
25
32
|
const id = 'aria-deprecated-role';
|
|
26
33
|
|
|
27
34
|
const meta = {
|
|
28
|
-
title: 'role attribute
|
|
35
|
+
title: 'role attribute should not use a deprecated or author-discouraged ARIA role',
|
|
29
36
|
description:
|
|
30
|
-
'Checks that an explicit role="" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use
|
|
37
|
+
'Checks that an explicit role="" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use (e.g. role="generic").',
|
|
31
38
|
i18n: {
|
|
32
39
|
titleKey: 'ariaDeprecatedRole_title',
|
|
33
40
|
descriptionKey: 'ariaDeprecatedRole_description'
|
|
@@ -63,12 +70,31 @@ function runInPage(ctx) {
|
|
|
63
70
|
? helpers.queryAllSmart('[role]')
|
|
64
71
|
: helpers.queryAll('[role]');
|
|
65
72
|
|
|
66
|
-
const
|
|
73
|
+
const failOccurrences = [];
|
|
74
|
+
const cantTellOccurrences = [];
|
|
67
75
|
let applicableCount = 0;
|
|
68
76
|
|
|
77
|
+
// A role on an element hidden from assistive technology has no effect, so
|
|
78
|
+
// ACT 674b10 does not apply to it.
|
|
79
|
+
function isHidden(el) {
|
|
80
|
+
try {
|
|
81
|
+
if (typeof helpers.isDomVisibleEligible === 'function') {
|
|
82
|
+
if (!helpers.isDomVisibleEligible(el, ctx)) return true;
|
|
83
|
+
}
|
|
84
|
+
for (let n = el; n && n.getAttribute; n = n.parentElement) {
|
|
85
|
+
if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
for (const el of nodes) {
|
|
70
94
|
if (!el || !el.getAttribute) continue;
|
|
71
95
|
|
|
96
|
+
if (isHidden(el)) continue;
|
|
97
|
+
|
|
72
98
|
const role = ariaHelpers.getExplicitRole(el);
|
|
73
99
|
if (!role) continue;
|
|
74
100
|
|
|
@@ -78,7 +104,14 @@ function runInPage(ctx) {
|
|
|
78
104
|
|
|
79
105
|
applicableCount += 1;
|
|
80
106
|
|
|
81
|
-
|
|
107
|
+
const deprecated = ariaHelpers.isDeprecatedRole(role);
|
|
108
|
+
const discouraged =
|
|
109
|
+
typeof ariaHelpers.isAuthorDiscouragedRole === 'function' &&
|
|
110
|
+
ariaHelpers.isAuthorDiscouragedRole(role);
|
|
111
|
+
const prohibited =
|
|
112
|
+
typeof ariaHelpers.isAuthorProhibitedRole === 'function' &&
|
|
113
|
+
ariaHelpers.isAuthorProhibitedRole(role);
|
|
114
|
+
if (!deprecated && !discouraged && !prohibited) continue;
|
|
82
115
|
|
|
83
116
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
84
117
|
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
@@ -86,34 +119,70 @@ function runInPage(ctx) {
|
|
|
86
119
|
? ariaHelpers.getDeprecatedRoleGuidance(role)
|
|
87
120
|
: 'Replace the deprecated role with its recommended replacement.';
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
if (prohibited) {
|
|
123
|
+
// Author MUST NOT: the usage is non-conforming, not merely discouraged.
|
|
124
|
+
failOccurrences.push({
|
|
125
|
+
selector: stableSelector,
|
|
126
|
+
html,
|
|
127
|
+
summary: `This element uses role="${role}", which authors must not explicitly declare.`,
|
|
128
|
+
hint: guidance,
|
|
129
|
+
occurrenceOutcome: 'fail',
|
|
130
|
+
i18n: {
|
|
131
|
+
summaryKey: 'ariaDeprecatedRole_summary_fail',
|
|
132
|
+
hintKey: 'ariaDeprecatedRole_hint_fail',
|
|
133
|
+
params: { role, guidance }
|
|
134
|
+
},
|
|
135
|
+
data: {
|
|
136
|
+
details: { reasonCode: 'ARIA_ROLE_AUTHOR_PROHIBITED', role, guidance }
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
} else if (discouraged) {
|
|
140
|
+
// Reserved for user-agent-internal use, at SHOULD NOT strength.
|
|
141
|
+
cantTellOccurrences.push({
|
|
142
|
+
selector: stableSelector,
|
|
143
|
+
html,
|
|
144
|
+
summary: `This element uses role="${role}", which is reserved for user agents (still valid, but discouraged).`,
|
|
145
|
+
hint: guidance,
|
|
146
|
+
occurrenceOutcome: 'cantTell',
|
|
147
|
+
i18n: {
|
|
148
|
+
summaryKey: 'ariaDeprecatedRole_summary_cantTell_discouraged',
|
|
149
|
+
hintKey: 'ariaDeprecatedRole_hint_cantTell',
|
|
150
|
+
params: { role, guidance }
|
|
151
|
+
},
|
|
152
|
+
data: {
|
|
153
|
+
details: { reasonCode: 'ARIA_ROLE_AUTHOR_DISCOURAGED', role, guidance }
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
// Deprecated but still valid: surfaced for the author to decide.
|
|
158
|
+
cantTellOccurrences.push({
|
|
159
|
+
selector: stableSelector,
|
|
160
|
+
html,
|
|
161
|
+
summary: `This element uses role="${role}", which is deprecated in WAI-ARIA.`,
|
|
162
|
+
hint: guidance,
|
|
163
|
+
occurrenceOutcome: 'cantTell',
|
|
164
|
+
i18n: {
|
|
165
|
+
summaryKey: 'ariaDeprecatedRole_summary_cantTell',
|
|
166
|
+
hintKey: 'ariaDeprecatedRole_hint_cantTell',
|
|
167
|
+
params: { role, guidance }
|
|
168
|
+
},
|
|
169
|
+
data: {
|
|
170
|
+
details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance }
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
103
174
|
}
|
|
104
175
|
|
|
105
176
|
if (applicableCount === 0) {
|
|
106
177
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
107
178
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
179
|
+
|
|
180
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
181
|
+
failOccurrences,
|
|
182
|
+
cantTellOccurrences,
|
|
183
|
+
rule.defaultSeverity || 'moderate'
|
|
184
|
+
);
|
|
185
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
117
186
|
}
|
|
118
187
|
|
|
119
188
|
module.exports = { id, meta, runInPage };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -14,10 +16,10 @@
|
|
|
14
16
|
* @expectation
|
|
15
17
|
* <body> does not have aria-hidden="true". Hiding the document body
|
|
16
18
|
* removes the entire page's content and structure from the
|
|
17
|
-
* accessibility tree at once —
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* accessibility tree at once — both 1.3.1 (Info and Relationships: the
|
|
20
|
+
* page's structure becomes entirely non-determinable) and 4.1.2 (Name,
|
|
21
|
+
* Role, Value: nothing in the document exposes a role/name/value any
|
|
22
|
+
* longer) apply.
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
const id = 'aria-hidden-body';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -12,7 +14,7 @@
|
|
|
12
14
|
* No element with aria-hidden="true" may itself be focusable, and no focusable element
|
|
13
15
|
* may exist within an aria-hidden="true" subtree.
|
|
14
16
|
*
|
|
15
|
-
* Notes
|
|
17
|
+
* Notes:
|
|
16
18
|
* - Focusability is computed via ctx.helpers.getFocusableInfo (native + tabindex + contenteditable).
|
|
17
19
|
* - Elements that are not rendered (e.g., display:none, visibility:hidden, [hidden]) are excluded.
|
|
18
20
|
* - Elements hidden via CSS in ways that still allow keyboard focus (e.g., opacity:0, off-screen, clip)
|
|
@@ -485,14 +487,11 @@ function runInPage(ctx) {
|
|
|
485
487
|
// An explicit negative tabindex removes the element from the keyboard
|
|
486
488
|
// tab sequence entirely, regardless of tag — the standard, WAI-
|
|
487
489
|
// recommended technique for safely hiding focusable content behind
|
|
488
|
-
// aria-hidden (
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
// flagging as a false positive). Such an element is still
|
|
494
|
-
// programmatically focusable (script could call .focus()), but that's
|
|
495
|
-
// not what "no focusable content behind aria-hidden" cares about.
|
|
490
|
+
// aria-hidden (e.g. <button tabindex="-1"> / <a tabindex="-1"> inside
|
|
491
|
+
// an aria-hidden container). This cares about tabbability, not raw
|
|
492
|
+
// focusability. Such an element is still programmatically focusable
|
|
493
|
+
// (script could call .focus()), but that's not what "no focusable
|
|
494
|
+
// content behind aria-hidden" cares about.
|
|
496
495
|
const explicitTabindex = trim(el.getAttribute('tabindex'));
|
|
497
496
|
if (
|
|
498
497
|
explicitTabindex !== '' &&
|
|
@@ -640,10 +639,9 @@ function runInPage(ctx) {
|
|
|
640
639
|
// cheaper first cannot change which elements end up in the bucket —
|
|
641
640
|
// it only skips the expensive check for the (typically vast) majority
|
|
642
641
|
// of focusable candidates that were never inside an aria-hidden root
|
|
643
|
-
// in the first place. On
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
// a pure ordering change, not a behavior change.
|
|
642
|
+
// in the first place. On pages with many focusable candidates and a
|
|
643
|
+
// large stylesheet this is a big speedup, and a pure ordering change,
|
|
644
|
+
// not a behavior change.
|
|
647
645
|
const rootEl = closestAriaHiddenTrue(el);
|
|
648
646
|
if (!rootEl) continue;
|
|
649
647
|
|
|
@@ -777,6 +775,7 @@ function runInPage(ctx) {
|
|
|
777
775
|
hint: downgradedToCantTell
|
|
778
776
|
? 'Verify this is an intentional focus sentinel/focus-trap handoff and that keyboard users never remain on hidden focus targets.'
|
|
779
777
|
: 'Remove focusability from descendants or remove aria-hidden; ensure focus and accessibility trees stay aligned.',
|
|
778
|
+
occurrenceOutcome: downgradedToCantTell ? 'cantTell' : 'fail',
|
|
780
779
|
i18n: {
|
|
781
780
|
summaryKey: downgradedToCantTell
|
|
782
781
|
? 'ariaHidden_focus_summary_cantTell_redirect'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -7,15 +9,17 @@
|
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 4.1.2
|
|
9
11
|
* @applicability
|
|
10
|
-
* Applies to (a) elements whose explicit, valid role is one of the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* caption, code, deletion, emphasis, generic,
|
|
14
|
-
* paragraph, presentation, strong, subscript,
|
|
15
|
-
* time), and (b)
|
|
16
|
-
*
|
|
17
|
-
* below)
|
|
18
|
-
*
|
|
12
|
+
* Applies to (a) elements whose explicit, valid role is one of the ARIA
|
|
13
|
+
* 1.2 roles with a documented "Prohibited ARIA States and Properties"
|
|
14
|
+
* list for naming attributes (pure text-semantics / non-naming
|
|
15
|
+
* structural roles: caption, code, deletion, emphasis, generic,
|
|
16
|
+
* insertion, mark, none, paragraph, presentation, strong, subscript,
|
|
17
|
+
* suggestion, superscript, time), and (b) elements with no role at all —
|
|
18
|
+
* a curated set of native HTML tags verified to carry no implicit role
|
|
19
|
+
* (see ROLELESS_NATIVE_TAGS below), or any autonomous custom element (a
|
|
20
|
+
* hyphenated, author-defined tag per the Custom Elements spec; see
|
|
21
|
+
* isRolelessCustomElementTag below) — in both cases, only elements that
|
|
22
|
+
* also carry aria-label or aria-labelledby.
|
|
19
23
|
* @expectation
|
|
20
24
|
* Prohibited attributes must not be present on (a); for (b), the naming
|
|
21
25
|
* attribute is at best unreliable (nothing accessible-name-aware to hang
|
|
@@ -24,97 +28,55 @@
|
|
|
24
28
|
* this produces.
|
|
25
29
|
* @implementation-notes
|
|
26
30
|
* - Deliberately scoped to the single, well-established prohibition class
|
|
27
|
-
* (naming attributes on pure text-semantics roles) rather than
|
|
28
|
-
*
|
|
31
|
+
* (naming attributes on pure text-semantics roles) rather than an
|
|
32
|
+
* exhaustive per-role prohibited-attribute table; see
|
|
29
33
|
* src/core/aria-helpers.js file header for this engine's confidence-
|
|
30
|
-
* scoping rationale.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* documented 1.2-introduced alias, identical semantics). The
|
|
46
|
-
* pre-existing `presentation-role-conflict` rule already treats
|
|
47
|
-
* aria-label/aria-labelledby as conflicting on these two roles, but at
|
|
48
|
-
* `manual`/cantTell confidence across a ~24-attribute general list —
|
|
49
|
-
* this addition lets the specific, unambiguous naming-prohibition case
|
|
50
|
-
* also fire as a hard, WCAG-normative `fail` via this rule, matching
|
|
51
|
-
* this engine's "one rule = one normative decision" pattern rather than
|
|
52
|
-
* only ever surfacing it as advisory.
|
|
53
|
-
* - Investigated, but deliberately did NOT add, `definition`/`term`
|
|
54
|
-
* despite both appearing on MDN's aria-label reference page's
|
|
55
|
-
* "not supported" list: that MDN list is demonstrably wrong for these
|
|
56
|
-
* two — a widely-used reference engine's own role data explicitly declares
|
|
57
|
-
* `nameFrom: ['author']` (`definition`) / `nameFrom: ['author',
|
|
58
|
-
* 'contents']` (`term`), and the W3C spec's own §5.2.8.4 "Roles
|
|
59
|
-
* Supporting Name From Author" index lists both by name; MDN's
|
|
60
|
-
* `definition_role` page even demonstrates `aria-labelledby` usage on
|
|
61
|
-
* it directly. A real, confirmed documentation bug on MDN's side, not
|
|
62
|
-
* a gap here.
|
|
34
|
+
* scoping rationale. The 14-role list comes from the W3C WAI-ARIA 1.2
|
|
35
|
+
* spec's §5.2.8.6 "Roles which cannot be named"; only roles/attrs with
|
|
36
|
+
* high confidence from the spec text are included, since a wrong entry
|
|
37
|
+
* here causes a false-positive fail.
|
|
38
|
+
* - The pre-existing presentation-role-conflict rule already treats
|
|
39
|
+
* aria-label/aria-labelledby as conflicting on presentation/none, but at
|
|
40
|
+
* manual/cantTell confidence across a broad attribute list; this rule's
|
|
41
|
+
* narrower, unambiguous naming-prohibition case fires as a hard,
|
|
42
|
+
* WCAG-normative fail instead, matching this engine's "one rule = one
|
|
43
|
+
* normative decision" pattern.
|
|
44
|
+
* - Deliberately excludes `definition`/`term` despite both appearing on
|
|
45
|
+
* MDN's aria-label "not supported" list: both support name from author
|
|
46
|
+
* (`nameFrom: ['author']` for definition, `['author', 'contents']` for
|
|
47
|
+
* term), and the W3C spec's §5.2.8.4 "Roles Supporting Name From Author"
|
|
48
|
+
* index lists both by name.
|
|
63
49
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
64
50
|
* property, while engine-level hidden-subtree filtering still applies
|
|
65
51
|
* unless engineOptions.includeHiddenElements is true.
|
|
66
|
-
* -
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* at all (their prohibited-attrs entries only ever matter for an
|
|
79
|
-
* EXPLICIT `role="paragraph"`/`role="strong"`/etc. restatement, a rare
|
|
80
|
-
* case — the native tag itself resolves to role `null`, same as a bare
|
|
81
|
-
* `<div>`/`<span>`, and falls into this same roleless branch). See
|
|
82
|
-
* ROLELESS_NATIVE_TAGS below for the resulting curated list —
|
|
83
|
-
* deliberately conservative: `<section>`/`<form>`/`<a>` are excluded
|
|
84
|
-
* even though they can also resolve to no role, because their native
|
|
85
|
-
* role is conditional (name-dependent/href-dependent) and already has
|
|
86
|
-
* dedicated, more nuanced handling elsewhere in this engine
|
|
87
|
-
* (`getElementRoleKey`'s `section`/`section[named]`/`header`/
|
|
88
|
-
* `header[toplevel]` branches) that this rule doesn't attempt to
|
|
89
|
-
* duplicate.
|
|
52
|
+
* - Second, independent branch: naming attributes on ROLELESS elements (no
|
|
53
|
+
* explicit role="", no implicit/native role either) — e.g. icon-only
|
|
54
|
+
* `<span aria-label="...">` tiles with no other accessible-name source.
|
|
55
|
+
* ROLELESS_NATIVE_TAGS below is a curated, deliberately conservative
|
|
56
|
+
* list of native tags confirmed to carry no implicit role (common
|
|
57
|
+
* text-level tags like `<p>`/`<strong>`/`<em>`/`<code>`/`<mark>`/`<time>`
|
|
58
|
+
* resolve to role `null`, same as a bare `<div>`/`<span>`);
|
|
59
|
+
* `<section>`/`<form>`/`<a>` are excluded even though they can also
|
|
60
|
+
* resolve to no role, because their native role is conditional
|
|
61
|
+
* (name-dependent/href-dependent) and already has dedicated handling in
|
|
62
|
+
* `getElementRoleKey`'s `section`/`section[named]`/`header`/
|
|
63
|
+
* `header[toplevel]` branches that this rule doesn't duplicate.
|
|
90
64
|
* Two confidence tiers instead of a flat fail: if the element's subtree
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* redundant/intentional override — reported as `cantTell`, not
|
|
95
|
-
* fail. Only a roleless element with
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* was present (`getExplicitRole`), not whether the value was a real,
|
|
107
|
-
* recognized ARIA role. An invalid/typo'd role token (e.g.
|
|
108
|
-
* `role="totally-bogus"`) therefore silently suppressed detection of an
|
|
109
|
-
* otherwise-flaggable roleless naming attribute — identical markup with
|
|
110
|
-
* the bogus role attribute removed entirely correctly failed, but with
|
|
111
|
-
* it present the element was skipped as if it had a real role. Per spec
|
|
112
|
-
* (and per this same file's own `getNearestAncestorRole` helper a few
|
|
113
|
-
* lines below, which already gets this right), an unrecognized role
|
|
114
|
-
* token is ignored by the accessibility tree, not honored — the element
|
|
115
|
-
* is still effectively roleless. Now validates via the existing
|
|
116
|
-
* `isValidConcreteRole` before treating an explicit role as real,
|
|
117
|
-
* matching `getNearestAncestorRole`'s own pattern.
|
|
65
|
+
* already produces a non-empty accessible name from its content (via
|
|
66
|
+
* `helpers.getContentNameInfo`, same as
|
|
67
|
+
* link-name-present/button-name-present), the naming attribute might
|
|
68
|
+
* just be a redundant/intentional override — reported as `cantTell`, not
|
|
69
|
+
* a hard fail. Only a roleless element with no other accessible-name
|
|
70
|
+
* source at all is a confident, deterministic `fail`. The
|
|
71
|
+
* widget-ancestor exemption (skip when the closest real ancestor role is
|
|
72
|
+
* a "widget"-type role) avoids over-flagging roleless helper spans/divs
|
|
73
|
+
* used as internal decoration inside a custom composite widget.
|
|
74
|
+
* - The Tier-2 "already has a role, not this branch's concern" guard
|
|
75
|
+
* validates the explicit role via `isValidConcreteRole` before treating
|
|
76
|
+
* it as real (matching `getNearestAncestorRole`'s own pattern): an
|
|
77
|
+
* unrecognized role token (e.g. `role="totally-bogus"`) is ignored by
|
|
78
|
+
* the accessibility tree, not honored, so the element is still
|
|
79
|
+
* effectively roleless and must still be checked by this branch.
|
|
118
80
|
*/
|
|
119
81
|
|
|
120
82
|
const id = 'aria-prohibited-attr';
|
|
@@ -212,6 +174,7 @@ function runInPage(ctx) {
|
|
|
212
174
|
failOccurrences.push({
|
|
213
175
|
selector: stableSelector,
|
|
214
176
|
html,
|
|
177
|
+
occurrenceOutcome: 'fail',
|
|
215
178
|
summary: 'This attribute is prohibited on this element’s role.',
|
|
216
179
|
hint: 'Remove this attribute; this role must not carry an accessible name.',
|
|
217
180
|
i18n: {
|
|
@@ -229,11 +192,10 @@ function runInPage(ctx) {
|
|
|
229
192
|
// --- Tier 2: no role at all (see header comment for the full rationale
|
|
230
193
|
// and how ROLELESS_NATIVE_TAGS/WIDGET_TYPE_ROLES were derived) ---
|
|
231
194
|
|
|
232
|
-
// Small, curated set of native tags
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// handled with more nuance elsewhere in this engine (see header comment).
|
|
195
|
+
// Small, curated set of native tags verified to carry no explicit or
|
|
196
|
+
// implicit ARIA role. Deliberately excludes <section>/<form>/<a> — all
|
|
197
|
+
// conditionally roleless too, but already handled with more nuance
|
|
198
|
+
// elsewhere in this engine (see header comment).
|
|
237
199
|
const ROLELESS_NATIVE_TAGS = new Set([
|
|
238
200
|
'p',
|
|
239
201
|
'b',
|
|
@@ -268,10 +230,8 @@ function runInPage(ctx) {
|
|
|
268
230
|
'legend'
|
|
269
231
|
]);
|
|
270
232
|
|
|
271
|
-
// WAI-ARIA roles
|
|
272
|
-
//
|
|
273
|
-
// WAI-ARIA taxonomy — this engine's algorithm branches on its own `type`
|
|
274
|
-
// field, so parity means matching that field exactly).
|
|
233
|
+
// WAI-ARIA roles typed as "widget" (the role set the roleless-branch
|
|
234
|
+
// exemption below branches on, not the six-category WAI-ARIA taxonomy).
|
|
275
235
|
const WIDGET_TYPE_ROLES = new Set([
|
|
276
236
|
'alert',
|
|
277
237
|
'alertdialog',
|
|
@@ -337,6 +297,37 @@ function runInPage(ctx) {
|
|
|
337
297
|
return '';
|
|
338
298
|
}
|
|
339
299
|
|
|
300
|
+
// A small, spec-reserved set of hyphenated tag names that are NOT
|
|
301
|
+
// autonomous custom elements despite containing a hyphen (legacy SVG/
|
|
302
|
+
// MathML tags predating the Custom Elements spec) — see
|
|
303
|
+
// https://html.spec.whatwg.org/#valid-custom-element-name's own
|
|
304
|
+
// exclusion list. Excluded so this doesn't misclassify them as
|
|
305
|
+
// always-roleless the same way a real custom element is.
|
|
306
|
+
const RESERVED_HYPHENATED_TAGS = new Set([
|
|
307
|
+
'annotation-xml',
|
|
308
|
+
'color-profile',
|
|
309
|
+
'font-face',
|
|
310
|
+
'font-face-src',
|
|
311
|
+
'font-face-uri',
|
|
312
|
+
'font-face-format',
|
|
313
|
+
'font-face-name',
|
|
314
|
+
'missing-glyph'
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
// An autonomous custom element (author-defined tag, always containing a
|
|
318
|
+
// hyphen per the Custom Elements spec's naming grammar) has no implicit
|
|
319
|
+
// ARIA role at all -- unlike native tags, there is no conditional-role
|
|
320
|
+
// nuance to worry about here (a native <a>/<section>/<form> can gain an
|
|
321
|
+
// implicit role depending on other attributes, which is exactly why
|
|
322
|
+
// ROLELESS_NATIVE_TAGS is a hand-verified allowlist rather than a
|
|
323
|
+
// blanket rule; a custom element has no such spec-defined conditional
|
|
324
|
+
// role logic whatsoever). Covers e.g. a `<play-button aria-label="...">`
|
|
325
|
+
// or `<app-carousel aria-label="...">` with no other name source, which
|
|
326
|
+
// the fixed native-tag allowlist below would otherwise skip.
|
|
327
|
+
function isRolelessCustomElementTag(tag) {
|
|
328
|
+
return tag.includes('-') && !RESERVED_HYPHENATED_TAGS.has(tag);
|
|
329
|
+
}
|
|
330
|
+
|
|
340
331
|
const namingSelector = '[aria-label],[aria-labelledby]';
|
|
341
332
|
const namingNodes = helpers.queryAllSmart
|
|
342
333
|
? helpers.queryAllSmart(namingSelector)
|
|
@@ -346,7 +337,7 @@ function runInPage(ctx) {
|
|
|
346
337
|
if (!el || !el.getAttribute) continue;
|
|
347
338
|
|
|
348
339
|
const tag = String(el.tagName || '').toLowerCase();
|
|
349
|
-
if (!ROLELESS_NATIVE_TAGS.has(tag)) continue;
|
|
340
|
+
if (!ROLELESS_NATIVE_TAGS.has(tag) && !isRolelessCustomElementTag(tag)) continue;
|
|
350
341
|
const explicitRole = ariaHelpers.getExplicitRole(el);
|
|
351
342
|
if (explicitRole && ariaHelpers.isValidConcreteRole(explicitRole)) continue; // has a real, recognized role — Tier 1's concern (if in ROLES_PROHIBITING_NAME) or a role this rule has no opinion on. An INVALID role token (e.g. a typo) is ignored per spec, same as no role attribute at all, and must still fall through to this branch.
|
|
352
343
|
if (ariaHelpers.getNativeRoleForElement(el)) continue; // has a real implicit role after all — not this branch's concern
|
|
@@ -378,6 +369,7 @@ function runInPage(ctx) {
|
|
|
378
369
|
cantTellOccurrences.push({
|
|
379
370
|
selector: stableSelector,
|
|
380
371
|
html,
|
|
372
|
+
occurrenceOutcome: 'cantTell',
|
|
381
373
|
summary: `This ${tag} has no role, so ${attr} may not be exposed as its accessible name by assistive technology — but the element's own content already provides one.`,
|
|
382
374
|
hint: 'Verify whether the existing text content already serves as this element’s label; if so the naming attribute is redundant, otherwise give the element a role that supports naming (e.g. role="img").',
|
|
383
375
|
i18n: {
|
|
@@ -398,6 +390,7 @@ function runInPage(ctx) {
|
|
|
398
390
|
failOccurrences.push({
|
|
399
391
|
selector: stableSelector,
|
|
400
392
|
html,
|
|
393
|
+
occurrenceOutcome: 'fail',
|
|
401
394
|
summary: `This ${tag} has no role and no other accessible-name source, so ${attr} is not reliably exposed to assistive technology.`,
|
|
402
395
|
hint: 'Give this element a role that supports an accessible name (e.g. role="img"/"button"), or remove this attribute if it serves no purpose without one.',
|
|
403
396
|
i18n: {
|