@surea11y/core 1.4.1 → 1.6.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 +212 -128
- package/README.md +46 -9
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +4 -4
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +30 -14
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +29 -7
- package/docs/LIMITATIONS.md +6 -4
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +3 -1
- package/docs/RULE_AUTHORING.md +104 -23
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +4 -4
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +8 -7
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +95 -40
- package/src/checks/automatic/aria-allowed-role.js +16 -18
- package/src/checks/automatic/aria-braille-equivalent.js +19 -21
- package/src/checks/automatic/aria-conditional-attr.js +22 -24
- package/src/checks/automatic/aria-deprecated-role.js +63 -50
- package/src/checks/automatic/aria-hidden-body.js +4 -11
- package/src/checks/automatic/aria-hidden-focus.js +104 -23
- package/src/checks/automatic/aria-prohibited-attr.js +71 -72
- package/src/checks/automatic/aria-prohibited-children.js +154 -61
- package/src/checks/automatic/aria-required-attr.js +74 -29
- package/src/checks/automatic/aria-required-children.js +38 -34
- package/src/checks/automatic/aria-required-parent.js +78 -29
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +33 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -18
- package/src/checks/automatic/autocomplete-valid.js +17 -19
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +46 -26
- package/src/checks/automatic/button-name-present.js +115 -34
- package/src/checks/automatic/combobox-name-present.js +40 -22
- package/src/checks/automatic/contrast-computable.js +32 -0
- package/src/checks/automatic/contrast-enhanced.js +21 -1
- package/src/checks/automatic/contrast-minimum.js +21 -1
- package/src/checks/automatic/css-orientation-lock.js +118 -41
- package/src/checks/automatic/definition-list-children-valid.js +25 -29
- package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
- package/src/checks/automatic/dialog-name-present.js +36 -20
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +50 -40
- package/src/checks/automatic/duplicate-id.js +198 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +39 -41
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +92 -38
- package/src/checks/automatic/iframe-name-present.js +53 -21
- package/src/checks/automatic/iframe-title-unique.js +19 -24
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +198 -49
- package/src/checks/automatic/link-in-text-block.js +29 -31
- package/src/checks/automatic/link-name-present.js +47 -31
- package/src/checks/automatic/list-children-valid.js +21 -23
- package/src/checks/automatic/listbox-name-present.js +42 -24
- package/src/checks/automatic/listitem-parent-valid.js +18 -21
- package/src/checks/automatic/menuitem-name-present.js +36 -20
- package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
- package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +38 -21
- package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
- package/src/checks/automatic/option-name-present.js +39 -22
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +41 -24
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +46 -24
- package/src/checks/automatic/server-side-image-map-absent.js +16 -19
- package/src/checks/automatic/slider-name-present.js +42 -23
- package/src/checks/automatic/spinbutton-name-present.js +46 -24
- package/src/checks/automatic/summary-name-present.js +34 -20
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +37 -20
- package/src/checks/automatic/table-headers-attr-valid.js +57 -24
- package/src/checks/automatic/table-th-has-data-cells.js +76 -24
- package/src/checks/automatic/target-size-minimum.js +172 -131
- package/src/checks/automatic/td-has-header.js +20 -25
- package/src/checks/automatic/textbox-name-present.js +42 -24
- package/src/checks/automatic/tooltip-name-present.js +37 -20
- package/src/checks/automatic/treeitem-name-present.js +39 -22
- package/src/checks/automatic/valid-lang.js +107 -24
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +21 -22
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +23 -26
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +73 -28
- package/src/checks/manual/empty-table-header-manual.js +33 -36
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +453 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +4 -11
- package/src/checks/manual/heading-order-manual.js +20 -25
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +18 -21
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +19 -21
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
- package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
- package/src/checks/manual/landmark-one-main-manual.js +8 -15
- package/src/checks/manual/landmark-unique-manual.js +31 -36
- package/src/checks/manual/link-name-quality-manual.js +162 -35
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +16 -19
- package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +8 -15
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
- package/src/checks/manual/region-manual.js +32 -25
- package/src/checks/manual/scope-attr-valid-manual.js +16 -19
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +44 -52
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +16 -19
- package/src/checks/manual/table-duplicate-name-manual.js +16 -19
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +13086 -5169
- package/src/report.js +16 -2
- package/surea11y.browser.js +5665 -4219
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
- package/bin/surea11y-core.js +0 -20
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check tooltip-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="tooltip" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="tooltip" (the attribute must name
|
|
13
|
+
* that role alone, not a fallback list) that are included in the
|
|
14
|
+
* accessibility tree. An element with the matching implicit role but no
|
|
15
|
+
* role attribute is out of scope.
|
|
16
|
+
* @expectation
|
|
17
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
18
|
+
* aria-labelledby that resolves to non-empty text, from title, or,
|
|
19
|
+
* role="tooltip" being name-from-content, from its own subtree text,
|
|
20
|
+
* where a descendant's own name (an <img alt>, aria-label or title) counts
|
|
21
|
+
* as that descendant's contribution rather than only its text nodes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'tooltip-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
@@ -51,7 +70,7 @@ function runInPage(ctx) {
|
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
73
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
74
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
75
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
76
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -70,7 +89,7 @@ function runInPage(ctx) {
|
|
|
70
89
|
const raw = getAttr(el, 'aria-labelledby');
|
|
71
90
|
if (!raw) return '';
|
|
72
91
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
73
|
-
// name-from-content of the referenced element
|
|
92
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
74
93
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
75
94
|
// aria-labelledby target's only name source is its title attribute,
|
|
76
95
|
// which name-from-content alone can never see).
|
|
@@ -148,24 +167,22 @@ function runInPage(ctx) {
|
|
|
148
167
|
}
|
|
149
168
|
})()
|
|
150
169
|
: null;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
});
|
|
170
|
+
|
|
171
|
+
occurrences.push(
|
|
172
|
+
helpers.reportOccurrence(el, {
|
|
173
|
+
summary: 'This tooltip has no accessible name.',
|
|
174
|
+
hint: 'Provide tooltip text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
|
|
175
|
+
i18n: {
|
|
176
|
+
summaryKey: 'tooltipNamePresent_summary_fail',
|
|
177
|
+
hintKey: 'tooltipNamePresent_hint_fail',
|
|
178
|
+
params: {}
|
|
179
|
+
},
|
|
180
|
+
data: {
|
|
181
|
+
details: { reasonCode: 'name_missing', controlType: 'tooltip', methodTried: res.method },
|
|
182
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
);
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
if (applicableCount === 0) {
|
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check treeitem-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="treeitem" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="treeitem" (the attribute must name
|
|
13
|
+
* that role alone, not a fallback list) that are included in the
|
|
14
|
+
* accessibility tree. An element with the matching implicit role but no
|
|
15
|
+
* role attribute is out of scope.
|
|
16
|
+
* @expectation
|
|
17
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
18
|
+
* aria-labelledby that resolves to non-empty text, from title, or,
|
|
19
|
+
* role="treeitem" being name-from-content, from its own subtree text,
|
|
20
|
+
* where a descendant's own name (an <img alt>, aria-label or title) counts
|
|
21
|
+
* as that descendant's contribution rather than only its text nodes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'treeitem-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
27
|
+
title: 'Tree items have an accessible name',
|
|
28
|
+
description: 'Checks that elements with role="treeitem" expose a non-empty accessible name.',
|
|
10
29
|
i18n: {
|
|
11
30
|
titleKey: 'treeitemNamePresent_title',
|
|
12
31
|
descriptionKey: 'treeitemNamePresent_description'
|
|
@@ -51,7 +70,7 @@ function runInPage(ctx) {
|
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
73
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
74
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
75
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
76
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -70,7 +89,7 @@ function runInPage(ctx) {
|
|
|
70
89
|
const raw = getAttr(el, 'aria-labelledby');
|
|
71
90
|
if (!raw) return '';
|
|
72
91
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
73
|
-
// name-from-content of the referenced element
|
|
92
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
74
93
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
75
94
|
// aria-labelledby target's only name source is its title attribute,
|
|
76
95
|
// which name-from-content alone can never see).
|
|
@@ -145,24 +164,22 @@ function runInPage(ctx) {
|
|
|
145
164
|
}
|
|
146
165
|
})()
|
|
147
166
|
: null;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
});
|
|
167
|
+
|
|
168
|
+
occurrences.push(
|
|
169
|
+
helpers.reportOccurrence(el, {
|
|
170
|
+
summary: 'This element has no accessible name.',
|
|
171
|
+
hint: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
|
|
172
|
+
i18n: {
|
|
173
|
+
summaryKey: 'treeitemNamePresent_summary_fail',
|
|
174
|
+
hintKey: 'treeitemNamePresent_hint_fail',
|
|
175
|
+
params: { controlType: 'treeitem' }
|
|
176
|
+
},
|
|
177
|
+
data: {
|
|
178
|
+
details: { reasonCode: 'name_missing', controlType: 'treeitem', methodTried: res.method },
|
|
179
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
);
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
if (applicableCount === 0) {
|
|
@@ -10,11 +10,23 @@
|
|
|
10
10
|
* @sc 3.1.2
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to any element other than the root <html> with a non-empty
|
|
13
|
-
* lang attribute
|
|
13
|
+
* lang attribute AND at least some non-whitespace "governed text" that
|
|
14
|
+
* actually inherits its language from that element, per ACT de46e4:
|
|
15
|
+
* - Descendant text/alt is governed by the nearest lang-carrying
|
|
16
|
+
* ancestor only, a nested descendant with its own non-empty lang
|
|
17
|
+
* re-scopes everything inside it, so that subtree no longer counts
|
|
18
|
+
* toward the outer element's applicability (it counts toward the
|
|
19
|
+
* nested element's own, if that one is also being evaluated).
|
|
20
|
+
* - A non-empty alt attribute on img/area/input[type=image] counts as
|
|
21
|
+
* governed text, the same as a text node.
|
|
22
|
+
* - Text (or alt) that CSS keeps out of the render tree (display:none,
|
|
23
|
+
* the hidden attribute, ...) does not count. aria-hidden and
|
|
24
|
+
* offscreen positioning do NOT exempt text, per ACT's own failed
|
|
25
|
+
* examples for both, only actual non-rendering does.
|
|
14
26
|
* @expectation
|
|
15
27
|
* The lang value matches a valid BCP47 language-tag syntax. WCAG 3.1.2
|
|
16
28
|
* (Language of Parts) requires that when a passage's language differs
|
|
17
|
-
* from the page's default, it is identified programmatically
|
|
29
|
+
* from the page's default, it is identified programmatically. An
|
|
18
30
|
* invalid tag fails to identify a real language at all.
|
|
19
31
|
* @implementation-notes
|
|
20
32
|
* - Distinct, atomic decision from html-lang-attr-present (that
|
|
@@ -22,7 +34,7 @@
|
|
|
22
34
|
* covers every other element, for SC 3.1.2.
|
|
23
35
|
* - Same minimal BCP47 *syntax* check as html-lang-attr-present (primary
|
|
24
36
|
* subtag + optional subtags), not IANA Language Subtag Registry
|
|
25
|
-
* validation
|
|
37
|
+
* validation, same documented scope limitation (syntactically
|
|
26
38
|
* well-formed but unregistered tags like "xx-ZZ" are not flagged).
|
|
27
39
|
*/
|
|
28
40
|
|
|
@@ -66,6 +78,78 @@ function runInPage(ctx) {
|
|
|
66
78
|
? helpers.isValidLanguageTag
|
|
67
79
|
: (v) => BCP47_RE.test(String(v || ''));
|
|
68
80
|
|
|
81
|
+
function isDomVisible(node) {
|
|
82
|
+
if (!node) return false;
|
|
83
|
+
if (helpers.isDomVisibleEligible)
|
|
84
|
+
return !!helpers.isDomVisibleEligible(node, ctx, { targetSet: 'dom' }).eligible;
|
|
85
|
+
if (helpers.getEligibilityInfo)
|
|
86
|
+
return !!helpers.getEligibilityInfo(node, ctx, { targetSet: 'dom' }).eligible;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasOwnNonEmptyLang(node) {
|
|
91
|
+
try {
|
|
92
|
+
const v = node.getAttribute ? node.getAttribute('lang') : null;
|
|
93
|
+
return v != null && v.trim() !== '';
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isAltBearing(node) {
|
|
100
|
+
const tag = (node.tagName || '').toLowerCase();
|
|
101
|
+
if (tag === 'img' || tag === 'area') return true;
|
|
102
|
+
if (tag !== 'input') return false;
|
|
103
|
+
try {
|
|
104
|
+
return (node.getAttribute('type') || '').toLowerCase() === 'image';
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// "Governed text": non-whitespace text (or alt) that inherits its
|
|
111
|
+
// language from `root`, per ACT de46e4, see @applicability above. Walks
|
|
112
|
+
// the flat subtree, stopping at any descendant carrying its own non-empty
|
|
113
|
+
// lang (that subtree governs itself, not `root`), and treating
|
|
114
|
+
// display:none/hidden content as absent. Bails out as soon as any
|
|
115
|
+
// qualifying text is found; a node-visit budget guards pathological
|
|
116
|
+
// markup the same way other subtree walks in this engine do.
|
|
117
|
+
const MAX_VISITS = 5000;
|
|
118
|
+
function hasGovernedText(root) {
|
|
119
|
+
let found = false;
|
|
120
|
+
let visits = 0;
|
|
121
|
+
|
|
122
|
+
function walk(node, isRoot) {
|
|
123
|
+
if (found || visits++ > MAX_VISITS) return;
|
|
124
|
+
if (!node || node.nodeType !== 1) return;
|
|
125
|
+
if (!isRoot && hasOwnNonEmptyLang(node)) return; // re-scoped to itself
|
|
126
|
+
|
|
127
|
+
if (isAltBearing(node)) {
|
|
128
|
+
const alt = node.getAttribute ? node.getAttribute('alt') : null;
|
|
129
|
+
if (alt != null && alt.trim() !== '' && isDomVisible(node)) found = true;
|
|
130
|
+
return; // alt-bearing elements have no other text to walk into
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!isDomVisible(node)) return;
|
|
134
|
+
|
|
135
|
+
const kids = node.childNodes ? Array.from(node.childNodes) : [];
|
|
136
|
+
for (const kid of kids) {
|
|
137
|
+
if (found) return;
|
|
138
|
+
if (kid.nodeType === 3) {
|
|
139
|
+
if (String(kid.nodeValue || '').trim()) {
|
|
140
|
+
found = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
} else if (kid.nodeType === 1) {
|
|
144
|
+
walk(kid, false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
walk(root, true);
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
|
|
69
153
|
const nodes = helpers.queryAllSmart
|
|
70
154
|
? helpers.queryAllSmart('[lang]')
|
|
71
155
|
: helpers.queryAll('[lang]');
|
|
@@ -80,10 +164,11 @@ function runInPage(ctx) {
|
|
|
80
164
|
const rawAttr = el.getAttribute('lang');
|
|
81
165
|
if (rawAttr === null || rawAttr === '') continue; // ACT de46e4: empty is out of scope
|
|
82
166
|
|
|
83
|
-
// The rule applies only where text actually inherits the language
|
|
84
|
-
// element
|
|
85
|
-
//
|
|
86
|
-
|
|
167
|
+
// The rule applies only where text actually inherits the language from
|
|
168
|
+
// THIS element specifically, not merely where the subtree has any text
|
|
169
|
+
// at all, which could all belong to a nested element's own (possibly
|
|
170
|
+
// valid) lang instead. See hasGovernedText's doc comment.
|
|
171
|
+
if (!hasGovernedText(el)) continue;
|
|
87
172
|
|
|
88
173
|
applicableCount += 1;
|
|
89
174
|
|
|
@@ -92,23 +177,21 @@ function runInPage(ctx) {
|
|
|
92
177
|
if (isValidTag(raw)) continue;
|
|
93
178
|
|
|
94
179
|
const tag = el.tagName.toLowerCase();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
});
|
|
180
|
+
|
|
181
|
+
occurrences.push(
|
|
182
|
+
helpers.reportOccurrence(el, {
|
|
183
|
+
summary: `This lang attribute value ("${raw}") is not a syntactically valid language tag.`,
|
|
184
|
+
hint: 'Use a valid BCP47 language tag (e.g. "fr", "es-MX").',
|
|
185
|
+
i18n: {
|
|
186
|
+
summaryKey: 'validLang_summary_fail',
|
|
187
|
+
hintKey: 'validLang_hint_fail',
|
|
188
|
+
params: { element: tag, value: raw }
|
|
189
|
+
},
|
|
190
|
+
data: {
|
|
191
|
+
details: { reasonCode: 'ELEMENT_LANG_INVALID', element: tag, value: raw }
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
);
|
|
112
195
|
}
|
|
113
196
|
|
|
114
197
|
if (applicableCount === 0) {
|
|
@@ -87,7 +87,7 @@ function runInPage(ctx) {
|
|
|
87
87
|
|
|
88
88
|
function computeNameInfo(el) {
|
|
89
89
|
// <video> is not a labelable element (no browser computes an accessible
|
|
90
|
-
// name from <label for="...">), so only ARIA naming and title count
|
|
90
|
+
// name from <label for="...">), so only ARIA naming and title count,
|
|
91
91
|
// do not accept native <label> associations.
|
|
92
92
|
const flags = [];
|
|
93
93
|
let aria = null;
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* @check accesskeys
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary accesskey values must be unique on the page
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies whenever two or more elements share the same non-empty
|
|
12
12
|
* accesskey attribute value (case-insensitive).
|
|
13
13
|
* @expectation
|
|
14
14
|
* Every accesskey value on the page is unique. Duplicate accesskeys
|
|
15
|
-
* make keyboard-shortcut activation ambiguous
|
|
15
|
+
* make keyboard-shortcut activation ambiguous: only one of the
|
|
16
16
|
* elements sharing the key can actually be reached by it, and which
|
|
17
17
|
* one is browser/platform-dependent.
|
|
18
18
|
* @implementation-notes
|
|
19
|
-
* - Not WCAG-normative
|
|
19
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
20
20
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
21
21
|
* header comment for the shared rationale/precedent.
|
|
22
22
|
*/
|
|
@@ -62,25 +62,24 @@ function runInPage(ctx) {
|
|
|
62
62
|
for (const [key, els] of groups) {
|
|
63
63
|
if (els.length <= 1) continue;
|
|
64
64
|
for (const el of els) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
65
|
+
occurrences.push(
|
|
66
|
+
helpers.reportOccurrence(el, {
|
|
67
|
+
summary: "This element's accesskey is shared with another element on the page.",
|
|
68
|
+
hint: 'Make each accesskey value unique across the page.',
|
|
69
|
+
i18n: {
|
|
70
|
+
summaryKey: 'accesskeys_summary_cantTell',
|
|
71
|
+
hintKey: 'accesskeys_hint_cantTell',
|
|
72
|
+
params: { accesskey: key, duplicateCount: String(els.length) }
|
|
73
|
+
},
|
|
74
|
+
data: {
|
|
75
|
+
details: {
|
|
76
|
+
reasonCode: 'ACCESSKEY_DUPLICATE',
|
|
77
|
+
accesskey: key,
|
|
78
|
+
duplicateCount: els.length
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
);
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <area> elements whose alt attribute is present but empty once
|
|
14
|
+
* trimmed, the markup that declares a hotspot decorative. The <area> must
|
|
15
|
+
* belong to a <map> that an <img usemap> actually references, and both
|
|
16
|
+
* that <img> and the <area> itself must be included in the accessibility
|
|
17
|
+
* tree; an <area> in an unused map is out of scope.
|
|
18
|
+
* role="presentation"/"none" takes an element out unless it is focusable.
|
|
12
19
|
* @expectation
|
|
13
20
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
21
|
*/
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <area> elements whose alt attribute is present and non-empty.
|
|
14
|
+
* The <area> must belong to a <map> that an <img usemap> actually
|
|
15
|
+
* references, and both that <img> and the <area> itself must be included
|
|
16
|
+
* in the accessibility tree; an <area> in an unused map is out of scope.
|
|
17
|
+
* role="presentation"/"none" takes an element out unless it is focusable.
|
|
12
18
|
* @expectation
|
|
13
19
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
20
|
*/
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* @expectation
|
|
15
15
|
* `aria-checked` is redundant on a native checkbox/radio (the role's
|
|
16
16
|
* checked state is already exposed natively), but when an author sets
|
|
17
|
-
* it explicitly it should agree with the element's actual state
|
|
17
|
+
* it explicitly it should agree with the element's actual state,
|
|
18
18
|
* otherwise assistive technology is told something different from what
|
|
19
19
|
* a sighted user perceives.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* - Authored as `type: 'manual'` (cantTell-capped, never fail), unlike
|
|
22
|
+
* most ARIA-validity rules in this file family. This engine analyzes
|
|
23
|
+
* STATIC markup only (no script execution), so `.checked`
|
|
24
24
|
* reliably reflects the static `checked` attribute for freshly-parsed
|
|
25
25
|
* markup, but a very common, entirely legitimate real-world pattern is a
|
|
26
26
|
* JS-hydrated widget whose server-rendered HTML intentionally ships
|
|
@@ -114,29 +114,26 @@ function runInPage(ctx) {
|
|
|
114
114
|
|
|
115
115
|
if (normalizedAriaChecked === actualState) continue;
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
ariaChecked: normalizedAriaChecked,
|
|
135
|
-
actualState,
|
|
136
|
-
type
|
|
117
|
+
occurrences.push(
|
|
118
|
+
helpers.reportOccurrence(el, {
|
|
119
|
+
summary:
|
|
120
|
+
'This element’s aria-checked value does not match its actual checked/indeterminate state.',
|
|
121
|
+
hint: 'Set aria-checked to match the element’s real state, or remove it; a native checkbox/radio already exposes this state without it.',
|
|
122
|
+
i18n: {
|
|
123
|
+
summaryKey: 'ariaCheckedStateMismatch_summary_cantTell',
|
|
124
|
+
hintKey: 'ariaCheckedStateMismatch_hint_cantTell',
|
|
125
|
+
params: { ariaChecked: normalizedAriaChecked, actualState, type }
|
|
126
|
+
},
|
|
127
|
+
data: {
|
|
128
|
+
details: {
|
|
129
|
+
reasonCode: 'ARIA_CHECKED_STATE_MISMATCH',
|
|
130
|
+
ariaChecked: normalizedAriaChecked,
|
|
131
|
+
actualState,
|
|
132
|
+
type
|
|
133
|
+
}
|
|
137
134
|
}
|
|
138
|
-
}
|
|
139
|
-
|
|
135
|
+
})
|
|
136
|
+
);
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
if (applicableCount === 0) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check aria-text
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary role="text" elements should have no focusable descendants
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Elements with an explicit `role="text"`.
|
|
12
12
|
* @expectation
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* subtree as a single unit of plain text (e.g. text visually split
|
|
15
15
|
* across multiple `<span>`s by styling). Per the WAI-ARIA Authoring
|
|
16
16
|
* Practices, this only makes sense when that subtree contains no
|
|
17
|
-
* focusable content
|
|
17
|
+
* focusable content: a focusable descendant inside a "this is just
|
|
18
18
|
* text" region is unreachable or confusing for keyboard/AT users.
|
|
19
19
|
* @implementation-notes
|
|
20
|
-
* - Not WCAG-normative
|
|
20
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
21
21
|
* `type: 'manual'` rule, matching the Tier 1b precedent (see
|
|
22
22
|
* `landmark-unique`'s header comment for the shared rationale).
|
|
23
23
|
* - "Focusable descendant" is a presence check (link/button/form
|
|
@@ -80,7 +80,7 @@ function runInPage(ctx) {
|
|
|
80
80
|
selector: stableSelector,
|
|
81
81
|
html,
|
|
82
82
|
summary: 'This role="text" element contains a focusable descendant.',
|
|
83
|
-
hint: 'Remove role="text" (or remove the focusable descendant)
|
|
83
|
+
hint: 'Remove role="text" (or remove the focusable descendant); a "plain text" region should not contain focusable content.',
|
|
84
84
|
i18n: {
|
|
85
85
|
summaryKey: 'ariaText_summary_cantTell',
|
|
86
86
|
hintKey: 'ariaText_hint_cantTell',
|