@surea11y/core 1.2.0 → 1.3.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 +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -7,16 +7,21 @@
|
|
|
7
7
|
* @standard WCAG 2.2
|
|
8
8
|
* @sc 4.1.2
|
|
9
9
|
* @applicability
|
|
10
|
-
* Applies to elements whose explicit, valid role is one of the small
|
|
11
|
-
* of WAI-ARIA 1.2 roles with a documented "Prohibited ARIA States and
|
|
10
|
+
* Applies to (a) elements whose explicit, valid role is one of the small
|
|
11
|
+
* set of WAI-ARIA 1.2 roles with a documented "Prohibited ARIA States and
|
|
12
12
|
* Properties" list (pure text-semantics / non-naming structural roles:
|
|
13
13
|
* caption, code, deletion, emphasis, generic, insertion, mark, none,
|
|
14
14
|
* paragraph, presentation, strong, subscript, suggestion, superscript,
|
|
15
|
-
* time), and
|
|
15
|
+
* time), and (b) a small, curated set of native HTML tags verified to
|
|
16
|
+
* carry no explicit or implicit ARIA role at all (see ROLELESS_NATIVE_TAGS
|
|
17
|
+
* below) — in both cases, only elements that also carry aria-label or
|
|
18
|
+
* aria-labelledby.
|
|
16
19
|
* @expectation
|
|
17
|
-
* Prohibited attributes must not be present
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* Prohibited attributes must not be present on (a); for (b), the naming
|
|
21
|
+
* attribute is at best unreliable (nothing accessible-name-aware to hang
|
|
22
|
+
* it off) and at worst silently ignored by assistive technology — see the
|
|
23
|
+
* roleless-branch implementation note below for the confidence split
|
|
24
|
+
* this produces.
|
|
20
25
|
* @implementation-notes
|
|
21
26
|
* - Deliberately scoped to the single, well-established prohibition class
|
|
22
27
|
* (naming attributes on pure text-semantics roles) rather than
|
|
@@ -58,13 +63,66 @@
|
|
|
58
63
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
59
64
|
* property, while engine-level hidden-subtree filtering still applies
|
|
60
65
|
* unless engineOptions.includeHiddenElements is true.
|
|
66
|
+
* - Widened 2026-07-31 to add a second, independent branch covering
|
|
67
|
+
* naming attributes on ROLELESS elements (no explicit role="", no
|
|
68
|
+
* implicit/native role either) — found on the emoji-mart demo page
|
|
69
|
+
* (missive.github.io/emoji-mart): hundreds of
|
|
70
|
+
* `<span aria-label="party_parrot" class="emoji-mart-emoji...">` tiles,
|
|
71
|
+
* plain roleless spans with no other accessible-name source, which this
|
|
72
|
+
* rule previously ignored entirely, since its own Tier-1 branch only ever
|
|
73
|
+
* looked at the EXPLICIT role="" attribute, never at "no role at all."
|
|
74
|
+
* Empirically determined (not guessed) which native tags genuinely carry
|
|
75
|
+
* no role at all, by resolving each candidate tag's role against a live
|
|
76
|
+
* Chromium page — several surprises: common text-level tags like `<p>`,
|
|
77
|
+
* `<strong>`, `<em>`, `<code>`, `<mark>`, `<time>` have no implicit role
|
|
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.
|
|
90
|
+
* Two confidence tiers instead of a flat fail: if the element's subtree
|
|
91
|
+
* ALREADY produces a non-empty accessible name from its content
|
|
92
|
+
* (computed the same way link-name-present/button-name-present do, via
|
|
93
|
+
* `helpers.getContentNameInfo`), the naming attribute might just be a
|
|
94
|
+
* redundant/intentional override — reported as `cantTell`, not a hard
|
|
95
|
+
* fail. Only a roleless element with NO other accessible-name source at
|
|
96
|
+
* all (the emoji-mart case: an icon-only span, background-image styled,
|
|
97
|
+
* no text anywhere in its subtree) is a confident, deterministic `fail`
|
|
98
|
+
* — nothing else could ever expose this element's name, and no role
|
|
99
|
+
* exists to make it a Name/Role/Value candidate in the first place.
|
|
100
|
+
* The widget-ancestor exemption (skip when the closest real ancestor role
|
|
101
|
+
* is a "widget"-type role) avoids over-flagging roleless helper
|
|
102
|
+
* spans/divs used as internal decoration inside a custom composite
|
|
103
|
+
* widget.
|
|
104
|
+
* - Fixed 2026-07-31 (same day as introduced): the Tier-2 "already has a
|
|
105
|
+
* role, not this branch's concern" guard checked only whether `role=""`
|
|
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.
|
|
61
118
|
*/
|
|
62
119
|
|
|
63
120
|
const id = 'aria-prohibited-attr';
|
|
64
121
|
|
|
65
122
|
const meta = {
|
|
66
123
|
title: 'ARIA naming attributes must not be used on roles that prohibit them',
|
|
67
|
-
description:
|
|
124
|
+
description:
|
|
125
|
+
'Checks that aria-label/aria-labelledby are not present on WAI-ARIA roles whose specification explicitly prohibits ARIA naming (e.g. generic, emphasis, strong, paragraph).',
|
|
68
126
|
i18n: {
|
|
69
127
|
titleKey: 'ariaProhibitedAttr_title',
|
|
70
128
|
descriptionKey: 'ariaProhibitedAttr_description'
|
|
@@ -73,7 +131,13 @@ const meta = {
|
|
|
73
131
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
74
132
|
wcagSc: ['4.1.2'],
|
|
75
133
|
normativeMappings: [
|
|
76
|
-
{
|
|
134
|
+
{
|
|
135
|
+
standard: 'WCAG',
|
|
136
|
+
version: '2.2',
|
|
137
|
+
requirement: '4.1.2',
|
|
138
|
+
title: 'Name, Role, Value',
|
|
139
|
+
conformanceLevel: 'A'
|
|
140
|
+
}
|
|
77
141
|
],
|
|
78
142
|
defaultSeverity: 'moderate',
|
|
79
143
|
category: 'robust',
|
|
@@ -83,7 +147,7 @@ const meta = {
|
|
|
83
147
|
};
|
|
84
148
|
|
|
85
149
|
function runInPage(ctx) {
|
|
86
|
-
const {
|
|
150
|
+
const { helpers, rule } = ctx;
|
|
87
151
|
|
|
88
152
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
89
153
|
if (!ariaHelpers) {
|
|
@@ -96,19 +160,36 @@ function runInPage(ctx) {
|
|
|
96
160
|
// scope) because the build inlines only this function's own source text
|
|
97
161
|
// — see scripts/build-core.js header ("runInPage MUST be self-contained").
|
|
98
162
|
const ROLES_PROHIBITING_NAME = new Set([
|
|
99
|
-
'caption',
|
|
100
|
-
'
|
|
101
|
-
'
|
|
163
|
+
'caption',
|
|
164
|
+
'code',
|
|
165
|
+
'deletion',
|
|
166
|
+
'emphasis',
|
|
167
|
+
'generic',
|
|
168
|
+
'insertion',
|
|
169
|
+
'mark',
|
|
170
|
+
'none',
|
|
171
|
+
'paragraph',
|
|
172
|
+
'presentation',
|
|
173
|
+
'strong',
|
|
174
|
+
'subscript',
|
|
175
|
+
'suggestion',
|
|
176
|
+
'superscript',
|
|
177
|
+
'time'
|
|
102
178
|
]);
|
|
103
179
|
|
|
104
180
|
const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
|
|
105
181
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const occurrences = [];
|
|
182
|
+
const failOccurrences = [];
|
|
183
|
+
const cantTellOccurrences = [];
|
|
109
184
|
let applicableCount = 0;
|
|
110
185
|
|
|
111
|
-
|
|
186
|
+
// --- Tier 1: explicit, valid role from the naming-prohibited set ---
|
|
187
|
+
|
|
188
|
+
const roleNodes = helpers.queryAllSmart
|
|
189
|
+
? helpers.queryAllSmart('[role]')
|
|
190
|
+
: helpers.queryAll('[role]');
|
|
191
|
+
|
|
192
|
+
for (const el of roleNodes) {
|
|
112
193
|
if (!el || !el.getAttribute) continue;
|
|
113
194
|
|
|
114
195
|
const role = ariaHelpers.getExplicitRole(el);
|
|
@@ -125,10 +206,10 @@ function runInPage(ctx) {
|
|
|
125
206
|
if (!present.length) continue;
|
|
126
207
|
|
|
127
208
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
128
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
209
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
129
210
|
|
|
130
211
|
for (const attr of present) {
|
|
131
|
-
|
|
212
|
+
failOccurrences.push({
|
|
132
213
|
selector: stableSelector,
|
|
133
214
|
html,
|
|
134
215
|
summary: 'This attribute is prohibited on this element’s role.',
|
|
@@ -145,13 +226,206 @@ function runInPage(ctx) {
|
|
|
145
226
|
}
|
|
146
227
|
}
|
|
147
228
|
|
|
229
|
+
// --- Tier 2: no role at all (see header comment for the full rationale
|
|
230
|
+
// and how ROLELESS_NATIVE_TAGS/WIDGET_TYPE_ROLES were derived) ---
|
|
231
|
+
|
|
232
|
+
// Small, curated set of native tags empirically verified (against a
|
|
233
|
+
// widely-used reference engine's own getRole() at runtime, not guessed)
|
|
234
|
+
// to carry no explicit or implicit ARIA role. Deliberately excludes
|
|
235
|
+
// <section>/<form>/<a> — all conditionally roleless too, but already
|
|
236
|
+
// handled with more nuance elsewhere in this engine (see header comment).
|
|
237
|
+
const ROLELESS_NATIVE_TAGS = new Set([
|
|
238
|
+
'p',
|
|
239
|
+
'b',
|
|
240
|
+
'i',
|
|
241
|
+
'em',
|
|
242
|
+
'strong',
|
|
243
|
+
'span',
|
|
244
|
+
'div',
|
|
245
|
+
'code',
|
|
246
|
+
'mark',
|
|
247
|
+
'time',
|
|
248
|
+
'ins',
|
|
249
|
+
'del',
|
|
250
|
+
'small',
|
|
251
|
+
'sub',
|
|
252
|
+
'sup',
|
|
253
|
+
'abbr',
|
|
254
|
+
'cite',
|
|
255
|
+
'q',
|
|
256
|
+
'kbd',
|
|
257
|
+
'samp',
|
|
258
|
+
'var',
|
|
259
|
+
'address',
|
|
260
|
+
'blockquote',
|
|
261
|
+
'pre',
|
|
262
|
+
'figcaption',
|
|
263
|
+
'picture',
|
|
264
|
+
'template',
|
|
265
|
+
'hgroup',
|
|
266
|
+
'wbr',
|
|
267
|
+
'br',
|
|
268
|
+
'legend'
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
// WAI-ARIA roles a widely-used reference engine's own role table types as
|
|
272
|
+
// "widget" (verified directly against its source, not the six-category
|
|
273
|
+
// WAI-ARIA taxonomy — this engine's algorithm branches on its own `type`
|
|
274
|
+
// field, so parity means matching that field exactly).
|
|
275
|
+
const WIDGET_TYPE_ROLES = new Set([
|
|
276
|
+
'alert',
|
|
277
|
+
'alertdialog',
|
|
278
|
+
'button',
|
|
279
|
+
'checkbox',
|
|
280
|
+
'combobox',
|
|
281
|
+
'dialog',
|
|
282
|
+
'gridcell',
|
|
283
|
+
'link',
|
|
284
|
+
'listbox',
|
|
285
|
+
'log',
|
|
286
|
+
'marquee',
|
|
287
|
+
'menuitem',
|
|
288
|
+
'menuitemcheckbox',
|
|
289
|
+
'menuitemradio',
|
|
290
|
+
'option',
|
|
291
|
+
'progressbar',
|
|
292
|
+
'radio',
|
|
293
|
+
'scrollbar',
|
|
294
|
+
'searchbox',
|
|
295
|
+
'slider',
|
|
296
|
+
'spinbutton',
|
|
297
|
+
'status',
|
|
298
|
+
'switch',
|
|
299
|
+
'tab',
|
|
300
|
+
'tabpanel',
|
|
301
|
+
'textbox',
|
|
302
|
+
'timer',
|
|
303
|
+
'treeitem'
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const getComposedParent =
|
|
307
|
+
helpers && typeof helpers.composedParent === 'function'
|
|
308
|
+
? helpers.composedParent
|
|
309
|
+
: function (n) {
|
|
310
|
+
return n && n.parentElement ? n.parentElement : null;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Nearest ancestor's real role (explicit-if-valid, else native/implicit),
|
|
314
|
+
// skipping roleless/presentation/none ancestors — used only to check
|
|
315
|
+
// whether that role is a "widget"-type one (the roleless-branch
|
|
316
|
+
// exemption). Not the same helper as aria-required-parent's containment
|
|
317
|
+
// walk: this one also accepts non-required-context roles.
|
|
318
|
+
function getNearestAncestorRole(el) {
|
|
319
|
+
let cur = getComposedParent(el);
|
|
320
|
+
let guard = 0;
|
|
321
|
+
while (cur && guard++ < 200) {
|
|
322
|
+
if (cur.nodeType !== 1) {
|
|
323
|
+
cur = getComposedParent(cur);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const explicit = ariaHelpers.getExplicitRole(cur);
|
|
327
|
+
const role =
|
|
328
|
+
explicit && ariaHelpers.isValidConcreteRole(explicit)
|
|
329
|
+
? explicit
|
|
330
|
+
: ariaHelpers.getNativeRoleForElement(cur);
|
|
331
|
+
if (!role || role === 'presentation' || role === 'none') {
|
|
332
|
+
cur = getComposedParent(cur);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
return role;
|
|
336
|
+
}
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const namingSelector = '[aria-label],[aria-labelledby]';
|
|
341
|
+
const namingNodes = helpers.queryAllSmart
|
|
342
|
+
? helpers.queryAllSmart(namingSelector)
|
|
343
|
+
: helpers.queryAll(namingSelector);
|
|
344
|
+
|
|
345
|
+
for (const el of namingNodes) {
|
|
346
|
+
if (!el || !el.getAttribute) continue;
|
|
347
|
+
|
|
348
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
349
|
+
if (!ROLELESS_NATIVE_TAGS.has(tag)) continue;
|
|
350
|
+
const explicitRole = ariaHelpers.getExplicitRole(el);
|
|
351
|
+
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
|
+
if (ariaHelpers.getNativeRoleForElement(el)) continue; // has a real implicit role after all — not this branch's concern
|
|
353
|
+
|
|
354
|
+
const present = [];
|
|
355
|
+
for (const attr of PROHIBITED_NAMING_ATTRS) {
|
|
356
|
+
const v = el.getAttribute(attr);
|
|
357
|
+
if (v != null && String(v).trim() !== '') present.push(attr);
|
|
358
|
+
}
|
|
359
|
+
if (!present.length) continue;
|
|
360
|
+
|
|
361
|
+
applicableCount += 1;
|
|
362
|
+
|
|
363
|
+
const ancestorRole = getNearestAncestorRole(el);
|
|
364
|
+
if (ancestorRole && WIDGET_TYPE_ROLES.has(ancestorRole)) continue; // roleless helper node inside a real widget — not flagged
|
|
365
|
+
|
|
366
|
+
const nameInfo = helpers.getContentNameInfo ? helpers.getContentNameInfo(el, ctx) : null;
|
|
367
|
+
const hasContentFallback = !!(
|
|
368
|
+
nameInfo &&
|
|
369
|
+
nameInfo.present &&
|
|
370
|
+
String(nameInfo.value || '').trim() !== ''
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
374
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
375
|
+
|
|
376
|
+
for (const attr of present) {
|
|
377
|
+
if (hasContentFallback) {
|
|
378
|
+
cantTellOccurrences.push({
|
|
379
|
+
selector: stableSelector,
|
|
380
|
+
html,
|
|
381
|
+
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
|
+
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
|
+
i18n: {
|
|
384
|
+
summaryKey: 'ariaProhibitedAttr_summary_cantTell_roleless',
|
|
385
|
+
hintKey: 'ariaProhibitedAttr_hint_cantTell_roleless',
|
|
386
|
+
params: { attr, element: tag }
|
|
387
|
+
},
|
|
388
|
+
data: {
|
|
389
|
+
details: {
|
|
390
|
+
reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS_NEEDS_REVIEW',
|
|
391
|
+
attr,
|
|
392
|
+
role: null,
|
|
393
|
+
element: tag
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
failOccurrences.push({
|
|
399
|
+
selector: stableSelector,
|
|
400
|
+
html,
|
|
401
|
+
summary: `This ${tag} has no role and no other accessible-name source, so ${attr} is not reliably exposed to assistive technology.`,
|
|
402
|
+
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
|
+
i18n: {
|
|
404
|
+
summaryKey: 'ariaProhibitedAttr_summary_fail_roleless',
|
|
405
|
+
hintKey: 'ariaProhibitedAttr_hint_fail_roleless',
|
|
406
|
+
params: { attr, element: tag }
|
|
407
|
+
},
|
|
408
|
+
data: {
|
|
409
|
+
details: { reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS', attr, role: null, element: tag }
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
148
416
|
if (applicableCount === 0) {
|
|
149
417
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
150
418
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
419
|
+
|
|
420
|
+
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
421
|
+
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
422
|
+
// the same run — both are returned together when the outcome is 'fail'.
|
|
423
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
424
|
+
failOccurrences,
|
|
425
|
+
cantTellOccurrences,
|
|
426
|
+
rule.defaultSeverity || 'moderate'
|
|
427
|
+
);
|
|
428
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
155
429
|
}
|
|
156
430
|
|
|
157
431
|
module.exports = { id, meta, runInPage };
|
|
@@ -111,7 +111,8 @@ const id = 'aria-prohibited-children';
|
|
|
111
111
|
|
|
112
112
|
const meta = {
|
|
113
113
|
title: 'Container roles must not own a child with a disallowed role',
|
|
114
|
-
description:
|
|
114
|
+
description:
|
|
115
|
+
"Checks that every accessible-tree-owned child of a container role (list, listbox, menu, menubar, radiogroup, rowgroup, table, grid, treegrid, tablist, tree, row) has one of that role's allowed owned roles — the same set as its required owned roles.",
|
|
115
116
|
i18n: {
|
|
116
117
|
titleKey: 'ariaProhibitedChildren_title',
|
|
117
118
|
descriptionKey: 'ariaProhibitedChildren_description'
|
|
@@ -120,7 +121,13 @@ const meta = {
|
|
|
120
121
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
121
122
|
wcagSc: ['4.1.2'],
|
|
122
123
|
normativeMappings: [
|
|
123
|
-
{
|
|
124
|
+
{
|
|
125
|
+
standard: 'WCAG',
|
|
126
|
+
version: '2.2',
|
|
127
|
+
requirement: '4.1.2',
|
|
128
|
+
title: 'Name, Role, Value',
|
|
129
|
+
conformanceLevel: 'A'
|
|
130
|
+
}
|
|
124
131
|
],
|
|
125
132
|
defaultSeverity: 'moderate',
|
|
126
133
|
category: 'robust',
|
|
@@ -130,7 +137,7 @@ const meta = {
|
|
|
130
137
|
};
|
|
131
138
|
|
|
132
139
|
function runInPage(ctx) {
|
|
133
|
-
const {
|
|
140
|
+
const { helpers, rule } = ctx;
|
|
134
141
|
|
|
135
142
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
136
143
|
if (!ariaHelpers) {
|
|
@@ -138,7 +145,8 @@ function runInPage(ctx) {
|
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
function isEligibleAcc(el) {
|
|
141
|
-
const fn =
|
|
148
|
+
const fn =
|
|
149
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
142
150
|
if (!fn) return true;
|
|
143
151
|
try {
|
|
144
152
|
const r = fn(el, ctx);
|
|
@@ -155,12 +163,30 @@ function runInPage(ctx) {
|
|
|
155
163
|
// roleless descendant carrying any of these is a real accessible-tree
|
|
156
164
|
// node a widely-used reference engine's getOwnedRoles also flags, not a transparent wrapper.
|
|
157
165
|
const GLOBAL_ARIA_ATTRS = [
|
|
158
|
-
'aria-atomic',
|
|
159
|
-
'aria-
|
|
160
|
-
'aria-
|
|
161
|
-
'aria-
|
|
162
|
-
'aria-
|
|
163
|
-
'aria-
|
|
166
|
+
'aria-atomic',
|
|
167
|
+
'aria-braillelabel',
|
|
168
|
+
'aria-brailleroledescription',
|
|
169
|
+
'aria-busy',
|
|
170
|
+
'aria-controls',
|
|
171
|
+
'aria-current',
|
|
172
|
+
'aria-describedby',
|
|
173
|
+
'aria-description',
|
|
174
|
+
'aria-details',
|
|
175
|
+
'aria-disabled',
|
|
176
|
+
'aria-dropeffect',
|
|
177
|
+
'aria-errormessage',
|
|
178
|
+
'aria-flowto',
|
|
179
|
+
'aria-grabbed',
|
|
180
|
+
'aria-haspopup',
|
|
181
|
+
'aria-hidden',
|
|
182
|
+
'aria-invalid',
|
|
183
|
+
'aria-keyshortcuts',
|
|
184
|
+
'aria-label',
|
|
185
|
+
'aria-labelledby',
|
|
186
|
+
'aria-live',
|
|
187
|
+
'aria-owns',
|
|
188
|
+
'aria-relevant',
|
|
189
|
+
'aria-roledescription'
|
|
164
190
|
];
|
|
165
191
|
|
|
166
192
|
function getGlobalAriaAttr(el) {
|
|
@@ -195,13 +221,17 @@ function runInPage(ctx) {
|
|
|
195
221
|
|
|
196
222
|
const kidRole = ariaHelpers.getContainmentRole(kid);
|
|
197
223
|
const isPresentational = kidRole === 'presentation' || kidRole === 'none';
|
|
198
|
-
const isTransparentGroup =
|
|
224
|
+
const isTransparentGroup =
|
|
225
|
+
(kidRole === 'group' || kidRole === 'rowgroup') && requiredSet.has(kidRole);
|
|
199
226
|
|
|
200
227
|
if (!kidRole && !isPresentational) {
|
|
201
228
|
const globalAttr = getGlobalAriaAttr(kid);
|
|
202
|
-
let mechanism
|
|
229
|
+
let mechanism;
|
|
203
230
|
try {
|
|
204
|
-
const fi =
|
|
231
|
+
const fi =
|
|
232
|
+
helpers && typeof helpers.getFocusableInfo === 'function'
|
|
233
|
+
? helpers.getFocusableInfo(kid, ctx)
|
|
234
|
+
: null;
|
|
205
235
|
mechanism = (fi && fi.focusable && fi.mechanism) || 'none';
|
|
206
236
|
} catch {
|
|
207
237
|
mechanism = 'none';
|
|
@@ -230,7 +260,9 @@ function runInPage(ctx) {
|
|
|
230
260
|
}
|
|
231
261
|
}
|
|
232
262
|
|
|
233
|
-
const nodes = helpers.queryAllSmart
|
|
263
|
+
const nodes = helpers.queryAllSmart
|
|
264
|
+
? helpers.queryAllSmart('[role]')
|
|
265
|
+
: helpers.queryAll('[role]');
|
|
234
266
|
|
|
235
267
|
const occurrences = [];
|
|
236
268
|
let applicableCount = 0;
|
|
@@ -256,7 +288,9 @@ function runInPage(ctx) {
|
|
|
256
288
|
if (entry.role && requiredSet.has(entry.role)) continue;
|
|
257
289
|
|
|
258
290
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(entry.el) : 'html';
|
|
259
|
-
const html = helpers.getOuterHtmlSnippet
|
|
291
|
+
const html = helpers.getOuterHtmlSnippet
|
|
292
|
+
? helpers.getOuterHtmlSnippet(entry.el)
|
|
293
|
+
: entry.el.outerHTML || '';
|
|
260
294
|
const containerSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
261
295
|
|
|
262
296
|
const isRoleless = !entry.role;
|
|
@@ -313,7 +347,12 @@ function runInPage(ctx) {
|
|
|
313
347
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
314
348
|
}
|
|
315
349
|
if (occurrences.length) {
|
|
316
|
-
return {
|
|
350
|
+
return {
|
|
351
|
+
ruleId: rule.ruleId,
|
|
352
|
+
outcome: 'fail',
|
|
353
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
354
|
+
occurrences
|
|
355
|
+
};
|
|
317
356
|
}
|
|
318
357
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
319
358
|
}
|
|
@@ -50,7 +50,8 @@ const id = 'aria-required-attr';
|
|
|
50
50
|
|
|
51
51
|
const meta = {
|
|
52
52
|
title: 'Roles with a required ARIA state/property must carry it',
|
|
53
|
-
description:
|
|
53
|
+
description:
|
|
54
|
+
'Checks that elements with an explicit role carry every unambiguous, context-independent required aria-* state/property for that role (e.g. role="checkbox" must have aria-checked).',
|
|
54
55
|
i18n: {
|
|
55
56
|
titleKey: 'ariaRequiredAttr_title',
|
|
56
57
|
descriptionKey: 'ariaRequiredAttr_description'
|
|
@@ -59,7 +60,13 @@ const meta = {
|
|
|
59
60
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
60
61
|
wcagSc: ['4.1.2'],
|
|
61
62
|
normativeMappings: [
|
|
62
|
-
{
|
|
63
|
+
{
|
|
64
|
+
standard: 'WCAG',
|
|
65
|
+
version: '2.2',
|
|
66
|
+
requirement: '4.1.2',
|
|
67
|
+
title: 'Name, Role, Value',
|
|
68
|
+
conformanceLevel: 'A'
|
|
69
|
+
}
|
|
63
70
|
],
|
|
64
71
|
defaultSeverity: 'serious',
|
|
65
72
|
category: 'robust',
|
|
@@ -69,7 +76,7 @@ const meta = {
|
|
|
69
76
|
};
|
|
70
77
|
|
|
71
78
|
function runInPage(ctx) {
|
|
72
|
-
const {
|
|
79
|
+
const { helpers, rule } = ctx;
|
|
73
80
|
|
|
74
81
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
75
82
|
if (!ariaHelpers) {
|
|
@@ -77,7 +84,8 @@ function runInPage(ctx) {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
function isEligibleAcc(el) {
|
|
80
|
-
const fn =
|
|
87
|
+
const fn =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
81
89
|
if (!fn) return true;
|
|
82
90
|
try {
|
|
83
91
|
const r = fn(el, ctx);
|
|
@@ -93,7 +101,9 @@ function runInPage(ctx) {
|
|
|
93
101
|
return v != null && String(v).trim().toLowerCase() === 'true';
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
const nodes = helpers.queryAllSmart
|
|
104
|
+
const nodes = helpers.queryAllSmart
|
|
105
|
+
? helpers.queryAllSmart('[role]')
|
|
106
|
+
: helpers.queryAll('[role]');
|
|
97
107
|
|
|
98
108
|
const occurrences = [];
|
|
99
109
|
let applicableCount = 0;
|
|
@@ -121,7 +131,7 @@ function runInPage(ctx) {
|
|
|
121
131
|
if (!missing.length) continue;
|
|
122
132
|
|
|
123
133
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
124
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
134
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
125
135
|
|
|
126
136
|
for (const attr of missing) {
|
|
127
137
|
occurrences.push({
|
|
@@ -145,9 +155,14 @@ function runInPage(ctx) {
|
|
|
145
155
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
146
156
|
}
|
|
147
157
|
if (occurrences.length) {
|
|
148
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
ruleId: rule.ruleId,
|
|
160
|
+
outcome: 'fail',
|
|
161
|
+
severity: rule.defaultSeverity || 'serious',
|
|
162
|
+
occurrences
|
|
163
|
+
};
|
|
149
164
|
}
|
|
150
165
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
module.exports = { id, meta, runInPage };
|
|
168
|
+
module.exports = { id, meta, runInPage };
|