@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
|
/**
|
|
@@ -20,10 +22,9 @@
|
|
|
20
22
|
* technology — activating the outer control and the inner one become
|
|
21
23
|
* ambiguous, and some AT only exposes one of the two.
|
|
22
24
|
* @implementation-notes
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* outside this element").
|
|
25
|
+
* - Reports on the outer (containing) control, not the nested descendant —
|
|
26
|
+
* this engine considers the container the fixable unit ("move the nested
|
|
27
|
+
* control outside this element").
|
|
27
28
|
* - A container can be reported once even with multiple nested
|
|
28
29
|
* descendants (listed together); a deeply nested chain (A > B > C, all
|
|
29
30
|
* interactive) reports both A and B as separate occurrences, since each
|
|
@@ -89,6 +90,19 @@ function runInPage(ctx) {
|
|
|
89
90
|
'[role="treeitem"]'
|
|
90
91
|
].join(', ');
|
|
91
92
|
|
|
93
|
+
const isAccTreeEligible =
|
|
94
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
95
|
+
|
|
96
|
+
function isEligible(node) {
|
|
97
|
+
if (!isAccTreeEligible) return true;
|
|
98
|
+
try {
|
|
99
|
+
const r = isAccTreeEligible(node, ctx);
|
|
100
|
+
return !!(r && r.eligible);
|
|
101
|
+
} catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
const nodes = helpers.queryAllSmart
|
|
93
107
|
? helpers.queryAllSmart(INTERACTIVE_SELECTOR)
|
|
94
108
|
: helpers.queryAll(INTERACTIVE_SELECTOR);
|
|
@@ -98,12 +112,20 @@ function runInPage(ctx) {
|
|
|
98
112
|
|
|
99
113
|
for (const el of nodes) {
|
|
100
114
|
if (!el || !el.querySelectorAll) continue;
|
|
115
|
+
if (!isEligible(el)) continue;
|
|
101
116
|
|
|
102
117
|
applicableCount += 1;
|
|
103
118
|
|
|
119
|
+
// The nested search uses the raw native querySelectorAll (not
|
|
120
|
+
// helpers.queryAllSmart), so it isn't subject to that helper's
|
|
121
|
+
// default hidden-content policy at all -- not even hard CSS-based
|
|
122
|
+
// hiding, let alone aria-hidden. A nested descendant that is never
|
|
123
|
+
// actually rendered or exposed to AT (display:none, aria-hidden,
|
|
124
|
+
// etc.) creates no real ambiguity for ANY user, since it isn't there
|
|
125
|
+
// to be confused with the outer control.
|
|
104
126
|
let nested;
|
|
105
127
|
try {
|
|
106
|
-
nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
|
|
128
|
+
nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR)).filter(isEligible);
|
|
107
129
|
} catch {
|
|
108
130
|
nested = [];
|
|
109
131
|
}
|
|
@@ -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';
|
|
@@ -96,16 +98,12 @@ function runInPage(ctx) {
|
|
|
96
98
|
const title = getAttr(el, 'title');
|
|
97
99
|
if (title) return { ok: true, method: 'title' };
|
|
98
100
|
|
|
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.
|
|
101
|
+
// role="progressbar" is name-from-author-only per WAI-ARIA: aria-label,
|
|
102
|
+
// aria-labelledby, or title — no content-based naming method at all. It
|
|
103
|
+
// must NOT fall back to subtree content: e.g. a <ul role="progressbar">
|
|
104
|
+
// loading skeleton with no name of its own, whose nested descendants
|
|
105
|
+
// carry their own aria-label for a different purpose — falling back to
|
|
106
|
+
// subtree content would wrongly treat that as the progressbar's name.
|
|
109
107
|
return { ok: false, method: 'none' };
|
|
110
108
|
}
|
|
111
109
|
|
|
@@ -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
|
/**
|
|
@@ -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
|
/**
|
|
@@ -9,7 +11,12 @@
|
|
|
9
11
|
* @applicability
|
|
10
12
|
* Applies to inline <svg> elements that are exposed to assistive technologies AND appear intended to be conveyed.
|
|
11
13
|
* "Intended to be conveyed" is approximated deterministically by at least one of:
|
|
12
|
-
* - role="img"
|
|
14
|
+
* - role="img", role="graphics-symbol", or role="graphics-document"
|
|
15
|
+
* on the SVG root element itself (the WAI-ARIA Graphics Module
|
|
16
|
+
* roles, alongside img). Deliberately does NOT extend to arbitrary
|
|
17
|
+
* role="graphics-symbol" descendants nested inside an <svg> — this
|
|
18
|
+
* check's scope is the <svg> root only; a broader feature, not
|
|
19
|
+
* attempted here.
|
|
13
20
|
* - aria-label / aria-labelledby present
|
|
14
21
|
* - <title> or <desc> present (desc alone is an applicability signal only — see @expectation)
|
|
15
22
|
* - focusable/tabbable (e.g., tabindex, native focusability)
|
|
@@ -23,10 +30,9 @@
|
|
|
23
30
|
* - an ARIA name (aria-label / aria-labelledby).
|
|
24
31
|
* A <desc> element alone does NOT satisfy this — per the SVG Accessibility
|
|
25
32
|
* API Mappings spec §7.1, <desc> only ever contributes to the accessible
|
|
26
|
-
* DESCRIPTION, never the accessible NAME
|
|
27
|
-
* against the SVG-AAM spec text, not assumed). An <svg> with only a
|
|
33
|
+
* DESCRIPTION, never the accessible NAME. An <svg> with only a
|
|
28
34
|
* <desc> and no <title>/ARIA name is still "applicable" (desc signals
|
|
29
|
-
* authorial intent) but fails
|
|
35
|
+
* authorial intent) but fails.
|
|
30
36
|
*/
|
|
31
37
|
|
|
32
38
|
const id = 'svg-text-alternative-present';
|
|
@@ -222,7 +228,13 @@ function runInPage(ctx) {
|
|
|
222
228
|
const descText = titleText ? '' : nonEmptyDescText(el); // avoid second scan if title already passes
|
|
223
229
|
const hasTitleOrDesc = !!(titleText || descText);
|
|
224
230
|
|
|
225
|
-
const hasIntent =
|
|
231
|
+
const hasIntent =
|
|
232
|
+
role === 'img' ||
|
|
233
|
+
role === 'graphics-symbol' ||
|
|
234
|
+
role === 'graphics-document' ||
|
|
235
|
+
hasAriaNamingAttr ||
|
|
236
|
+
hasTitleOrDesc ||
|
|
237
|
+
focusable;
|
|
226
238
|
|
|
227
239
|
if (!hasIntent) continue;
|
|
228
240
|
|
|
@@ -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
|
* the referencing cell, and (d) is not the cell itself.
|
|
15
17
|
* @implementation-notes
|
|
16
18
|
* - One occurrence per offending cell (not per bad token), listing every
|
|
17
|
-
* invalid reference
|
|
18
|
-
* td-headers-attr reporting granularity.
|
|
19
|
+
* invalid reference.
|
|
19
20
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
20
21
|
* property, while engine-level hidden-subtree filtering still applies
|
|
21
22
|
* unless engineOptions.includeHiddenElements is true.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -591,6 +593,7 @@ function runInPage(ctx) {
|
|
|
591
593
|
cantTellOccurrences.push({
|
|
592
594
|
selector: buildSelector(it.el),
|
|
593
595
|
html: htmlSnippet(it.el),
|
|
596
|
+
occurrenceOutcome: 'cantTell',
|
|
594
597
|
summary:
|
|
595
598
|
'Target may be too small and too close to another target, but the overlap is near the detection threshold and could not be confidently measured.',
|
|
596
599
|
hint: 'Manually verify the effective spacing between this target and its neighbor; increase target size or spacing if the overlap is real.',
|
|
@@ -619,6 +622,7 @@ function runInPage(ctx) {
|
|
|
619
622
|
cantTellOccurrences.push({
|
|
620
623
|
selector: buildSelector(it.el),
|
|
621
624
|
html: htmlSnippet(it.el),
|
|
625
|
+
occurrenceOutcome: 'cantTell',
|
|
622
626
|
summary:
|
|
623
627
|
'Target is too small and too close to another target, but may be exempt as part of an essential graphic or image-map region.',
|
|
624
628
|
hint: 'Verify whether this target’s size is genuinely essential to its function (e.g. part of an SVG/canvas/image map); if not, increase target size or spacing.',
|
|
@@ -642,6 +646,7 @@ function runInPage(ctx) {
|
|
|
642
646
|
failOccurrences.push({
|
|
643
647
|
selector: buildSelector(it.el),
|
|
644
648
|
html: htmlSnippet(it.el),
|
|
649
|
+
occurrenceOutcome: 'fail',
|
|
645
650
|
summary: 'Target is too small and too close to another target.',
|
|
646
651
|
hint: 'Increase target size to at least 24 by 24 CSS pixels, or add sufficient spacing.',
|
|
647
652
|
i18n: {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -69,6 +71,19 @@ function runInPage(ctx) {
|
|
|
69
71
|
return (v == null ? '' : String(v)).trim();
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
const isAccTreeEligible =
|
|
75
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
76
|
+
|
|
77
|
+
function isEligible(node) {
|
|
78
|
+
if (!isAccTreeEligible) return true;
|
|
79
|
+
try {
|
|
80
|
+
const r = isAccTreeEligible(node, ctx);
|
|
81
|
+
return !!(r && r.eligible);
|
|
82
|
+
} catch {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
|
|
73
88
|
|
|
74
89
|
const occurrences = [];
|
|
@@ -95,8 +110,12 @@ function runInPage(ctx) {
|
|
|
95
110
|
|
|
96
111
|
applicableCount += 1;
|
|
97
112
|
|
|
113
|
+
// An aria-hidden <th> is removed from the accessibility tree entirely
|
|
114
|
+
// -- a screen reader never announces it, so it can't actually serve as
|
|
115
|
+
// another cell's row/column header, even though it's still structurally
|
|
116
|
+
// a <th>.
|
|
98
117
|
function isHeaderCell(cell) {
|
|
99
|
-
return !!(cell && cell.tagName && cell.tagName.toLowerCase() === 'th');
|
|
118
|
+
return !!(cell && cell.tagName && cell.tagName.toLowerCase() === 'th' && isEligible(cell));
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
function hasColumnHeaderAbove(r, c) {
|
|
@@ -121,6 +140,10 @@ function runInPage(ctx) {
|
|
|
121
140
|
const cell = cells[c];
|
|
122
141
|
if (!cell || isHeaderCell(cell)) continue;
|
|
123
142
|
|
|
143
|
+
// An aria-hidden data cell isn't exposed to AT either, so it has
|
|
144
|
+
// no need for an accessible header association.
|
|
145
|
+
if (!isEligible(cell)) continue;
|
|
146
|
+
|
|
124
147
|
const headersAttr = trim(cell.getAttribute('headers'));
|
|
125
148
|
if (headersAttr) continue;
|
|
126
149
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check accesskeys
|
|
5
7
|
* @atomic true
|
|
6
8
|
* @summary accesskey values must be unique on the page
|
|
7
|
-
* @standard Best Practices (
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
|
|
8
10
|
* @applicability
|
|
9
11
|
* Applies whenever two or more elements share the same non-empty
|
|
10
12
|
* accesskey attribute value (case-insensitive).
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -14,34 +16,23 @@
|
|
|
14
16
|
* checked state is already exposed natively), but when an author sets
|
|
15
17
|
* it explicitly it should agree with the element's actual state —
|
|
16
18
|
* otherwise assistive technology is told something different from what
|
|
17
|
-
* a sighted user perceives.
|
|
18
|
-
* a widely-used reference engine's own `aria-conditional-attr` rule
|
|
19
|
-
* performs (verified directly against its `ariaConditionalCheckboxAttr`/
|
|
20
|
-
* `ariaConditionalRadioAttr` source).
|
|
19
|
+
* a sighted user perceives.
|
|
21
20
|
* @implementation-notes
|
|
22
21
|
* - Deliberately authored as `type: 'manual'` (cantTell-capped, never
|
|
23
|
-
* fail), unlike most ARIA-validity rules in this file family.
|
|
24
|
-
*
|
|
25
|
-
* mismatch it reports is a genuine, current-state fact. This engine
|
|
26
|
-
* analyzes STATIC markup only (no script execution) — `.checked`
|
|
22
|
+
* fail), unlike most ARIA-validity rules in this file family. This
|
|
23
|
+
* engine analyzes STATIC markup only (no script execution) — `.checked`
|
|
27
24
|
* reliably reflects the static `checked` attribute for freshly-parsed
|
|
28
|
-
* markup
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* corpus is full of. Flagging for human review (cantTell) captures the
|
|
36
|
-
* same signal without the false-positive risk of asserting it as a
|
|
37
|
-
* confirmed violation.
|
|
25
|
+
* markup, but a very common, entirely legitimate real-world pattern is a
|
|
26
|
+
* JS-hydrated widget whose server-rendered HTML intentionally ships
|
|
27
|
+
* `aria-checked="true"` (or `"mixed"`) BEFORE client JS sets the native
|
|
28
|
+
* `checked`/`indeterminate` property to match on hydration. A hard
|
|
29
|
+
* `fail` here would misfire on that pattern constantly. Flagging for
|
|
30
|
+
* human review (cantTell) captures the same signal without the
|
|
31
|
+
* false-positive risk of asserting it as a confirmed violation.
|
|
38
32
|
* - `aria-checked="mixed"` is only meaningful for checkboxes (radio
|
|
39
|
-
* buttons have no indeterminate state)
|
|
40
|
-
* own `normalizeAriaChecked` (radio) vs `normalizeAriaChecked2` (checkbox)
|
|
41
|
-
* split, verified directly against its source.
|
|
33
|
+
* buttons have no indeterminate state).
|
|
42
34
|
* - Any `aria-checked` value other than "true"/"false"/"mixed" (checkbox)
|
|
43
|
-
* or "true"/"false" (radio) is treated as equivalent to "false"
|
|
44
|
-
* matches that reference engine's own normalization exactly.
|
|
35
|
+
* or "true"/"false" (radio) is treated as equivalent to "false".
|
|
45
36
|
* - Not rule-gated on isAccTreeEligible: whether the accessible state
|
|
46
37
|
* matches is still a markup-correctness property, while engine-level
|
|
47
38
|
* hidden-subtree filtering applies unless engineOptions.includeHiddenElements
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check aria-text
|
|
5
7
|
* @atomic true
|
|
6
8
|
* @summary role="text" elements should have no focusable descendants
|
|
7
|
-
* @standard Best Practices (
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
|
|
8
10
|
* @applicability
|
|
9
11
|
* Elements with an explicit `role="text"`.
|
|
10
12
|
* @expectation
|
|
@@ -15,10 +17,9 @@
|
|
|
15
17
|
* focusable content — a focusable descendant inside a "this is just
|
|
16
18
|
* text" region is unreachable or confusing for keyboard/AT users.
|
|
17
19
|
* @implementation-notes
|
|
18
|
-
* - Not WCAG-normative
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* rationale).
|
|
20
|
+
* - Not WCAG-normative — authored as an advisory, cantTell-capped
|
|
21
|
+
* `type: 'manual'` rule, matching the Tier 1b precedent (see
|
|
22
|
+
* `landmark-unique`'s header comment for the shared rationale).
|
|
22
23
|
* - "Focusable descendant" is a presence check (link/button/form
|
|
23
24
|
* control/`[tabindex]`/`iframe`/`[contenteditable]`), the same
|
|
24
25
|
* conservative selector `scrollable-region-focusable` uses, not a full
|