@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
|
/**
|
|
@@ -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
|
const id = 'tab-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
|
/**
|
|
@@ -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
|
/**
|
|
@@ -7,7 +9,10 @@
|
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 1.3.1
|
|
9
11
|
* @applicability
|
|
10
|
-
* Applies to <table> elements that
|
|
12
|
+
* Applies to <table> elements that keep their table semantics and are included
|
|
13
|
+
* in the accessibility tree, and that contain at least one <th> which is
|
|
14
|
+
* visible, included in the accessibility tree, and not overridden by an
|
|
15
|
+
* explicit role other than rowheader/columnheader.
|
|
11
16
|
* @expectation
|
|
12
17
|
* The table also contains at least one <td> somewhere in it.
|
|
13
18
|
* @implementation-notes
|
|
@@ -28,9 +33,10 @@
|
|
|
28
33
|
* if some particular <th> in it doesn't actually describe any cell
|
|
29
34
|
* (false negative, not a false positive — acceptable under this
|
|
30
35
|
* engine's philosophy).
|
|
31
|
-
* -
|
|
32
|
-
*
|
|
33
|
-
*
|
|
36
|
+
* - Applicability IS gated, because the zero-<td> case above turns any
|
|
37
|
+
* over-broad scope straight into a false fail: a layout table marked
|
|
38
|
+
* role="presentation" with a stray <th> and no <td> was being failed,
|
|
39
|
+
* as were tables whose only header was hidden or carried role="cell".
|
|
34
40
|
*/
|
|
35
41
|
|
|
36
42
|
const id = 'table-th-has-data-cells';
|
|
@@ -70,16 +76,73 @@ function runInPage(ctx) {
|
|
|
70
76
|
const occurrences = [];
|
|
71
77
|
let applicableCount = 0;
|
|
72
78
|
|
|
79
|
+
function explicitRole(el) {
|
|
80
|
+
try {
|
|
81
|
+
return String((el && el.getAttribute && el.getAttribute('role')) || '')
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.split(/\s+/)[0];
|
|
85
|
+
} catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isIncludedInTree(el) {
|
|
91
|
+
const fn =
|
|
92
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
93
|
+
? helpers.isIncludedInAccessibilityTree
|
|
94
|
+
: null;
|
|
95
|
+
if (!fn) return true;
|
|
96
|
+
try {
|
|
97
|
+
const r = fn(el, ctx);
|
|
98
|
+
return typeof r === 'boolean' ? r : !!(r && r.eligible);
|
|
99
|
+
} catch {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// role="presentation"/"none" removes the table role, and with it every
|
|
105
|
+
// header cell the role would have implied. A table is not focusable, so
|
|
106
|
+
// presentational role conflict resolution does not restore it.
|
|
107
|
+
function hasTableSemantics(table) {
|
|
108
|
+
const role = explicitRole(table);
|
|
109
|
+
if (role === 'presentation' || role === 'none') return false;
|
|
110
|
+
if (role && role !== 'table' && role !== 'grid' && role !== 'treegrid') return false;
|
|
111
|
+
return isIncludedInTree(table);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A <th> only carries a rowheader/columnheader role while nothing overrides
|
|
115
|
+
// it, and a hidden header describes nothing to a screen reader either way.
|
|
116
|
+
function isHeaderCellInScope(th) {
|
|
117
|
+
if (!th) return false;
|
|
118
|
+
const role = explicitRole(th);
|
|
119
|
+
if (role && role !== 'rowheader' && role !== 'columnheader') return false;
|
|
120
|
+
if (helpers.isDomVisibleEligible) {
|
|
121
|
+
try {
|
|
122
|
+
if (!helpers.isDomVisibleEligible(th, ctx, { targetSet: 'dom' }).eligible) return false;
|
|
123
|
+
} catch {
|
|
124
|
+
// fall through to the tree check
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return isIncludedInTree(th);
|
|
128
|
+
}
|
|
129
|
+
|
|
73
130
|
for (const table of tables) {
|
|
74
131
|
if (!table || !table.querySelectorAll) continue;
|
|
75
132
|
|
|
133
|
+
// A table stripped of its semantics has no header cells to describe
|
|
134
|
+
// anything, so nothing in it is in scope.
|
|
135
|
+
if (!hasTableSemantics(table)) continue;
|
|
136
|
+
|
|
76
137
|
let ths;
|
|
77
138
|
try {
|
|
78
139
|
ths = table.querySelectorAll('th');
|
|
79
140
|
} catch {
|
|
80
141
|
ths = [];
|
|
81
142
|
}
|
|
82
|
-
|
|
143
|
+
|
|
144
|
+
const headers = Array.from(ths).filter(isHeaderCellInScope);
|
|
145
|
+
if (!headers.length) continue;
|
|
83
146
|
|
|
84
147
|
applicableCount += 1;
|
|
85
148
|
|
|
@@ -91,7 +154,7 @@ function runInPage(ctx) {
|
|
|
91
154
|
}
|
|
92
155
|
if (hasDataCell) continue;
|
|
93
156
|
|
|
94
|
-
for (const th of
|
|
157
|
+
for (const th of headers) {
|
|
95
158
|
if (!th) continue;
|
|
96
159
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(th) : 'html';
|
|
97
160
|
const html = helpers.getOuterHtmlSnippet
|
|
@@ -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,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'textbox-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 = 'tooltip-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 = 'treeitem-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
|
/**
|
|
@@ -56,7 +58,13 @@ const meta = {
|
|
|
56
58
|
function runInPage(ctx) {
|
|
57
59
|
const { helpers, rule } = ctx;
|
|
58
60
|
|
|
61
|
+
// Shape alone accepts unregistered tags such as "eng" and "em-US", so the
|
|
62
|
+
// primary subtag is checked against the IANA registry via the shared helper.
|
|
59
63
|
const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
64
|
+
const isValidTag =
|
|
65
|
+
typeof helpers.isValidLanguageTag === 'function'
|
|
66
|
+
? helpers.isValidLanguageTag
|
|
67
|
+
: (v) => BCP47_RE.test(String(v || ''));
|
|
60
68
|
|
|
61
69
|
const nodes = helpers.queryAllSmart
|
|
62
70
|
? helpers.queryAllSmart('[lang]')
|
|
@@ -69,12 +77,19 @@ function runInPage(ctx) {
|
|
|
69
77
|
if (!el || !el.getAttribute) continue;
|
|
70
78
|
if (el.tagName && el.tagName.toLowerCase() === 'html') continue;
|
|
71
79
|
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
80
|
+
const rawAttr = el.getAttribute('lang');
|
|
81
|
+
if (rawAttr === null || rawAttr === '') continue; // ACT de46e4: empty is out of scope
|
|
82
|
+
|
|
83
|
+
// The rule applies only where text actually inherits the language, so an
|
|
84
|
+
// element with no text, or whose text reaches no one, has nothing to
|
|
85
|
+
// declare a language for.
|
|
86
|
+
if (!String(el.textContent || '').trim()) continue;
|
|
74
87
|
|
|
75
88
|
applicableCount += 1;
|
|
76
89
|
|
|
77
|
-
|
|
90
|
+
// A whitespace-only value is in scope and has no primary language tag.
|
|
91
|
+
const raw = String(rawAttr).trim();
|
|
92
|
+
if (isValidTag(raw)) continue;
|
|
78
93
|
|
|
79
94
|
const tag = el.tagName.toLowerCase();
|
|
80
95
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
@@ -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
|