@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
|
/**
|
|
@@ -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) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -65,19 +67,44 @@ function runInPage(ctx) {
|
|
|
65
67
|
const occurrences = [];
|
|
66
68
|
let applicableCount = 0;
|
|
67
69
|
|
|
70
|
+
// Programmatically hidden per the ACT glossary: display:none, visibility not
|
|
71
|
+
// visible, or aria-hidden on the element or an ancestor.
|
|
72
|
+
function isHidden(el) {
|
|
73
|
+
try {
|
|
74
|
+
if (typeof helpers.isDomVisibleEligible === 'function') {
|
|
75
|
+
if (!helpers.isDomVisibleEligible(el, ctx)) return true;
|
|
76
|
+
}
|
|
77
|
+
for (let n = el; n && n.getAttribute; n = n.parentElement) {
|
|
78
|
+
if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
for (const el of nodes) {
|
|
69
87
|
if (!el || !el.getAttribute) continue;
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
if (
|
|
89
|
+
// ACT 674b10 is not applicable to a programmatically hidden element.
|
|
90
|
+
if (isHidden(el)) continue;
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
// role takes a fallback list and the first token the browser recognises
|
|
93
|
+
// wins, so role="searchfield searchbox" resolves to searchbox. The rule
|
|
94
|
+
// fails only when no token names a concrete role.
|
|
95
|
+
const tokens =
|
|
96
|
+
typeof ariaHelpers.getAllRoleTokens === 'function'
|
|
97
|
+
? ariaHelpers.getAllRoleTokens(el)
|
|
98
|
+
: [ariaHelpers.getExplicitRole(el)].filter(Boolean);
|
|
99
|
+
if (!tokens.length) continue; // role="" or whitespace-only: not this rule's concern
|
|
75
100
|
|
|
76
|
-
|
|
77
|
-
const isKnown = ariaHelpers.isKnownRole(role);
|
|
101
|
+
applicableCount += 1;
|
|
78
102
|
|
|
79
|
-
|
|
103
|
+
const usable = tokens.find((t) => ariaHelpers.isKnownRole(t) && !ariaHelpers.isAbstractRole(t));
|
|
104
|
+
if (usable) continue;
|
|
80
105
|
|
|
106
|
+
const role = tokens[0];
|
|
107
|
+
const isKnown = tokens.some((t) => ariaHelpers.isKnownRole(t));
|
|
81
108
|
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
82
109
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
83
110
|
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -13,25 +15,26 @@
|
|
|
13
15
|
* @expectation
|
|
14
16
|
* Each attribute's value conforms to its WAI-ARIA-declared value type:
|
|
15
17
|
* boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
|
|
16
|
-
* from a fixed enumerated set, an integer, a real number, or
|
|
17
|
-
* ID reference (list) that resolves to an existing element in
|
|
18
|
-
* document.
|
|
18
|
+
* from a fixed enumerated set, an integer, a real number, or an empty
|
|
19
|
+
* value or ID reference (list) that resolves to an existing element in
|
|
20
|
+
* the document.
|
|
19
21
|
* @implementation-notes
|
|
20
22
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
21
23
|
* property, while engine-level hidden-subtree filtering still applies
|
|
22
24
|
* unless engineOptions.includeHiddenElements is true.
|
|
23
|
-
* - ID-reference resolution (
|
|
24
|
-
*
|
|
25
|
-
* aria-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
25
|
+
* - ID-reference resolution (see aria-helpers.js's idExists) only flags
|
|
26
|
+
* idref-list attributes (aria-labelledby, aria-describedby,
|
|
27
|
+
* aria-controls, aria-owns, etc.) when NONE of the space-separated ids
|
|
28
|
+
* resolve — a partially-dangling list (some ids exist, some don't) is
|
|
29
|
+
* left unflagged (only invalidate when every token fails to resolve).
|
|
30
|
+
* Single-idref attributes (aria-activedescendant, aria-errormessage) are
|
|
31
|
+
* flagged whenever their one id doesn't resolve.
|
|
32
|
+
* - An explicitly-EMPTY idref/idref-list value (e.g.
|
|
33
|
+
* `aria-describedby=""`) is valid, not a violation — `allowEmpty` holds
|
|
34
|
+
* for every idref/idref-list ARIA attribute. A common, deliberate pattern
|
|
35
|
+
* in templated markup (e.g. React conditionally rendering
|
|
36
|
+
* `aria-describedby={hasError ? errorId : ''}`); flagging it would be a
|
|
37
|
+
* false positive.
|
|
35
38
|
*/
|
|
36
39
|
|
|
37
40
|
const id = 'aria-valid-attr-value';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -128,7 +130,15 @@ function runInPage(ctx) {
|
|
|
128
130
|
if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
|
|
129
131
|
i += 1;
|
|
130
132
|
if (tokens[i] === 'shipping' || tokens[i] === 'billing') i += 1;
|
|
131
|
-
|
|
133
|
+
// A contact modality token is only allowed when the field that follows is
|
|
134
|
+
// a contact field, so "work photo" is invalid while "work email" is not.
|
|
135
|
+
if (CONTACT_MODALITY.has(tokens[i])) {
|
|
136
|
+
const next = tokens[i + 1];
|
|
137
|
+
const isContactField =
|
|
138
|
+
next === 'email' || next === 'impp' || next === 'tel' || (next || '').startsWith('tel-');
|
|
139
|
+
if (!isContactField) return false;
|
|
140
|
+
i += 1;
|
|
141
|
+
}
|
|
132
142
|
|
|
133
143
|
let end = tokens.length;
|
|
134
144
|
if (tokens[end - 1] === 'webauthn') end -= 1;
|
|
@@ -145,11 +155,39 @@ function runInPage(ctx) {
|
|
|
145
155
|
const occurrences = [];
|
|
146
156
|
let applicableCount = 0;
|
|
147
157
|
|
|
158
|
+
// ACT 73f2c2 exempts controls where the attribute cannot describe an input
|
|
159
|
+
// purpose: the on/off toggle, disabled controls, input types with a fixed
|
|
160
|
+
// value, and controls that take no input.
|
|
161
|
+
const FIXED_VALUE_TYPES = new Set([
|
|
162
|
+
'button',
|
|
163
|
+
'checkbox',
|
|
164
|
+
'file',
|
|
165
|
+
'image',
|
|
166
|
+
'radio',
|
|
167
|
+
'reset',
|
|
168
|
+
'submit'
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
function isExempt(el) {
|
|
172
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
173
|
+
if (tag === 'input') {
|
|
174
|
+
const type = String(el.getAttribute('type') || 'text').toLowerCase();
|
|
175
|
+
if (FIXED_VALUE_TYPES.has(type)) return true;
|
|
176
|
+
}
|
|
177
|
+
if (el.hasAttribute && el.hasAttribute('disabled')) return true;
|
|
178
|
+
if (String(el.getAttribute('aria-disabled') || '').toLowerCase() === 'true') return true;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
148
182
|
for (const el of nodes) {
|
|
149
183
|
if (!el || !el.getAttribute) continue;
|
|
150
184
|
const raw = String(el.getAttribute('autocomplete') || '').trim();
|
|
151
185
|
if (!raw) continue;
|
|
152
186
|
|
|
187
|
+
const tokens = raw.toLowerCase().split(/\s+/).filter(Boolean);
|
|
188
|
+
if (tokens.length === 1 && (tokens[0] === 'on' || tokens[0] === 'off')) continue;
|
|
189
|
+
if (isExempt(el)) continue;
|
|
190
|
+
|
|
153
191
|
applicableCount += 1;
|
|
154
192
|
|
|
155
193
|
if (isValidAutocomplete(raw)) continue;
|