@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 progressbar-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="progressbar" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 1.1.1
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="progressbar" (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, or from title.
|
|
19
|
+
* role="progressbar" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom progressbar widget is not
|
|
21
|
+
* reliably exposed as its name.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'progressbar-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
@@ -54,7 +73,7 @@ function runInPage(ctx) {
|
|
|
54
73
|
const raw = getAttr(el, 'aria-labelledby');
|
|
55
74
|
if (!raw) return '';
|
|
56
75
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
57
|
-
// name-from-content of the referenced element
|
|
76
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
58
77
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
59
78
|
// aria-labelledby target's only name source is its title attribute,
|
|
60
79
|
// which name-from-content alone can never see).
|
|
@@ -106,10 +125,10 @@ function runInPage(ctx) {
|
|
|
106
125
|
if (title) return { ok: true, method: 'title' };
|
|
107
126
|
|
|
108
127
|
// role="progressbar" is name-from-author-only per WAI-ARIA: aria-label,
|
|
109
|
-
// aria-labelledby, or title
|
|
128
|
+
// aria-labelledby, or title, no content-based naming method at all. It
|
|
110
129
|
// must NOT fall back to subtree content: e.g. a <ul role="progressbar">
|
|
111
130
|
// loading skeleton with no name of its own, whose nested descendants
|
|
112
|
-
// carry their own aria-label for a different purpose
|
|
131
|
+
// carry their own aria-label for a different purpose, falling back to
|
|
113
132
|
// subtree content would wrongly treat that as the progressbar's name.
|
|
114
133
|
return { ok: false, method: 'none' };
|
|
115
134
|
}
|
|
@@ -135,28 +154,26 @@ function runInPage(ctx) {
|
|
|
135
154
|
}
|
|
136
155
|
})()
|
|
137
156
|
: null;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
summaryKey: 'progressbarNamePresent_summary_fail',
|
|
148
|
-
hintKey: 'progressbarNamePresent_hint_fail',
|
|
149
|
-
params: {}
|
|
150
|
-
},
|
|
151
|
-
data: {
|
|
152
|
-
details: {
|
|
153
|
-
reasonCode: 'name_missing',
|
|
154
|
-
controlType: 'progressbar',
|
|
155
|
-
methodTried: res.method
|
|
157
|
+
|
|
158
|
+
occurrences.push(
|
|
159
|
+
helpers.reportOccurrence(el, {
|
|
160
|
+
summary: 'This progress bar has no accessible name.',
|
|
161
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this progress bar's accessible name.",
|
|
162
|
+
i18n: {
|
|
163
|
+
summaryKey: 'progressbarNamePresent_summary_fail',
|
|
164
|
+
hintKey: 'progressbarNamePresent_hint_fail',
|
|
165
|
+
params: {}
|
|
156
166
|
},
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
data: {
|
|
168
|
+
details: {
|
|
169
|
+
reasonCode: 'name_missing',
|
|
170
|
+
controlType: 'progressbar',
|
|
171
|
+
methodTried: res.method
|
|
172
|
+
},
|
|
173
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
if (applicableCount === 0) {
|
|
@@ -5,26 +5,36 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @check role-img-text-alternative-present
|
|
7
7
|
* @atomic true
|
|
8
|
-
* @summary Accessible elements with role="img" must have
|
|
8
|
+
* @summary Accessible elements with role="img"/"graphics-symbol"/"graphics-document" must have a text alternative
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @applicability
|
|
12
|
-
* Applies to elements with role="img"
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Applies to elements with role="img", role="graphics-symbol" or
|
|
13
|
+
* role="graphics-document" that are included in the accessibility tree
|
|
14
|
+
* (ACT 23a2a8's "programmatically hidden" exemption:
|
|
15
|
+
* display:none/visibility:hidden/aria-hidden="true" on the element or an
|
|
16
|
+
* ancestor, with no carve-out for focusable or IDREF-referenced elements,
|
|
17
|
+
* aria-hidden-focus and duplicate-id-aria own those separately). Per ACT
|
|
18
|
+
* 7d6734, this reaches any element carrying one of these roles, not only
|
|
19
|
+
* the <svg> root, e.g. a <circle role="graphics-symbol"> nested inside a
|
|
20
|
+
* plain <svg>. The <svg> root itself is left to svg-text-alternative-present's
|
|
21
|
+
* own (already ACT-clean) title/aria-name handling; the <img> tag is
|
|
22
|
+
* excluded here since it has its own dedicated rule.
|
|
15
23
|
* @expectation
|
|
16
|
-
* Each applicable element
|
|
24
|
+
* Each applicable element has an accessible text alternative:
|
|
17
25
|
* - aria-label with a non-empty value; OR
|
|
18
26
|
* - aria-labelledby referencing at least one existing element that contributes non-empty text; OR
|
|
19
|
-
* - a non-empty title attribute (last-resort accessible-name source per HTML-AAM)
|
|
27
|
+
* - a non-empty title attribute (last-resort accessible-name source per HTML-AAM); OR
|
|
28
|
+
* - for an SVG-namespace element, a non-empty first-child <title> (SVG-AAM's own naming mechanism, not only for the <svg> root).
|
|
20
29
|
*/
|
|
21
30
|
|
|
22
31
|
const id = 'role-img-text-alternative-present';
|
|
23
32
|
|
|
24
33
|
const meta = {
|
|
25
|
-
title:
|
|
34
|
+
title:
|
|
35
|
+
'[role="img"/"graphics-symbol"/"graphics-document"] must have an accessible text alternative',
|
|
26
36
|
description:
|
|
27
|
-
'Checks that elements with role="img" provide an accessible text alternative
|
|
37
|
+
'Checks that elements with role="img", "graphics-symbol" or "graphics-document" provide an accessible text alternative using aria-label, aria-labelledby, a title attribute, or (for SVG elements) a first-child <title>.',
|
|
28
38
|
i18n: {
|
|
29
39
|
titleKey: 'roleImg_textAlternativePresent_title',
|
|
30
40
|
descriptionKey: 'roleImg_textAlternativePresent_description'
|
|
@@ -86,7 +96,7 @@ function runInPage(ctx) {
|
|
|
86
96
|
|
|
87
97
|
const imgElements = (() => {
|
|
88
98
|
// do not consider element "img" because it has its own rule
|
|
89
|
-
const sel = '[role="img" i]:not(img)';
|
|
99
|
+
const sel = '[role="img" i]:not(img), [role="graphics-symbol" i], [role="graphics-document" i]';
|
|
90
100
|
try {
|
|
91
101
|
return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
|
|
92
102
|
} catch {
|
|
@@ -101,8 +111,16 @@ function runInPage(ctx) {
|
|
|
101
111
|
const occurrences = [];
|
|
102
112
|
let applicableCount = 0;
|
|
103
113
|
|
|
114
|
+
// ACT 23a2a8 (same rule as native img-alt-present) exempts programmatically
|
|
115
|
+
// hidden images and that glossary term has no focusability carve-out, so a
|
|
116
|
+
// tabbable/IDREF-referenced role="img" inside aria-hidden stays out of
|
|
117
|
+
// scope here too; aria-hidden-focus reports that markup instead.
|
|
104
118
|
const isAccTreeEligible =
|
|
105
|
-
helpers && typeof helpers.
|
|
119
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
120
|
+
? helpers.isIncludedInAccessibilityTree
|
|
121
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
122
|
+
? helpers.isAccTreeEligible
|
|
123
|
+
: null;
|
|
106
124
|
|
|
107
125
|
const getAccessibleNameInfo =
|
|
108
126
|
helpers && typeof helpers.getAccessibleNameInfo === 'function'
|
|
@@ -122,10 +140,21 @@ function runInPage(ctx) {
|
|
|
122
140
|
}
|
|
123
141
|
})();
|
|
124
142
|
if (elig && elig.eligible === false) continue;
|
|
143
|
+
// isIncludedInAccessibilityTree returns a plain boolean, not an
|
|
144
|
+
// {eligible, reasons} object like isAccTreeEligible's fallback shape.
|
|
145
|
+
if (typeof elig === 'boolean' && elig === false) continue;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
applicableCount += 1;
|
|
128
149
|
|
|
150
|
+
const matchedRole = (() => {
|
|
151
|
+
try {
|
|
152
|
+
return trim(el.getAttribute('role')).split(/\s+/)[0].toLowerCase();
|
|
153
|
+
} catch {
|
|
154
|
+
return 'img';
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
|
|
129
158
|
// Expectation: aria-label OR aria-labelledby. We use helper name-info when available,
|
|
130
159
|
// but we also validate the source to keep this rule scoped/deterministic.
|
|
131
160
|
|
|
@@ -163,6 +192,25 @@ function runInPage(ctx) {
|
|
|
163
192
|
})();
|
|
164
193
|
const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
|
|
165
194
|
|
|
195
|
+
// SVG-AAM's own accessible-name mechanism: a first-child <title>
|
|
196
|
+
// element (not the HTML title attribute) is the standard way to name
|
|
197
|
+
// any element in the SVG namespace, not only the <svg> root -- a
|
|
198
|
+
// role="graphics-symbol" <circle> named only this way still has a real
|
|
199
|
+
// text alternative, same as a role="img" <svg>.
|
|
200
|
+
const svgTitleChildText = (() => {
|
|
201
|
+
try {
|
|
202
|
+
const isSvgNamespace = el.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
203
|
+
if (!isSvgNamespace) return '';
|
|
204
|
+
const first = el.firstElementChild;
|
|
205
|
+
const firstTag = first ? (first.localName || first.tagName || '').toLowerCase() : '';
|
|
206
|
+
if (firstTag !== 'title') return '';
|
|
207
|
+
return trim(first.textContent);
|
|
208
|
+
} catch {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
const hasValidTitleSource = hasValidTitle || !!svgTitleChildText;
|
|
213
|
+
|
|
166
214
|
let nameInfo = null;
|
|
167
215
|
|
|
168
216
|
// Fast outcomes first (no helper needed)
|
|
@@ -170,19 +218,19 @@ function runInPage(ctx) {
|
|
|
170
218
|
let hasName = false;
|
|
171
219
|
|
|
172
220
|
if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
|
|
173
|
-
if (
|
|
221
|
+
if (hasValidTitleSource) {
|
|
174
222
|
hasName = true;
|
|
175
223
|
} else {
|
|
176
224
|
reasonCode = 'missingTextAlternative';
|
|
177
225
|
}
|
|
178
226
|
} else if (hasAriaLabelAttr && !hasValidAriaLabel) {
|
|
179
|
-
if (
|
|
227
|
+
if (hasValidTitleSource) {
|
|
180
228
|
hasName = true;
|
|
181
229
|
} else {
|
|
182
230
|
reasonCode = 'emptyAriaLabel';
|
|
183
231
|
}
|
|
184
232
|
} else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
|
|
185
|
-
if (
|
|
233
|
+
if (hasValidTitleSource) {
|
|
186
234
|
hasName = true;
|
|
187
235
|
} else {
|
|
188
236
|
reasonCode = 'emptyAriaLabelledby';
|
|
@@ -214,12 +262,12 @@ function runInPage(ctx) {
|
|
|
214
262
|
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
215
263
|
|
|
216
264
|
const baseOccurrence = {
|
|
217
|
-
summary:
|
|
218
|
-
hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this
|
|
265
|
+
summary: `Missing text alternative on element with role="${matchedRole}".`,
|
|
266
|
+
hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this element a text alternative.',
|
|
219
267
|
i18n: {
|
|
220
268
|
summaryKey: 'roleImg_textAlternativePresent_summary_fail',
|
|
221
269
|
hintKey: 'roleImg_textAlternativePresent_hint_fail',
|
|
222
|
-
params: { role:
|
|
270
|
+
params: { role: matchedRole }
|
|
223
271
|
},
|
|
224
272
|
data: {
|
|
225
273
|
details: {
|
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check searchbox-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="searchbox" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="searchbox" (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, or from title.
|
|
19
|
+
* role="searchbox" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom searchbox widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<input role="searchbox">)
|
|
22
|
+
* an associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'searchbox-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
28
|
+
title: 'Searchboxes have an accessible name',
|
|
29
|
+
description: 'Checks that elements with role="searchbox" expose a non-empty accessible name.',
|
|
10
30
|
i18n: {
|
|
11
31
|
titleKey: 'searchboxNamePresent_title',
|
|
12
32
|
descriptionKey: 'searchboxNamePresent_description'
|
|
@@ -51,7 +71,7 @@ function runInPage(ctx) {
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
74
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
75
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
76
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
77
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -68,7 +88,7 @@ function runInPage(ctx) {
|
|
|
68
88
|
|
|
69
89
|
// A <label> contributes a name via its own aria-label/aria-labelledby
|
|
70
90
|
// (checked first, same ARIA-over-content precedence any element's
|
|
71
|
-
// accessible name gives
|
|
91
|
+
// accessible name gives, e.g. <label aria-label="Search"><svg
|
|
72
92
|
// aria-hidden="true">...</svg></label> names its control "Search" even
|
|
73
93
|
// though the label's only child content is aria-hidden) or, failing
|
|
74
94
|
// that, its rendered content (getConservativeSubtreeText).
|
|
@@ -83,10 +103,10 @@ function runInPage(ctx) {
|
|
|
83
103
|
if (content) return content;
|
|
84
104
|
// Final fallback per the general accname text-alternative algorithm,
|
|
85
105
|
// which applies to any element being asked for its name regardless of
|
|
86
|
-
// why (own aria-label, an aria-labelledby reference, or
|
|
106
|
+
// why (own aria-label, an aria-labelledby reference, or, here, native
|
|
87
107
|
// <label for> association): title, when nothing else yields a name.
|
|
88
108
|
// Purely additive (only fills in a name where there was none before),
|
|
89
|
-
// so it carries no false-positive risk
|
|
109
|
+
// so it carries no false-positive risk, see dialog-name-present.js's
|
|
90
110
|
// identical <iframe>-title-fallback fix for the concrete real-world
|
|
91
111
|
// trigger this same accname step covers elsewhere.
|
|
92
112
|
return getAttr(lab, 'title');
|
|
@@ -96,7 +116,7 @@ function runInPage(ctx) {
|
|
|
96
116
|
const raw = getAttr(el, 'aria-labelledby');
|
|
97
117
|
if (!raw) return '';
|
|
98
118
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
99
|
-
// name-from-content of the referenced element
|
|
119
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
100
120
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
101
121
|
// aria-labelledby target's only name source is its title attribute,
|
|
102
122
|
// which name-from-content alone can never see).
|
|
@@ -227,24 +247,26 @@ function runInPage(ctx) {
|
|
|
227
247
|
}
|
|
228
248
|
})()
|
|
229
249
|
: null;
|
|
230
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
231
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
232
250
|
|
|
233
|
-
occurrences.push(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
occurrences.push(
|
|
252
|
+
helpers.reportOccurrence(el, {
|
|
253
|
+
summary: 'This element has no accessible name.',
|
|
254
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this searchbox's accessible name.",
|
|
255
|
+
i18n: {
|
|
256
|
+
summaryKey: 'searchboxNamePresent_summary_fail',
|
|
257
|
+
hintKey: 'searchboxNamePresent_hint_fail',
|
|
258
|
+
params: { controlType: 'searchbox' }
|
|
259
|
+
},
|
|
260
|
+
data: {
|
|
261
|
+
details: {
|
|
262
|
+
reasonCode: 'name_missing',
|
|
263
|
+
controlType: 'searchbox',
|
|
264
|
+
methodTried: res.method
|
|
265
|
+
},
|
|
266
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
);
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
if (applicableCount === 0) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @expectation
|
|
14
14
|
* The image does not use ismap at all. Server-side image maps depend on
|
|
15
15
|
* the browser sending click coordinates to the server, which has no
|
|
16
|
-
* keyboard-operable equivalent
|
|
16
|
+
* keyboard-operable equivalent, there is no way to determine or expose
|
|
17
17
|
* individual clickable regions to assistive technology or keyboard
|
|
18
18
|
* users. Client-side image maps (<map>/<area>, each with real href/alt)
|
|
19
19
|
* are the accessible alternative and are not flagged by this rule.
|
|
@@ -66,24 +66,21 @@ function runInPage(ctx) {
|
|
|
66
66
|
|
|
67
67
|
applicableCount += 1;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
details: { reasonCode: 'SERVER_SIDE_IMAGE_MAP' }
|
|
85
|
-
}
|
|
86
|
-
});
|
|
69
|
+
occurrences.push(
|
|
70
|
+
helpers.reportOccurrence(el, {
|
|
71
|
+
summary:
|
|
72
|
+
'This image uses a server-side image map, which has no keyboard-operable equivalent.',
|
|
73
|
+
hint: 'Replace the server-side image map (ismap) with a client-side image map (<map>/<area>) or separate accessible links/buttons.',
|
|
74
|
+
i18n: {
|
|
75
|
+
summaryKey: 'serverSideImageMapAbsent_summary_fail',
|
|
76
|
+
hintKey: 'serverSideImageMapAbsent_hint_fail',
|
|
77
|
+
params: {}
|
|
78
|
+
},
|
|
79
|
+
data: {
|
|
80
|
+
details: { reasonCode: 'SERVER_SIDE_IMAGE_MAP' }
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
);
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
if (applicableCount === 0) {
|
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check slider-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="slider" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="slider" (the attribute must name that
|
|
13
|
+
* role alone, not a fallback list) that are included in the accessibility
|
|
14
|
+
* tree. An element with the matching implicit role but no role attribute
|
|
15
|
+
* is out of scope. A native <input type="range"> without the role belongs
|
|
16
|
+
* to form-control-programmatic-label-present.
|
|
17
|
+
* @expectation
|
|
18
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
19
|
+
* aria-labelledby that resolves to non-empty text, or from title.
|
|
20
|
+
* role="slider" is name-from-author-only, so subtree text is never
|
|
21
|
+
* accepted: text sitting inside a custom slider widget is not reliably
|
|
22
|
+
* exposed as its name. On a labelable element (<input role="slider">) an
|
|
23
|
+
* associated <label> counts as well.
|
|
24
|
+
*/
|
|
25
|
+
|
|
5
26
|
const id = 'slider-name-present';
|
|
6
27
|
|
|
7
28
|
const meta = {
|
|
@@ -67,7 +88,7 @@ function runInPage(ctx) {
|
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
function getConservativeSubtreeText(document, container) {
|
|
70
|
-
// "Name from content"
|
|
91
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
71
92
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
72
93
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
73
94
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -84,7 +105,7 @@ function runInPage(ctx) {
|
|
|
84
105
|
|
|
85
106
|
// A <label> contributes a name via its own aria-label/aria-labelledby
|
|
86
107
|
// (checked first, same ARIA-over-content precedence any element's
|
|
87
|
-
// accessible name gives
|
|
108
|
+
// accessible name gives, e.g. <label aria-label="Search"><svg
|
|
88
109
|
// aria-hidden="true">...</svg></label> names its control "Search" even
|
|
89
110
|
// though the label's only child content is aria-hidden) or, failing
|
|
90
111
|
// that, its rendered content (getConservativeSubtreeText).
|
|
@@ -99,10 +120,10 @@ function runInPage(ctx) {
|
|
|
99
120
|
if (content) return content;
|
|
100
121
|
// Final fallback per the general accname text-alternative algorithm,
|
|
101
122
|
// which applies to any element being asked for its name regardless of
|
|
102
|
-
// why (own aria-label, an aria-labelledby reference, or
|
|
123
|
+
// why (own aria-label, an aria-labelledby reference, or, here, native
|
|
103
124
|
// <label for> association): title, when nothing else yields a name.
|
|
104
125
|
// Purely additive (only fills in a name where there was none before),
|
|
105
|
-
// so it carries no false-positive risk
|
|
126
|
+
// so it carries no false-positive risk, see dialog-name-present.js's
|
|
106
127
|
// identical <iframe>-title-fallback fix for the concrete real-world
|
|
107
128
|
// trigger this same accname step covers elsewhere.
|
|
108
129
|
return getAttr(lab, 'title');
|
|
@@ -112,7 +133,7 @@ function runInPage(ctx) {
|
|
|
112
133
|
const raw = getAttr(el, 'aria-labelledby');
|
|
113
134
|
if (!raw) return '';
|
|
114
135
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
115
|
-
// name-from-content of the referenced element
|
|
136
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
116
137
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
117
138
|
// aria-labelledby target's only name source is its title attribute,
|
|
118
139
|
// which name-from-content alone can never see).
|
|
@@ -209,7 +230,7 @@ function runInPage(ctx) {
|
|
|
209
230
|
}
|
|
210
231
|
|
|
211
232
|
// role="slider" is name-from-author-only per WAI-ARIA (unlike e.g.
|
|
212
|
-
// role="option"/"tab"), so it must NOT fall back to subtree content
|
|
233
|
+
// role="option"/"tab"), so it must NOT fall back to subtree content,
|
|
213
234
|
// visible text near/inside a custom slider widget is not reliably
|
|
214
235
|
// exposed as its accessible name by assistive technologies.
|
|
215
236
|
return { ok: false, method: 'none' };
|
|
@@ -242,24 +263,22 @@ function runInPage(ctx) {
|
|
|
242
263
|
}
|
|
243
264
|
})()
|
|
244
265
|
: null;
|
|
245
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
246
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
247
266
|
|
|
248
|
-
occurrences.push(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
267
|
+
occurrences.push(
|
|
268
|
+
helpers.reportOccurrence(el, {
|
|
269
|
+
summary: 'This slider has no accessible name.',
|
|
270
|
+
hint: 'Provide a label, aria-label, or aria-labelledby so assistive technologies can identify the slider.',
|
|
271
|
+
i18n: {
|
|
272
|
+
summaryKey: 'sliderNamePresent_summary_fail',
|
|
273
|
+
hintKey: 'sliderNamePresent_hint_fail',
|
|
274
|
+
params: { kind }
|
|
275
|
+
},
|
|
276
|
+
data: {
|
|
277
|
+
details: { reasonCode: 'name_missing', controlType: kind, methodTried: res.method },
|
|
278
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
);
|
|
263
282
|
}
|
|
264
283
|
|
|
265
284
|
if (applicableCount === 0) {
|
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check spinbutton-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="spinbutton" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="spinbutton" (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, or from title.
|
|
19
|
+
* role="spinbutton" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom spinbutton widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<input role="spinbutton">)
|
|
22
|
+
* an associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'spinbutton-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
28
|
+
title: 'Spinbuttons have an accessible name',
|
|
29
|
+
description: 'Checks that elements with role="spinbutton" expose a non-empty accessible name.',
|
|
10
30
|
i18n: {
|
|
11
31
|
titleKey: 'spinbuttonNamePresent_title',
|
|
12
32
|
descriptionKey: 'spinbuttonNamePresent_description'
|
|
@@ -51,7 +71,7 @@ function runInPage(ctx) {
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
74
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
75
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
76
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
77
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -68,7 +88,7 @@ function runInPage(ctx) {
|
|
|
68
88
|
|
|
69
89
|
// A <label> contributes a name via its own aria-label/aria-labelledby
|
|
70
90
|
// (checked first, same ARIA-over-content precedence any element's
|
|
71
|
-
// accessible name gives
|
|
91
|
+
// accessible name gives, e.g. <label aria-label="Search"><svg
|
|
72
92
|
// aria-hidden="true">...</svg></label> names its control "Search" even
|
|
73
93
|
// though the label's only child content is aria-hidden) or, failing
|
|
74
94
|
// that, its rendered content (getConservativeSubtreeText).
|
|
@@ -83,10 +103,10 @@ function runInPage(ctx) {
|
|
|
83
103
|
if (content) return content;
|
|
84
104
|
// Final fallback per the general accname text-alternative algorithm,
|
|
85
105
|
// which applies to any element being asked for its name regardless of
|
|
86
|
-
// why (own aria-label, an aria-labelledby reference, or
|
|
106
|
+
// why (own aria-label, an aria-labelledby reference, or, here, native
|
|
87
107
|
// <label for> association): title, when nothing else yields a name.
|
|
88
108
|
// Purely additive (only fills in a name where there was none before),
|
|
89
|
-
// so it carries no false-positive risk
|
|
109
|
+
// so it carries no false-positive risk, see dialog-name-present.js's
|
|
90
110
|
// identical <iframe>-title-fallback fix for the concrete real-world
|
|
91
111
|
// trigger this same accname step covers elsewhere.
|
|
92
112
|
return getAttr(lab, 'title');
|
|
@@ -96,7 +116,7 @@ function runInPage(ctx) {
|
|
|
96
116
|
const raw = getAttr(el, 'aria-labelledby');
|
|
97
117
|
if (!raw) return '';
|
|
98
118
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
99
|
-
// name-from-content of the referenced element
|
|
119
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
100
120
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
101
121
|
// aria-labelledby target's only name source is its title attribute,
|
|
102
122
|
// which name-from-content alone can never see).
|
|
@@ -227,24 +247,26 @@ function runInPage(ctx) {
|
|
|
227
247
|
}
|
|
228
248
|
})()
|
|
229
249
|
: null;
|
|
230
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
231
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
232
250
|
|
|
233
|
-
occurrences.push(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
occurrences.push(
|
|
252
|
+
helpers.reportOccurrence(el, {
|
|
253
|
+
summary: 'This element has no accessible name.',
|
|
254
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this spinbutton's accessible name.",
|
|
255
|
+
i18n: {
|
|
256
|
+
summaryKey: 'spinbuttonNamePresent_summary_fail',
|
|
257
|
+
hintKey: 'spinbuttonNamePresent_hint_fail',
|
|
258
|
+
params: { controlType: 'spinbutton' }
|
|
259
|
+
},
|
|
260
|
+
data: {
|
|
261
|
+
details: {
|
|
262
|
+
reasonCode: 'name_missing',
|
|
263
|
+
controlType: 'spinbutton',
|
|
264
|
+
methodTried: res.method
|
|
265
|
+
},
|
|
266
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
);
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
if (applicableCount === 0) {
|