@surea11y/core 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -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/package.json +50 -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 +15 -51
- 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 +4 -3
- 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 +2 -0
- 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 +2 -0
- package/src/checks/automatic/avoid-inline-spacing.js +3 -2
- package/src/checks/automatic/binary-control-name-present.js +2 -0
- package/src/checks/automatic/button-name-present.js +7 -7
- package/src/checks/automatic/bypass-blocks-present.js +9 -7
- package/src/checks/automatic/canvas-text-alternative-present.js +2 -0
- package/src/checks/automatic/combobox-name-present.js +2 -0
- 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 +10 -10
- 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 +2 -0
- package/src/checks/automatic/form-control-single-label.js +6 -7
- 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 +7 -9
- package/src/checks/automatic/input-image-alt-present.js +4 -6
- package/src/checks/automatic/label-in-name.js +15 -19
- package/src/checks/automatic/language-page-present.js +2 -0
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +2 -0
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +2 -0
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +2 -0
- package/src/checks/automatic/meta-refresh-no-exceptions.js +4 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +2 -0
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +2 -0
- package/src/checks/automatic/meter-name-present.js +4 -3
- package/src/checks/automatic/nested-interactive-controls-absent.js +27 -5
- package/src/checks/automatic/object-text-alternative-present.js +2 -0
- package/src/checks/automatic/option-name-present.js +2 -0
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +8 -10
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +2 -0
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +2 -0
- package/src/checks/automatic/spinbutton-name-present.js +2 -0
- package/src/checks/automatic/summary-name-present.js +2 -0
- 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 +2 -0
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +2 -0
- 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 +2 -0
- package/src/checks/automatic/tooltip-name-present.js +2 -0
- package/src/checks/automatic/treeitem-name-present.js +2 -0
- package/src/checks/automatic/valid-lang.js +2 -0
- 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/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 +2 -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 +54 -47
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +42 -26
- 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 +6772 -2158
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +18 -3
- package/surea11y.browser.js +2731 -999
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- 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
|
/**
|
|
@@ -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: {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -20,86 +22,54 @@
|
|
|
20
22
|
* owned set. Nothing else is a structurally valid direct child of a
|
|
21
23
|
* composite/container role.
|
|
22
24
|
* @implementation-notes
|
|
23
|
-
* - A distinct atomic decision from aria-required-children (see
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* container
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* engine's own already-existing, shared `helpers.getFocusableInfo` for
|
|
51
|
-
* the focusability half. Both signals (global-attribute presence,
|
|
52
|
-
* focusability) are static, declarative markup facts with no live-DOM/
|
|
53
|
-
* hydration risk, unlike e.g. `aria-checked-state-mismatch`'s DOM-
|
|
54
|
-
* property comparison.
|
|
55
|
-
* - Fixed 2026-07-30: a roleless-but-focusable descendant's message and
|
|
56
|
-
* `data.details.attr` used to always claim "carries tabindex" even when
|
|
57
|
-
* the element had no tabindex attribute at all and was only focusable
|
|
58
|
-
* natively (e.g. an <a href> link). `helpers.getFocusableInfo`'s
|
|
59
|
-
* `mechanism` field ('tabindex' | 'native' | ...) is now used to tell
|
|
60
|
-
* the two apart, with a distinct `nativeFocusable` attr/message for the
|
|
61
|
-
* native case. Found via a real Angular app: a routerLink <a> inside a
|
|
62
|
-
* role="list" was reported as "carries tabindex" though the rendered
|
|
63
|
-
* markup had no such attribute.
|
|
64
|
-
* - Recursion stops at the first non-transparent role boundary, same as
|
|
65
|
-
* that reference engine: a nested container with its own real role (e.g. a
|
|
66
|
-
* <div role="listbox"> inside a menubar) is evaluated as ITS OWN
|
|
67
|
-
* owned-role entry against the outer container (and, separately, gets
|
|
68
|
-
* its own applicability pass as a container in the same rule run) —
|
|
69
|
-
* its descendants are never misattributed to the outer container.
|
|
70
|
-
* - Fixed 2026-07-31: child-role resolution used `ariaHelpers.getExplicitRole`
|
|
71
|
-
* (explicit role="" attribute only), unlike aria-required-children's
|
|
72
|
-
* descendant matching which uses `ariaHelpers.getContainmentRole` (explicit
|
|
25
|
+
* - A distinct atomic decision from aria-required-children (see that
|
|
26
|
+
* rule): "does at least one required child exist" vs "is every owned
|
|
27
|
+
* child one of the allowed roles" — split per this repo's "one rule =
|
|
28
|
+
* one normative decision" principle.
|
|
29
|
+
* - The "allowed owned roles" set is exactly REQUIRED_OWNED_ROLES, not a
|
|
30
|
+
* separately authored, broader list: an owned element is allowed only if
|
|
31
|
+
* its role is literally in the container's required set.
|
|
32
|
+
* - A ROLELESS descendant that has any global WAI-ARIA attribute or is
|
|
33
|
+
* focusable is also flagged: it's treated as an owned entry with
|
|
34
|
+
* `role: null`, which can never match a container's required-owned-roles
|
|
35
|
+
* set, so it's always "unallowed". Both signals
|
|
36
|
+
* (global-attribute presence, focusability via the shared
|
|
37
|
+
* `helpers.getFocusableInfo`) are static, declarative markup facts with
|
|
38
|
+
* no live-DOM/hydration risk, unlike e.g.
|
|
39
|
+
* `aria-checked-state-mismatch`'s DOM-property comparison.
|
|
40
|
+
* `helpers.getFocusableInfo`'s `mechanism` field ('tabindex' | 'native' |
|
|
41
|
+
* ...) distinguishes an explicit `tabindex` attribute from native
|
|
42
|
+
* focusability (e.g. an `<a href>`), so the reported `data.details.attr`
|
|
43
|
+
* and message correctly say `nativeFocusable` rather than claiming a
|
|
44
|
+
* tabindex attribute that isn't actually present in the markup.
|
|
45
|
+
* - Recursion stops at the first non-transparent role boundary: a nested
|
|
46
|
+
* container with its own real role
|
|
47
|
+
* (e.g. a `<div role="listbox">` inside a menubar) is evaluated as its
|
|
48
|
+
* own owned-role entry against the outer container (and, separately,
|
|
49
|
+
* gets its own applicability pass as a container in the same rule run)
|
|
50
|
+
* — its descendants are never misattributed to the outer container.
|
|
51
|
+
* - Child-role resolution uses `ariaHelpers.getContainmentRole` (explicit
|
|
73
52
|
* role, falling back to the native-tag map — li/tr/td/th/tbody/ul/ol/
|
|
74
|
-
* table/select/input[type=radio]
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* without re-deriving it, and that reasoning turned out not to hold —
|
|
95
|
-
* a closed dialog/flyout menu populated on open is a real false-positive
|
|
96
|
-
* shape. In this rule specifically the descendant-level eligibility gate
|
|
97
|
-
* already made the container-level gate redundant for correctness (an
|
|
98
|
-
* ineligible container has no eligible descendants either, so `owned`
|
|
99
|
-
* ends up empty and nothing fails) — but skipping the container up front
|
|
100
|
-
* reports `notApplicable` instead of a vacuous `pass`, which is the more
|
|
101
|
-
* accurate outcome for a container that isn't currently exposed at all,
|
|
102
|
-
* and avoids walking a subtree whose result is already known.
|
|
53
|
+
* table/select/input[type=radio]), the same resolution
|
|
54
|
+
* aria-required-children's descendant matching uses — not
|
|
55
|
+
* `getExplicitRole`, which only sees an explicit role="" attribute. A
|
|
56
|
+
* bare `<li>` with no role="" (the common CSS-reset workaround
|
|
57
|
+
* `<ul role="list"><li>...</li></ul>`) must still resolve to the
|
|
58
|
+
* implicit listitem role so the walk stops at that boundary instead of
|
|
59
|
+
* recursing straight through it into the listitem's own subtree; the
|
|
60
|
+
* same applies to every container role in REQUIRED_OWNED_ROLES whose
|
|
61
|
+
* native-tag counterpart the child map covers (e.g. a bare
|
|
62
|
+
* `<tr>`/`<td>` under a role="table"/"grid"/"row" container).
|
|
63
|
+
* - Gated on isAccTreeEligible for the container itself, matching
|
|
64
|
+
* aria-required-children: a closed dialog/flyout menu populated on open
|
|
65
|
+
* is a real false-positive shape otherwise. The descendant-level
|
|
66
|
+
* eligibility gate alone would make the container-level gate redundant
|
|
67
|
+
* for correctness (an ineligible container has no eligible descendants
|
|
68
|
+
* either, so `owned` ends up empty and nothing fails), but skipping the
|
|
69
|
+
* container up front reports `notApplicable` instead of a vacuous
|
|
70
|
+
* `pass` — the more accurate outcome for a container that isn't
|
|
71
|
+
* currently exposed at all — and avoids walking a subtree whose result
|
|
72
|
+
* is already known.
|
|
103
73
|
* - No aria-busy exemption here (unlike aria-required-children): the
|
|
104
74
|
* WAI-ARIA spec's aria-busy escape hatch is specifically about a
|
|
105
75
|
* container missing its required owned elements while loading, not
|
|
@@ -161,7 +131,7 @@ function runInPage(ctx) {
|
|
|
161
131
|
// aria-allowed-attr.js's GLOBAL_ATTRS — duplicated, not imported, since
|
|
162
132
|
// runInPage must be self-contained per scripts/build-core.js). A
|
|
163
133
|
// roleless descendant carrying any of these is a real accessible-tree
|
|
164
|
-
// node
|
|
134
|
+
// node, not a transparent wrapper.
|
|
165
135
|
const GLOBAL_ARIA_ATTRS = [
|
|
166
136
|
'aria-atomic',
|
|
167
137
|
'aria-braillelabel',
|
|
@@ -205,13 +175,12 @@ function runInPage(ctx) {
|
|
|
205
175
|
// non-transparent role boundary otherwise — see header comment. A
|
|
206
176
|
// roleless descendant is ALSO a non-transparent boundary (an owned
|
|
207
177
|
// entry with role: null, which can never satisfy a required-role set)
|
|
208
|
-
// when it carries a global aria-* attribute or is focusable
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
// wrapper the walk should pass through.
|
|
178
|
+
// when it carries a global aria-* attribute or is focusable.
|
|
179
|
+
// kidRole comes from getContainmentRole, not getExplicitRole: "roleless"
|
|
180
|
+
// here means neither an explicit role="" NOR one of the native
|
|
181
|
+
// containment tags (li, tr, td, ...), so a bare <li>/<tr>/... is a real
|
|
182
|
+
// listitem/row boundary, not a transparent wrapper the walk should pass
|
|
183
|
+
// through.
|
|
215
184
|
function collectOwnedRoles(el, requiredSet, out, depth) {
|
|
216
185
|
if (depth > MAX_DEPTH) return;
|
|
217
186
|
const kids = el.children ? Array.prototype.slice.call(el.children) : [];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -18,13 +20,10 @@
|
|
|
18
20
|
* - Deliberately scoped to REQUIRED_PROPS_BY_ROLE in src/core/aria-helpers.js,
|
|
19
21
|
* which only lists a required property when the spec is unambiguous and
|
|
20
22
|
* context-independent — see that file's header for the rationale.
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* that engine itself excludes progressbar from its own table for this reason)
|
|
26
|
-
* and `combobox`'s `aria-controls` (confirmed via MDN's combobox role
|
|
27
|
-
* page to be conditional — only required once the popup is actually
|
|
23
|
+
* - `meter`'s `aria-valuenow` is required. Deliberately NOT required:
|
|
24
|
+
* `progressbar`'s `aria-valuenow` (a legitimately indeterminate
|
|
25
|
+
* progressbar omits it) and `combobox`'s `aria-controls` (conditional per
|
|
26
|
+
* MDN's combobox role page — only required once the popup is actually
|
|
28
27
|
* displayed, not unconditionally). See src/core/aria-helpers.js's
|
|
29
28
|
* REQUIRED_PROPS_BY_ROLE comment for the full reasoning.
|
|
30
29
|
* - Gated on isAccTreeEligible for the element itself: unlike a syntax-
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -42,9 +44,7 @@
|
|
|
42
44
|
* widget is missing required owned elements due to script execution or
|
|
43
45
|
* loading, authors MUST mark a containing element with aria-busy equal
|
|
44
46
|
* to true." A container carrying aria-busy="true" is skipped the same
|
|
45
|
-
* way — only the exact string "true" counts (absent/"false" do not)
|
|
46
|
-
* matching a widely-used reference engine's own aria-required-children
|
|
47
|
-
* behavior.
|
|
47
|
+
* way — only the exact string "true" counts (absent/"false" do not).
|
|
48
48
|
* - Descendant search tries a fast native querySelectorAll(CANDIDATE_
|
|
49
49
|
* SELECTOR) first (covers the light-DOM-only common case with no added
|
|
50
50
|
* cost); only when that finds nothing AND the container has a <slot>
|
|
@@ -53,8 +53,7 @@
|
|
|
53
53
|
* querySelectorAll only sees a <slot>'s unrendered fallback content, never
|
|
54
54
|
* what's actually distributed into it. Deliberately scoped to slot
|
|
55
55
|
* expansion only, not a general "also descend into any nested custom
|
|
56
|
-
* element's own shadow root" walk — no
|
|
57
|
-
* that yet.
|
|
56
|
+
* element's own shadow root" walk — no known case needs that yet.
|
|
58
57
|
*/
|
|
59
58
|
|
|
60
59
|
const id = 'aria-required-children';
|
|
@@ -133,17 +132,15 @@ function runInPage(ctx) {
|
|
|
133
132
|
// <slot></slot>, with the actual role="listitem" elements living in the
|
|
134
133
|
// light DOM and projected in) would never find them there — same class
|
|
135
134
|
// of bug as aria-required-parent's ancestor search, just in the opposite
|
|
136
|
-
// (descendant) direction.
|
|
137
|
-
// sp-sidenav-item: its shadow root's role="list" div owns its listitems
|
|
138
|
-
// only through slot projection.
|
|
135
|
+
// (descendant) direction.
|
|
139
136
|
//
|
|
140
137
|
// Deliberately scoped to slot expansion only — does NOT separately
|
|
141
138
|
// descend into an unrelated nested custom element's own shadow root
|
|
142
139
|
// (e.g. a <my-widget> child with no <slot> involvement at all). That's a
|
|
143
140
|
// qualitatively different question (does an arbitrary component's own
|
|
144
141
|
// internal structure count as this container's "owned children"?) with
|
|
145
|
-
// no
|
|
146
|
-
//
|
|
142
|
+
// no known case driving it yet; slot projection is the shape that
|
|
143
|
+
// actually comes up.
|
|
147
144
|
function collectComposedDescendants(node, out, seen, limit) {
|
|
148
145
|
if (!node || !node.children) return;
|
|
149
146
|
for (const child of Array.from(node.children)) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -94,24 +96,20 @@ function runInPage(ctx) {
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
// Roles that may host a nested listitem/treeitem group without breaking
|
|
97
|
-
// the required-context chain
|
|
98
|
-
//
|
|
99
|
-
// roles via its ownGroupRoles option).
|
|
99
|
+
// the required-context chain — the group role is transparent for exactly
|
|
100
|
+
// these two roles.
|
|
100
101
|
const GROUP_TRANSPARENT_FOR_ROLES = new Set(['listitem', 'treeitem']);
|
|
101
102
|
|
|
102
103
|
// A real ancestor role — not "no role at all" and not the two roles that
|
|
103
104
|
// strip an element from the accessibility tree's parent/child chain
|
|
104
|
-
// entirely (presentation/none) — stops the search
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// <ul role="tablist">: that reference engine correctly fails this (the tablist is never the
|
|
113
|
-
// tab's accessible-tree parent, listitem is), which the old "walk every
|
|
114
|
-
// ancestor" version here missed entirely.
|
|
105
|
+
// entirely (presentation/none) — stops the search. This is stricter than
|
|
106
|
+
// "any ancestor with the right role anywhere up the tree": the
|
|
107
|
+
// required-context relationship is about the accessibility tree's actual
|
|
108
|
+
// PARENT, so an intervening ancestor with its OWN distinct real role
|
|
109
|
+
// (e.g. a plain <li>'s native "listitem" role) blocks the search even if
|
|
110
|
+
// a further-up ancestor has the correct role. E.g. a <button role="tab">
|
|
111
|
+
// inside a plain <li> (native listitem) inside <ul role="tablist"> fails:
|
|
112
|
+
// the tablist is never the tab's accessible-tree parent, the listitem is.
|
|
115
113
|
function getRealContextRole(el) {
|
|
116
114
|
const role = ariaHelpers.getContainmentRole(el);
|
|
117
115
|
if (!role || role === 'presentation' || role === 'none') return '';
|
|
@@ -121,13 +119,11 @@ function runInPage(ctx) {
|
|
|
121
119
|
// Flat-tree ancestor walk (ctx.helpers.composedParent — assignedSlot wins
|
|
122
120
|
// over parentNode, then shadow host). A slotted light-DOM element's real
|
|
123
121
|
// rendered ancestor is whatever the shadow tree wraps its <slot> in (e.g.
|
|
124
|
-
// a role="list" container), not its own light-DOM parentElement
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// shadow tree that has no further light-DOM parent — skip those and keep
|
|
130
|
-
// climbing rather than treating them as a (roleless) context.
|
|
122
|
+
// a role="list" container), not its own light-DOM parentElement.
|
|
123
|
+
// composedParent can return a non-Element node (a ShadowRoot, nodeType
|
|
124
|
+
// 11) when climbing out of a shadow tree that has no further light-DOM
|
|
125
|
+
// parent — skip those and keep climbing rather than treating them as a
|
|
126
|
+
// (roleless) context.
|
|
131
127
|
const getComposedParent =
|
|
132
128
|
helpers && typeof helpers.composedParent === 'function'
|
|
133
129
|
? helpers.composedParent
|
|
@@ -142,10 +138,9 @@ function runInPage(ctx) {
|
|
|
142
138
|
// Mutable working copy: passing a transparent "group" ancestor also
|
|
143
139
|
// makes the element's OWN role an acceptable context from that point on
|
|
144
140
|
// (a nested treeitem-under-group-under-treeitem chain is a normal,
|
|
145
|
-
// arbitrarily-deep ARIA tree/list, not just one level)
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// once per element in runInPage) is never mutated.
|
|
141
|
+
// arbitrarily-deep ARIA tree/list, not just one level). Cloned lazily
|
|
142
|
+
// so the caller's Set (built once per element in runInPage) is never
|
|
143
|
+
// mutated.
|
|
149
144
|
let roles = acceptableRoles;
|
|
150
145
|
while (cur && guard++ < 200) {
|
|
151
146
|
if (cur.nodeType !== 1) {
|