@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
|
/**
|
|
@@ -7,17 +9,19 @@
|
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 1.4.4
|
|
9
11
|
* @applicability
|
|
10
|
-
* Applies to <meta name="viewport"> elements
|
|
11
|
-
*
|
|
12
|
+
* Applies to <meta name="viewport"> elements whose content attribute sets
|
|
13
|
+
* maximum-scale or user-scalable. Content setting neither cannot restrict
|
|
14
|
+
* zoom.
|
|
12
15
|
* @expectation
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* user-scalable is absent, yes, device-width, device-height, or a number
|
|
17
|
+
* outside the range -1 to 1; and maximum-scale is absent, device-width,
|
|
18
|
+
* device-height, negative, or 2 or more. Anything else stops the user
|
|
19
|
+
* zooming text to 200%, which WCAG 1.4.4 (Resize Text) requires.
|
|
17
20
|
* @implementation-notes
|
|
18
|
-
* -
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* - An unparseable value counts as a restriction, because CSS Device
|
|
22
|
+
* Adaptation translates it to 0: maximum-scale=yes disables zoom exactly
|
|
23
|
+
* as maximum-scale=0 does. A negative maximum-scale is out of range and
|
|
24
|
+
* dropped by the browser, so it restricts nothing and passes.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
const id = 'meta-viewport-zoom-enabled';
|
|
@@ -87,20 +91,40 @@ function runInPage(ctx) {
|
|
|
87
91
|
const raw = String(el.getAttribute('content') || '').trim();
|
|
88
92
|
if (!raw) continue;
|
|
89
93
|
|
|
90
|
-
applicableCount += 1;
|
|
91
|
-
|
|
92
94
|
const parsed = parseContent(raw);
|
|
95
|
+
|
|
96
|
+
// ACT b4f0c3 applies only when content carries maximum-scale or
|
|
97
|
+
// user-scalable; content that sets neither cannot restrict zoom.
|
|
98
|
+
if (parsed['user-scalable'] === undefined && parsed['maximum-scale'] === undefined) continue;
|
|
99
|
+
|
|
100
|
+
applicableCount += 1;
|
|
93
101
|
const reasons = [];
|
|
94
102
|
|
|
103
|
+
// CSS Device Adaptation translates an unparseable value to 0, so
|
|
104
|
+
// user-scalable=invalid and maximum-scale=yes disable zoom just as
|
|
105
|
+
// user-scalable=no does. A negative maximum-scale is out of range and
|
|
106
|
+
// dropped instead, which is why it does not restrict anything.
|
|
95
107
|
const userScalable = parsed['user-scalable'];
|
|
96
|
-
if (
|
|
97
|
-
|
|
108
|
+
if (
|
|
109
|
+
userScalable !== undefined &&
|
|
110
|
+
userScalable !== 'yes' &&
|
|
111
|
+
userScalable !== 'device-width' &&
|
|
112
|
+
userScalable !== 'device-height'
|
|
113
|
+
) {
|
|
114
|
+
const scale = parseFloat(userScalable);
|
|
115
|
+
if (Number.isNaN(scale) || (scale > -1 && scale < 1)) {
|
|
116
|
+
reasons.push('user-scalable=' + userScalable);
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
const maxScaleRaw = parsed['maximum-scale'];
|
|
101
|
-
if (
|
|
121
|
+
if (
|
|
122
|
+
maxScaleRaw !== undefined &&
|
|
123
|
+
maxScaleRaw !== 'device-width' &&
|
|
124
|
+
maxScaleRaw !== 'device-height'
|
|
125
|
+
) {
|
|
102
126
|
const maxScale = parseFloat(maxScaleRaw);
|
|
103
|
-
if (
|
|
127
|
+
if (Number.isNaN(maxScale) || (maxScale >= 0 && maxScale < 2)) {
|
|
104
128
|
reasons.push('maximum-scale=' + maxScaleRaw);
|
|
105
129
|
}
|
|
106
130
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'meter-name-present';
|
|
@@ -65,9 +67,16 @@ function runInPage(ctx) {
|
|
|
65
67
|
return '';
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
71
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
72
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
68
73
|
function isEligibleAcc(helpers, el, ctx) {
|
|
69
74
|
const fn =
|
|
70
|
-
helpers && typeof helpers.
|
|
75
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
76
|
+
? helpers.isIncludedInAccessibilityTree
|
|
77
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
78
|
+
? helpers.isAccTreeEligible
|
|
79
|
+
: null;
|
|
71
80
|
if (!fn) return true;
|
|
72
81
|
try {
|
|
73
82
|
const r = fn(el, ctx);
|
|
@@ -96,9 +105,8 @@ function runInPage(ctx) {
|
|
|
96
105
|
const title = getAttr(el, 'title');
|
|
97
106
|
if (title) return { ok: true, method: 'title' };
|
|
98
107
|
|
|
99
|
-
// role="meter" is name-from-author-only per WAI-ARIA
|
|
100
|
-
//
|
|
101
|
-
// 'aria-labelledby', title] — no content-based naming method at all).
|
|
108
|
+
// role="meter" is name-from-author-only per WAI-ARIA: aria-label,
|
|
109
|
+
// aria-labelledby, or title — no content-based naming method at all.
|
|
102
110
|
// It must NOT fall back to subtree content — visible text near/inside a
|
|
103
111
|
// custom meter widget is not reliably exposed as its accessible name.
|
|
104
112
|
return { ok: false, method: 'none' };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -11,23 +13,48 @@
|
|
|
11
13
|
* a[href], button, input (not hidden), select, textarea; or an explicit
|
|
12
14
|
* ARIA widget role: button, link, checkbox, radio, switch, tab, textbox,
|
|
13
15
|
* combobox, listbox, menuitem, menuitemcheckbox, menuitemradio, option,
|
|
14
|
-
* slider, spinbutton, searchbox, treeitem).
|
|
16
|
+
* slider, spinbutton, searchbox, treeitem). The container is applicable
|
|
17
|
+
* regardless of whether it is itself focusable — focusability is only
|
|
18
|
+
* used to decide whether a *descendant* nests an interactive control.
|
|
15
19
|
* @expectation
|
|
16
|
-
* The element does not contain, as a descendant, another
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* The element does not contain, as a descendant, another *operable*
|
|
21
|
+
* interactive control (e.g. a <button> wrapping a <select>, or a link
|
|
22
|
+
* containing a checkbox). Nested interactive controls are not reliably
|
|
23
|
+
* announced or operable via assistive technology — activating the outer
|
|
24
|
+
* control and the inner one become ambiguous, and some AT only exposes
|
|
25
|
+
* one of the two.
|
|
22
26
|
* @implementation-notes
|
|
23
|
-
* -
|
|
24
|
-
* (
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* - A descendant counts as a nested interactive control only when it is both
|
|
28
|
+
* (a) a native interactive element or an explicit ARIA widget role and
|
|
29
|
+
* (b) operable: exposed to the accessibility tree and platform-focusable
|
|
30
|
+
* (natively focusable, or focusable via tabindex), per
|
|
31
|
+
* helpers.getFocusableInfo, which accounts for contenteditable, :disabled,
|
|
32
|
+
* inert, and invalid/negative tabindex.
|
|
33
|
+
* - A widget-role child that its container drives rather than exposes as its
|
|
34
|
+
* own focus target creates no operability ambiguity and does not count:
|
|
35
|
+
* a role="option" inside a listbox/combobox, a role="tab" inside a tablist,
|
|
36
|
+
* a role="treeitem" inside a tree, a role="menuitem" inside a menu, or a
|
|
37
|
+
* role="radio" inside a radiogroup. Composites manage such children with one
|
|
38
|
+
* of two focus models, and both are exempt: aria-activedescendant (the child
|
|
39
|
+
* has no tabindex and is not focusable) and roving tabindex (the active
|
|
40
|
+
* child carries tabindex="0" and the rest tabindex="-1"). Under roving
|
|
41
|
+
* tabindex the owned child IS platform-focusable, so focusability alone
|
|
42
|
+
* cannot be the discriminator; membership in the container's owned-child set
|
|
43
|
+
* is what exempts it. A control that is itself focusable while nested in a
|
|
44
|
+
* control it does NOT belong to still counts: a <button> inside an <a href>,
|
|
45
|
+
* a focusable role="button" inside a link, an editable field inside a
|
|
46
|
+
* control, or an orphan role="option" with no owning listbox/combobox.
|
|
47
|
+
* - A disabled native control (<button disabled>, <input disabled>) and an
|
|
48
|
+
* inert descendant are not focusable, so they do not nest an interactive
|
|
49
|
+
* control; a display:none or aria-hidden descendant is not exposed to AT
|
|
50
|
+
* and is likewise out of scope. aria-disabled leaves an element focusable,
|
|
51
|
+
* so it still counts.
|
|
52
|
+
* - Occurrences report the outer (containing) control, the fixable unit
|
|
53
|
+
* ("move the nested control outside this element"), not the descendant.
|
|
54
|
+
* - Only the shallowest operable descendants are attributed to a container.
|
|
55
|
+
* In a chain A > B > C where all three are operable, A is reported for B
|
|
56
|
+
* and B for C; A is not reported for C, whose nesting belongs to its
|
|
57
|
+
* nearest operable ancestor B.
|
|
31
58
|
*/
|
|
32
59
|
|
|
33
60
|
const id = 'nested-interactive-controls-absent';
|
|
@@ -89,6 +116,137 @@ function runInPage(ctx) {
|
|
|
89
116
|
'[role="treeitem"]'
|
|
90
117
|
].join(', ');
|
|
91
118
|
|
|
119
|
+
const isAccTreeEligible =
|
|
120
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
121
|
+
const getFocusableInfo =
|
|
122
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
123
|
+
|
|
124
|
+
function isEligible(node) {
|
|
125
|
+
if (!isAccTreeEligible) return true;
|
|
126
|
+
try {
|
|
127
|
+
const r = isAccTreeEligible(node, ctx);
|
|
128
|
+
return !!(r && r.eligible);
|
|
129
|
+
} catch {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function matchesInteractive(node) {
|
|
135
|
+
return !!(
|
|
136
|
+
node &&
|
|
137
|
+
node.nodeType === 1 &&
|
|
138
|
+
typeof node.matches === 'function' &&
|
|
139
|
+
node.matches(INTERACTIVE_SELECTOR)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A composite widget owns children of a fixed role and drives their focus
|
|
144
|
+
// itself (via roving tabindex or aria-activedescendant). The roving-tabindex
|
|
145
|
+
// model gives the active child tabindex="0" and the rest tabindex="-1", so
|
|
146
|
+
// the owned children ARE platform-focusable even though the container, not
|
|
147
|
+
// the child, is the operable unit. Such a child is part of the composite,
|
|
148
|
+
// not a control nested inside another control, so it must not be reported
|
|
149
|
+
// regardless of its tabindex. The map is keyed by the child role and lists
|
|
150
|
+
// the container roles that legitimately own it.
|
|
151
|
+
const COMPOSITE_CHILD_CONTAINERS = {
|
|
152
|
+
option: ['listbox', 'combobox'],
|
|
153
|
+
tab: ['tablist'],
|
|
154
|
+
treeitem: ['tree'],
|
|
155
|
+
menuitem: ['menu', 'menubar'],
|
|
156
|
+
menuitemcheckbox: ['menu', 'menubar'],
|
|
157
|
+
menuitemradio: ['menu', 'menubar'],
|
|
158
|
+
radio: ['radiogroup']
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function getExplicitRole(node) {
|
|
162
|
+
if (!node || node.nodeType !== 1 || typeof node.getAttribute !== 'function') return '';
|
|
163
|
+
const raw = node.getAttribute('role');
|
|
164
|
+
if (!raw) return '';
|
|
165
|
+
// role accepts a space-separated fallback list; the first token wins.
|
|
166
|
+
const first = raw.trim().split(/\s+/)[0];
|
|
167
|
+
return first ? first.toLowerCase() : '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parentElementOf(node) {
|
|
171
|
+
if (!node) return null;
|
|
172
|
+
if (node.parentElement) return node.parentElement;
|
|
173
|
+
const p = node.parentNode;
|
|
174
|
+
return p && p.nodeType === 1 ? p : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// True when `node` is an owned child of a composite widget: its role is a
|
|
178
|
+
// composite-child role and a matching container role is present on one of
|
|
179
|
+
// its ancestors. The roving tab stop (tabindex="0") such a container places
|
|
180
|
+
// on its active child does not make that child an independently nested
|
|
181
|
+
// control.
|
|
182
|
+
function isManagedCompositeChild(node) {
|
|
183
|
+
const role = getExplicitRole(node);
|
|
184
|
+
const containers = COMPOSITE_CHILD_CONTAINERS[role];
|
|
185
|
+
if (!containers) return false;
|
|
186
|
+
let p = parentElementOf(node);
|
|
187
|
+
while (p && p.nodeType === 1) {
|
|
188
|
+
if (containers.indexOf(getExplicitRole(p)) !== -1) return true;
|
|
189
|
+
p = parentElementOf(p);
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Operable = a native-interactive / ARIA-widget element that is exposed to
|
|
195
|
+
// the accessibility tree and platform-focusable. A widget-role element with
|
|
196
|
+
// no independent focus (e.g. a role="option" with no tabindex, driven by
|
|
197
|
+
// its container via roving tabindex or aria-activedescendant) is not
|
|
198
|
+
// operable and does not nest an interactive control. When no focus model is
|
|
199
|
+
// available, role membership alone qualifies. (Composite-owned children are
|
|
200
|
+
// handled earlier as attribution boundaries in collectNestedOperable, so
|
|
201
|
+
// they never reach here.)
|
|
202
|
+
function isOperableInteractive(node) {
|
|
203
|
+
if (!matchesInteractive(node)) return false;
|
|
204
|
+
if (!isEligible(node)) return false;
|
|
205
|
+
if (!getFocusableInfo) return true;
|
|
206
|
+
try {
|
|
207
|
+
const info = getFocusableInfo(node, ctx);
|
|
208
|
+
return !!(info && info.focusable);
|
|
209
|
+
} catch {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Shallowest operable interactive descendants of `root`. Traversal stops at
|
|
215
|
+
// each counted node instead of descending into it, so a control nested
|
|
216
|
+
// inside another operable control is attributed to its nearest operable
|
|
217
|
+
// ancestor rather than to every enclosing container. Eligibility is applied
|
|
218
|
+
// per node during the walk, so hidden or aria-hidden subtrees drop out.
|
|
219
|
+
function collectNestedOperable(root) {
|
|
220
|
+
const out = [];
|
|
221
|
+
const top = root && root.children;
|
|
222
|
+
if (!top || !top.length) return out;
|
|
223
|
+
const stack = [];
|
|
224
|
+
for (let i = top.length - 1; i >= 0; i--) stack.push(top[i]);
|
|
225
|
+
while (stack.length) {
|
|
226
|
+
const node = stack.pop();
|
|
227
|
+
if (node && node.nodeType === 1) {
|
|
228
|
+
// A composite-owned child (option in a listbox/combobox, tab in a
|
|
229
|
+
// tablist, ...) is not a nested interactive control: its container
|
|
230
|
+
// owns it and drives its focus (roving tabindex or
|
|
231
|
+
// aria-activedescendant). Treat it as an attribution boundary — do not
|
|
232
|
+
// count it, and do not descend past it. Any control genuinely nested
|
|
233
|
+
// inside it is attributed to the child itself (examined as its own
|
|
234
|
+
// container in the main loop), keeping the report at the nearest
|
|
235
|
+
// interactive ancestor rather than bubbling up to the composite.
|
|
236
|
+
if (isManagedCompositeChild(node)) continue;
|
|
237
|
+
if (isOperableInteractive(node)) {
|
|
238
|
+
out.push(node);
|
|
239
|
+
continue; // do not descend into a counted control
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const kids = node && node.children;
|
|
243
|
+
if (kids && kids.length) {
|
|
244
|
+
for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
92
250
|
const nodes = helpers.queryAllSmart
|
|
93
251
|
? helpers.queryAllSmart(INTERACTIVE_SELECTOR)
|
|
94
252
|
: helpers.queryAll(INTERACTIVE_SELECTOR);
|
|
@@ -97,22 +255,16 @@ function runInPage(ctx) {
|
|
|
97
255
|
let applicableCount = 0;
|
|
98
256
|
|
|
99
257
|
for (const el of nodes) {
|
|
100
|
-
if (!el ||
|
|
258
|
+
if (!el || el.nodeType !== 1) continue;
|
|
259
|
+
if (!isEligible(el)) continue;
|
|
101
260
|
|
|
102
261
|
applicableCount += 1;
|
|
103
262
|
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
|
|
107
|
-
} catch {
|
|
108
|
-
nested = [];
|
|
109
|
-
}
|
|
263
|
+
const nested = collectNestedOperable(el);
|
|
110
264
|
if (!nested.length) continue;
|
|
111
265
|
|
|
112
266
|
const nestedTags = nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown'));
|
|
113
|
-
const dedupedNestedTags = [
|
|
114
|
-
...new Set(nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown')))
|
|
115
|
-
];
|
|
267
|
+
const dedupedNestedTags = [...new Set(nestedTags)];
|
|
116
268
|
|
|
117
269
|
const tag = el.tagName.toLowerCase();
|
|
118
270
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -7,9 +9,8 @@
|
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 1.1.1
|
|
9
11
|
* @applicability
|
|
10
|
-
* Applies to <object> elements
|
|
11
|
-
*
|
|
12
|
-
* if they are tabbable or referenced by IDREF relationships (per engine eligibility checks).
|
|
12
|
+
* Applies to <object> elements included in the accessibility tree.
|
|
13
|
+
* Hidden objects are excluded whether or not they are focusable.
|
|
13
14
|
* role="presentation"/role="none" are excluded only when not focusable.
|
|
14
15
|
* @expectation
|
|
15
16
|
* Each applicable <object> provides a text alternative via:
|
|
@@ -69,8 +70,15 @@ function runInPage(ctx) {
|
|
|
69
70
|
const getEligibilityInfo =
|
|
70
71
|
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
// ACT 8fc3b6 applies only to objects included in the accessibility tree, so a
|
|
74
|
+
// focusable object inside aria-hidden is out of scope; aria-hidden-focus
|
|
75
|
+
// reports that markup instead.
|
|
76
|
+
const isEligibleHelper =
|
|
77
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
78
|
+
? helpers.isIncludedInAccessibilityTree
|
|
79
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
80
|
+
? helpers.isAccTreeEligible
|
|
81
|
+
: null;
|
|
74
82
|
|
|
75
83
|
const getFocusableInfo =
|
|
76
84
|
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
@@ -146,14 +154,15 @@ function runInPage(ctx) {
|
|
|
146
154
|
for (const el of objects) {
|
|
147
155
|
if (!el || !el.getAttribute) continue;
|
|
148
156
|
|
|
149
|
-
if (
|
|
157
|
+
if (isEligibleHelper) {
|
|
150
158
|
const elig = (() => {
|
|
151
159
|
try {
|
|
152
|
-
return
|
|
160
|
+
return isEligibleHelper(el, ctx);
|
|
153
161
|
} catch {
|
|
154
162
|
return { eligible: true, reasons: [] };
|
|
155
163
|
}
|
|
156
164
|
})();
|
|
165
|
+
if (elig === false) continue;
|
|
157
166
|
if (elig && elig.eligible === false) continue;
|
|
158
167
|
}
|
|
159
168
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'option-name-present';
|
|
@@ -81,9 +83,16 @@ function runInPage(ctx) {
|
|
|
81
83
|
return '';
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
87
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
88
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
84
89
|
function isEligibleAcc(helpers, el, ctx) {
|
|
85
90
|
const fn =
|
|
86
|
-
helpers && typeof helpers.
|
|
91
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
92
|
+
? helpers.isIncludedInAccessibilityTree
|
|
93
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
94
|
+
? helpers.isAccTreeEligible
|
|
95
|
+
: null;
|
|
87
96
|
if (!fn) return true;
|
|
88
97
|
try {
|
|
89
98
|
const r = fn(el, ctx);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'progressbar-name-present';
|
|
@@ -65,9 +67,16 @@ function runInPage(ctx) {
|
|
|
65
67
|
return '';
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
71
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
72
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
68
73
|
function isEligibleAcc(helpers, el, ctx) {
|
|
69
74
|
const fn =
|
|
70
|
-
helpers && typeof helpers.
|
|
75
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
76
|
+
? helpers.isIncludedInAccessibilityTree
|
|
77
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
78
|
+
? helpers.isAccTreeEligible
|
|
79
|
+
: null;
|
|
71
80
|
if (!fn) return true;
|
|
72
81
|
try {
|
|
73
82
|
const r = fn(el, ctx);
|
|
@@ -96,16 +105,12 @@ function runInPage(ctx) {
|
|
|
96
105
|
const title = getAttr(el, 'title');
|
|
97
106
|
if (title) return { ok: true, method: 'title' };
|
|
98
107
|
|
|
99
|
-
// role="progressbar" is name-from-author-only per WAI-ARIA
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// aria-label="Loading Box" (for a completely different purpose) —
|
|
106
|
-
// falling back to subtree content picked that up and wrongly treated
|
|
107
|
-
// it as the progressbar's own name, when that reference engine correctly
|
|
108
|
-
// still flags it as unnamed.
|
|
108
|
+
// role="progressbar" is name-from-author-only per WAI-ARIA: aria-label,
|
|
109
|
+
// aria-labelledby, or title — no content-based naming method at all. It
|
|
110
|
+
// must NOT fall back to subtree content: e.g. a <ul role="progressbar">
|
|
111
|
+
// loading skeleton with no name of its own, whose nested descendants
|
|
112
|
+
// carry their own aria-label for a different purpose — falling back to
|
|
113
|
+
// subtree content would wrongly treat that as the progressbar's name.
|
|
109
114
|
return { ok: false, method: 'none' };
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -14,8 +16,7 @@
|
|
|
14
16
|
* Each applicable element with role="img" has an accessible text alternative:
|
|
15
17
|
* - aria-label with a non-empty value; OR
|
|
16
18
|
* - aria-labelledby referencing at least one existing element that contributes non-empty text; OR
|
|
17
|
-
* - a non-empty title attribute (last-resort accessible-name source per HTML-AAM
|
|
18
|
-
* also accepted by a widely-used reference engine's equivalent role-img-alt rule).
|
|
19
|
+
* - a non-empty title attribute (last-resort accessible-name source per HTML-AAM).
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const id = 'role-img-text-alternative-present';
|
|
@@ -152,8 +153,7 @@ function runInPage(ctx) {
|
|
|
152
153
|
const hasValidAriaLabel = hasAriaLabelAttr && ariaLabel.length > 0;
|
|
153
154
|
const hasValidAriaLabelledbyAttr = hasAriaLabelledbyAttr && ariaLabelledby.length > 0;
|
|
154
155
|
|
|
155
|
-
// Last-resort naming mechanism per HTML-AAM
|
|
156
|
-
// a widely-used reference engine's role-img-alt): a non-empty title attribute.
|
|
156
|
+
// Last-resort naming mechanism per HTML-AAM: a non-empty title attribute.
|
|
157
157
|
const titleRaw = (() => {
|
|
158
158
|
try {
|
|
159
159
|
return el.getAttribute('title');
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'searchbox-name-present';
|
|
@@ -107,9 +109,16 @@ function runInPage(ctx) {
|
|
|
107
109
|
return '';
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
113
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
114
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
110
115
|
function isEligibleAcc(helpers, el, ctx) {
|
|
111
116
|
const fn =
|
|
112
|
-
helpers && typeof helpers.
|
|
117
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
118
|
+
? helpers.isIncludedInAccessibilityTree
|
|
119
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
120
|
+
? helpers.isAccTreeEligible
|
|
121
|
+
: null;
|
|
113
122
|
if (!fn) return true;
|
|
114
123
|
try {
|
|
115
124
|
const r = fn(el, ctx);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -16,9 +18,8 @@
|
|
|
16
18
|
* users. Client-side image maps (<map>/<area>, each with real href/alt)
|
|
17
19
|
* are the accessible alternative and are not flagged by this rule.
|
|
18
20
|
* @implementation-notes
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
* alternative" exists elsewhere on the page).
|
|
21
|
+
* - Presence of ismap is itself the violation (there is no automatable way
|
|
22
|
+
* to verify a "usable alternative" exists elsewhere on the page).
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
const id = 'server-side-image-map-absent';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'slider-name-present';
|
|
@@ -123,9 +125,16 @@ function runInPage(ctx) {
|
|
|
123
125
|
return '';
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
129
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
130
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
126
131
|
function isEligibleAcc(helpers, el, ctx) {
|
|
127
132
|
const fn =
|
|
128
|
-
helpers && typeof helpers.
|
|
133
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
134
|
+
? helpers.isIncludedInAccessibilityTree
|
|
135
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
136
|
+
? helpers.isAccTreeEligible
|
|
137
|
+
: null;
|
|
129
138
|
if (!fn) return true;
|
|
130
139
|
try {
|
|
131
140
|
const r = fn(el, ctx);
|
|
@@ -139,7 +148,9 @@ function runInPage(ctx) {
|
|
|
139
148
|
const occurrences = [];
|
|
140
149
|
let applicableCount = 0;
|
|
141
150
|
|
|
142
|
-
|
|
151
|
+
// Native input[type=range] belongs to
|
|
152
|
+
// form-control-programmatic-label-present.
|
|
153
|
+
const selector = '[role="slider"]';
|
|
143
154
|
const nodes = helpers.queryAllSmart
|
|
144
155
|
? helpers.queryAllSmart(selector)
|
|
145
156
|
: helpers.queryAll(selector);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'spinbutton-name-present';
|
|
@@ -107,9 +109,16 @@ function runInPage(ctx) {
|
|
|
107
109
|
return '';
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
113
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
114
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
110
115
|
function isEligibleAcc(helpers, el, ctx) {
|
|
111
116
|
const fn =
|
|
112
|
-
helpers && typeof helpers.
|
|
117
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
118
|
+
? helpers.isIncludedInAccessibilityTree
|
|
119
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
120
|
+
? helpers.isAccTreeEligible
|
|
121
|
+
: null;
|
|
113
122
|
if (!fn) return true;
|
|
114
123
|
try {
|
|
115
124
|
const r = fn(el, ctx);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'summary-name-present';
|
|
@@ -81,9 +83,16 @@ function runInPage(ctx) {
|
|
|
81
83
|
return '';
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
87
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
88
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
84
89
|
function isEligibleAcc(helpers, el, ctx) {
|
|
85
90
|
const fn =
|
|
86
|
-
helpers && typeof helpers.
|
|
91
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
92
|
+
? helpers.isIncludedInAccessibilityTree
|
|
93
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
94
|
+
? helpers.isAccTreeEligible
|
|
95
|
+
: null;
|
|
87
96
|
if (!fn) return true;
|
|
88
97
|
try {
|
|
89
98
|
const r = fn(el, ctx);
|