@surea11y/core 1.5.0 → 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 +193 -149
- package/README.md +27 -6
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +16 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +6 -4
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +53 -25
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +3 -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 +68 -10
- package/src/checks/automatic/aria-allowed-role.js +2 -2
- package/src/checks/automatic/aria-braille-equivalent.js +3 -3
- package/src/checks/automatic/aria-conditional-attr.js +5 -5
- package/src/checks/automatic/aria-deprecated-role.js +1 -1
- package/src/checks/automatic/aria-hidden-body.js +2 -2
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +18 -18
- package/src/checks/automatic/aria-prohibited-children.js +130 -37
- package/src/checks/automatic/aria-required-attr.js +60 -12
- package/src/checks/automatic/aria-required-children.js +21 -14
- package/src/checks/automatic/aria-required-parent.js +61 -9
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-valid-attr-value.js +15 -12
- package/src/checks/automatic/aria-valid-attr.js +1 -1
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- 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 +96 -19
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +5 -3
- 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 +1 -1
- package/src/checks/automatic/iframe-focusable-content.js +63 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +172 -18
- package/src/checks/automatic/link-in-text-block.js +10 -10
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- 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 +23 -4
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- 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 +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +71 -53
- package/src/checks/automatic/td-has-header.js +5 -5
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- 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 +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- 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 +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- 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 +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- 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 +4 -4
- 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 +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- 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 +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- 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 +8965 -1647
- package/src/report.js +2 -2
- package/surea11y.browser.js +3768 -611
- package/surea11y.i18n.de.js +1 -1
- package/surea11y.i18n.es.js +1 -1
- package/surea11y.i18n.fr.js +1 -1
- package/bin/surea11y-core.js +0 -20
|
@@ -5,20 +5,34 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @check img-alt-decorative
|
|
7
7
|
* @atomic true
|
|
8
|
-
* @summary Manual review:
|
|
8
|
+
* @summary Manual review: an image/canvas/svg excluded from the accessibility tree must be purely decorative
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to visible <img>, <canvas> or <svg> elements excluded from the
|
|
14
|
+
* accessibility tree by any of: an aria-hidden ancestor-or-self, an
|
|
15
|
+
* explicit role="none"/"presentation" not overridden by focusability, an
|
|
16
|
+
* <img alt=""> (the native decorative marker, same focusability
|
|
17
|
+
* override), an unlabeled <svg> whose implicit role is graphics-document
|
|
18
|
+
* (no img/graphics-symbol role restatement, aria-name, <title>/<desc>, or
|
|
19
|
+
* focusability), or an unlabeled <canvas> with no explicit role at all.
|
|
20
|
+
* Per ACT e88epe, an element is skipped entirely when any ancestor
|
|
21
|
+
* already has an author-supplied name (aria-label, aria-labelledby,
|
|
22
|
+
* title, or an associated <label>), that ancestor's name is what
|
|
23
|
+
* matters, not this element's exclusion (the common real case: an
|
|
24
|
+
* icon-only button already named via aria-label).
|
|
12
25
|
* @expectation
|
|
13
|
-
* Human review is required to confirm
|
|
26
|
+
* Human review is required to confirm the excluded element is purely
|
|
27
|
+
* decorative and conveys no information a user would otherwise miss.
|
|
14
28
|
*/
|
|
15
29
|
|
|
16
30
|
const id = 'img-alt-decorative';
|
|
17
31
|
|
|
18
32
|
const meta = {
|
|
19
|
-
title: '<img>
|
|
33
|
+
title: 'Excluded <img>/<canvas>/<svg> must be decorative (manual review)',
|
|
20
34
|
description:
|
|
21
|
-
'Flags <img> elements
|
|
35
|
+
'Flags <img>, <canvas> and <svg> elements excluded from the accessibility tree (aria-hidden, role="none"/"presentation", empty alt, or an unlabeled svg/canvas) for human review that they are purely decorative.',
|
|
22
36
|
i18n: {
|
|
23
37
|
titleKey: 'img_altDecorative_title',
|
|
24
38
|
descriptionKey: 'img_altDecorative_description'
|
|
@@ -65,56 +79,207 @@ function runInPage(ctx) {
|
|
|
65
79
|
}
|
|
66
80
|
};
|
|
67
81
|
|
|
68
|
-
const
|
|
69
|
-
helpers && typeof helpers.
|
|
82
|
+
const isIncludedInAccessibilityTree =
|
|
83
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
84
|
+
? helpers.isIncludedInAccessibilityTree
|
|
85
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
86
|
+
? helpers.isAccTreeEligible
|
|
87
|
+
: null;
|
|
70
88
|
|
|
71
89
|
const getFocusableInfo =
|
|
72
90
|
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
73
91
|
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
const getAccessibleNameInfo =
|
|
93
|
+
helpers && typeof helpers.getAccessibleNameInfo === 'function'
|
|
94
|
+
? helpers.getAccessibleNameInfo
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
98
|
+
|
|
99
|
+
function trim(v) {
|
|
100
|
+
try {
|
|
101
|
+
return (v == null ? '' : String(v)).trim();
|
|
102
|
+
} catch {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getExplicitRole(el) {
|
|
108
|
+
if (ariaHelpers && typeof ariaHelpers.getExplicitRole === 'function') {
|
|
76
109
|
try {
|
|
77
|
-
return
|
|
78
|
-
.trim()
|
|
79
|
-
.toLowerCase();
|
|
110
|
+
return ariaHelpers.getExplicitRole(el) || '';
|
|
80
111
|
} catch {
|
|
81
112
|
return '';
|
|
82
113
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const raw = trim(el.getAttribute('role'));
|
|
117
|
+
return raw ? raw.split(/\s+/)[0].toLowerCase() : '';
|
|
118
|
+
} catch {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
85
122
|
|
|
86
|
-
|
|
87
|
-
let focusable;
|
|
123
|
+
function isFocusable(el) {
|
|
88
124
|
if (getFocusableInfo) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} else {
|
|
125
|
+
try {
|
|
126
|
+
const fi = getFocusableInfo(el, ctx);
|
|
127
|
+
return !!(fi && fi.focusable);
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
98
133
|
const tabindex = el.getAttribute('tabindex');
|
|
99
|
-
|
|
134
|
+
return (
|
|
100
135
|
tabindex != null &&
|
|
101
136
|
String(tabindex).trim() !== '' &&
|
|
102
|
-
!Number.isNaN(Number(String(tabindex).trim()))
|
|
137
|
+
!Number.isNaN(Number(String(tabindex).trim()))
|
|
138
|
+
);
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isDomVisible(el) {
|
|
145
|
+
if (!el) return false;
|
|
146
|
+
if (helpers && helpers.isDomVisibleEligible)
|
|
147
|
+
return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
|
|
148
|
+
if (helpers && helpers.getEligibilityInfo)
|
|
149
|
+
return !!helpers.getEligibilityInfo(el, ctx, { targetSet: 'dom' }).eligible;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Same offscreen-hint pattern as avoid-inline-spacing.js.
|
|
154
|
+
function isOffscreen(el) {
|
|
155
|
+
if (!helpers || typeof helpers.getVisibilityHintsInfo !== 'function') return false;
|
|
156
|
+
try {
|
|
157
|
+
const info = helpers.getVisibilityHintsInfo(el, ctx, {});
|
|
158
|
+
return !!(info && Array.isArray(info.hints) && info.hints.indexOf('offscreen') !== -1);
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
103
161
|
}
|
|
104
|
-
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasAriaNamingAttr(el) {
|
|
165
|
+
try {
|
|
166
|
+
return el.getAttribute('aria-label') != null || el.getAttribute('aria-labelledby') != null;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Same first-child <title>/<desc> convention as svg-text-alternative-present.js.
|
|
173
|
+
function hasNonEmptyFirstChildTitleOrDesc(svg) {
|
|
174
|
+
try {
|
|
175
|
+
const first = svg.firstElementChild;
|
|
176
|
+
const tn = first ? (first.localName || first.tagName || '').toLowerCase() : '';
|
|
177
|
+
if (tn === 'title' || tn === 'desc') return !!trim(first.textContent);
|
|
178
|
+
} catch {
|
|
179
|
+
// ignore
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// General exclusion: aria-hidden, inert, or otherwise not included per the
|
|
185
|
+
// shared eligibility model. Visibility is checked separately beforehand,
|
|
186
|
+
// so a display:none/hidden element never reaches this path.
|
|
187
|
+
function isGenerallyExcluded(el) {
|
|
188
|
+
if (!isIncludedInAccessibilityTree) return false;
|
|
189
|
+
try {
|
|
190
|
+
const r = isIncludedInAccessibilityTree(el, ctx);
|
|
191
|
+
if (typeof r === 'boolean') return !r;
|
|
192
|
+
return !(r && r.eligible !== false);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Presentational exclusion: explicit role="none"/"presentation", or (img
|
|
199
|
+
// only) the native alt="" marker. Both are overridden by focusability, per
|
|
200
|
+
// ARIA conflict resolution (a focusable element is never presentational).
|
|
201
|
+
function isPresentationallyExcluded(el, tag) {
|
|
202
|
+
const role = getExplicitRole(el);
|
|
203
|
+
let presentational = role === 'presentation' || role === 'none';
|
|
204
|
+
if (!presentational && tag === 'img') {
|
|
205
|
+
const alt = el.getAttribute('alt');
|
|
206
|
+
presentational = alt != null && trim(alt) === '';
|
|
207
|
+
}
|
|
208
|
+
if (!presentational) return false;
|
|
209
|
+
return !isFocusable(el);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ACT e88epe's "ignored svg": an implicit graphics-document role (no
|
|
213
|
+
// explicit role, or role explicitly restated as graphics-document) with
|
|
214
|
+
// no accessible name and not focusable. An svg explicitly given role="img"
|
|
215
|
+
// /"graphics-symbol", an aria-name, a <title>/<desc>, or a tab stop is
|
|
216
|
+
// "included", a naming question for svg-text-alternative-present, not
|
|
217
|
+
// this rule's "is it decorative" question.
|
|
218
|
+
function isIgnoredSvg(el) {
|
|
219
|
+
const role = getExplicitRole(el);
|
|
220
|
+
if (role && role !== 'graphics-document') return false;
|
|
221
|
+
if (hasAriaNamingAttr(el)) return false;
|
|
222
|
+
if (hasNonEmptyFirstChildTitleOrDesc(el)) return false;
|
|
223
|
+
if (isFocusable(el)) return false;
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ACT e88epe's "ignored canvas": no explicit role at all and no
|
|
228
|
+
// accessible name. Canvas fallback content as a naming mechanism is not
|
|
229
|
+
// modeled here (a separate, narrower question than this rule needs to
|
|
230
|
+
// settle; see docs/DESIGN_CHALLENGES.md).
|
|
231
|
+
function isIgnoredCanvas(el) {
|
|
232
|
+
const role = getExplicitRole(el);
|
|
233
|
+
if (role) return false;
|
|
234
|
+
if (hasAriaNamingAttr(el)) return false;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const AUTHOR_NAME_MECHANISMS = new Set(['aria-label', 'aria-labelledby', 'title', 'label']);
|
|
239
|
+
|
|
240
|
+
// ACT e88epe's own exception: never applies under an ancestor already
|
|
241
|
+
// named by the author. The common real case is an icon-only button
|
|
242
|
+
// (<button aria-label="Close"><svg>...</svg></button>) that already has a
|
|
243
|
+
// correct name from the button itself. Whether the svg "is decorative"
|
|
244
|
+
// is moot there, and flagging it would just be noise on ordinary icon usage.
|
|
245
|
+
function hasAncestorNamedFromAuthor(el) {
|
|
246
|
+
if (!getAccessibleNameInfo) return false;
|
|
247
|
+
const getComposedParent =
|
|
248
|
+
helpers && typeof helpers.composedParent === 'function'
|
|
249
|
+
? helpers.composedParent
|
|
250
|
+
: (n) => (n && n.parentElement ? n.parentElement : null);
|
|
251
|
+
|
|
252
|
+
let cur = getComposedParent(el);
|
|
253
|
+
let guard = 0;
|
|
254
|
+
while (cur && guard++ < 200) {
|
|
255
|
+
if (cur.nodeType === 1) {
|
|
256
|
+
try {
|
|
257
|
+
const info = getAccessibleNameInfo(cur, ctx, { maxRefs: 8 });
|
|
258
|
+
if (
|
|
259
|
+
info &&
|
|
260
|
+
info.present &&
|
|
261
|
+
trim(info.value) &&
|
|
262
|
+
AUTHOR_NAME_MECHANISMS.has(info.mechanism)
|
|
263
|
+
)
|
|
264
|
+
return true;
|
|
265
|
+
} catch {
|
|
266
|
+
// ignore
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
cur = getComposedParent(cur);
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
105
272
|
}
|
|
106
273
|
|
|
107
274
|
const els = (() => {
|
|
108
|
-
|
|
109
|
-
// - alt="" (exact)
|
|
110
|
-
// - alt that starts/ends with space (to catch whitespace-only like " ")
|
|
111
|
-
const sel = 'img[alt=""], img[alt^=" "], img[alt$=" "]';
|
|
275
|
+
const sel = 'img, canvas, svg';
|
|
112
276
|
try {
|
|
113
277
|
return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
|
|
114
278
|
} catch {
|
|
115
279
|
return queryAll(sel);
|
|
116
280
|
}
|
|
117
281
|
})();
|
|
282
|
+
|
|
118
283
|
const uniqueEls = [];
|
|
119
284
|
const seen = new Set();
|
|
120
285
|
for (const el of els) {
|
|
@@ -124,7 +289,7 @@ function runInPage(ctx) {
|
|
|
124
289
|
}
|
|
125
290
|
}
|
|
126
291
|
|
|
127
|
-
if (!
|
|
292
|
+
if (!uniqueEls.length) {
|
|
128
293
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
129
294
|
}
|
|
130
295
|
|
|
@@ -132,39 +297,33 @@ function runInPage(ctx) {
|
|
|
132
297
|
let applicableCount = 0;
|
|
133
298
|
|
|
134
299
|
for (const el of uniqueEls) {
|
|
135
|
-
if (!el || !el.
|
|
300
|
+
if (!el || !el.tagName) continue;
|
|
301
|
+
const tag = el.tagName.toLowerCase();
|
|
136
302
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
return isAccTreeEligible(el, ctx);
|
|
141
|
-
} catch {
|
|
142
|
-
return { eligible: true, reasons: [] };
|
|
143
|
-
}
|
|
144
|
-
})();
|
|
145
|
-
if (elig && elig.eligible === false) continue;
|
|
146
|
-
}
|
|
303
|
+
if (!isDomVisible(el)) continue;
|
|
304
|
+
if (isOffscreen(el)) continue;
|
|
147
305
|
|
|
148
|
-
|
|
306
|
+
let excluded = isGenerallyExcluded(el);
|
|
307
|
+
if (!excluded) excluded = isPresentationallyExcluded(el, tag);
|
|
308
|
+
if (!excluded && tag === 'svg') excluded = isIgnoredSvg(el);
|
|
309
|
+
if (!excluded && tag === 'canvas') excluded = isIgnoredCanvas(el);
|
|
149
310
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (rawAlt == null) continue;
|
|
153
|
-
if (String(rawAlt).trim() !== '') continue;
|
|
311
|
+
if (!excluded) continue;
|
|
312
|
+
if (hasAncestorNamedFromAuthor(el)) continue;
|
|
154
313
|
|
|
155
314
|
applicableCount += 1;
|
|
156
315
|
|
|
157
316
|
const baseOccurrence = {
|
|
158
|
-
summary:
|
|
159
|
-
hint: 'Confirm the
|
|
317
|
+
summary: `Review whether this <${tag}> is decorative.`,
|
|
318
|
+
hint: 'Confirm the element is purely decorative. If it conveys information or function, give it a real text alternative (or an accessible name) instead of excluding it.',
|
|
160
319
|
i18n: {
|
|
161
320
|
summaryKey: 'img_altDecorative_summary_cantTell',
|
|
162
321
|
hintKey: 'img_altDecorative_hint_cantTell',
|
|
163
|
-
params: { element:
|
|
322
|
+
params: { element: tag }
|
|
164
323
|
},
|
|
165
324
|
data: {
|
|
166
325
|
visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
167
|
-
details:
|
|
326
|
+
details: { reasonCode: 'EXCLUDED_ELEMENT_REVIEW', element: tag }
|
|
168
327
|
}
|
|
169
328
|
};
|
|
170
329
|
|
|
@@ -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 <img> elements whose alt attribute is present and non-empty.
|
|
14
|
+
* The element must be included in the accessibility tree, and
|
|
15
|
+
* role="presentation"/"none" takes it out of scope unless it is focusable,
|
|
16
|
+
* which restores its role. An <img> with no alt at all is
|
|
17
|
+
* img-alt-present's failure, and one with alt="" is img-alt-decorative's
|
|
18
|
+
* review.
|
|
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,14 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <input type="image"> elements whose alt attribute is present
|
|
14
|
+
* but empty once trimmed, and which still carry a name from another
|
|
15
|
+
* source: an ARIA name resolving to non-empty text, or a title. Without
|
|
16
|
+
* that other name there is nothing to weigh the empty alt against, and the
|
|
17
|
+
* control is input-image-alt-present's failure instead. The element must
|
|
18
|
+
* be included in the accessibility tree, and role="presentation"/"none"
|
|
19
|
+
* takes it out of scope unless it is focusable, which restores its role.
|
|
12
20
|
* @expectation
|
|
13
21
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
22
|
*/
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <input type="image"> elements whose alt attribute is present
|
|
14
|
+
* and non-empty: an image button whose alt is its label. The element must
|
|
15
|
+
* be included in the accessibility tree, and role="presentation"/"none"
|
|
16
|
+
* takes it out of scope unless it is focusable, which restores its role.
|
|
12
17
|
* @expectation
|
|
13
18
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
19
|
*/
|
|
@@ -6,19 +6,19 @@
|
|
|
6
6
|
* @check label-title-only
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Form controls should not rely on the title attribute as their only label
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies to labelable form controls (input, excluding
|
|
12
12
|
* hidden/submit/reset/button/image; select; textarea) that have a
|
|
13
13
|
* non-empty title attribute.
|
|
14
14
|
* @expectation
|
|
15
|
-
* The control also has a real label
|
|
16
|
-
* aria-label, or aria-labelledby
|
|
15
|
+
* The control also has a real label (a wrapping/associated <label>,
|
|
16
|
+
* aria-label, or aria-labelledby), rather than depending on the title
|
|
17
17
|
* attribute alone. A title-only tooltip is not reliably exposed by all
|
|
18
18
|
* assistive technology and is not visible at all until hover/focus,
|
|
19
19
|
* unlike a persistent visible label.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* - Not WCAG-normative
|
|
21
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
22
22
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
23
23
|
* header comment for the shared rationale/precedent.
|
|
24
24
|
* - Distinct from form-control-programmatic-label-present (that
|
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
* @check landmark-banner-is-top-level
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary The banner landmark must not be nested inside another landmark
|
|
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 the page contains at least one banner candidate:
|
|
12
12
|
* explicit role="banner", OR a <header> with NO role attribute at all,
|
|
13
13
|
* regardless of nesting (see implementation notes on why candidate
|
|
14
|
-
* selection is
|
|
14
|
+
* selection is unconditional on purpose).
|
|
15
15
|
* @expectation
|
|
16
16
|
* No banner candidate has an ancestor that is itself any landmark
|
|
17
17
|
* region. A banner nested inside another landmark is not a top-level,
|
|
18
18
|
* whole-page banner and confuses landmark-based navigation for
|
|
19
19
|
* assistive technology users.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* - Not WCAG-normative
|
|
22
|
-
* `type: 'manual'` rule per
|
|
21
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
22
|
+
* `type: 'manual'` rule per the design doc's policy model
|
|
23
23
|
* ("Advisory / best-practice rules may exist, but must not produce
|
|
24
24
|
* `fail`"). Matches the existing `page-title-patterns-manual.js`
|
|
25
25
|
* precedent: deterministic DOM analysis, no human required, but
|
|
@@ -61,7 +61,7 @@ const meta = {
|
|
|
61
61
|
function runInPage(ctx) {
|
|
62
62
|
const { document, root, helpers, rule } = ctx;
|
|
63
63
|
|
|
64
|
-
// Declared inside runInPage
|
|
64
|
+
// Declared inside runInPage; see scripts/build-core.js header
|
|
65
65
|
// ("runInPage MUST be self-contained").
|
|
66
66
|
function normalizeWs(s) {
|
|
67
67
|
return String(s || '')
|
|
@@ -91,7 +91,7 @@ function runInPage(ctx) {
|
|
|
91
91
|
|
|
92
92
|
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
93
93
|
// question "does this element sit inside a sectioning-content/<main>
|
|
94
|
-
// ancestor that suppresses its conditional implicit role"
|
|
94
|
+
// ancestor that suppresses its conditional implicit role": role-aware
|
|
95
95
|
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
96
96
|
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
97
97
|
// rather than a local tag-only copy. See that function's header comment
|
|
@@ -110,7 +110,7 @@ function runInPage(ctx) {
|
|
|
110
110
|
if (tag === 'main') return 'main';
|
|
111
111
|
if (tag === 'nav') return 'navigation';
|
|
112
112
|
if (tag === 'aside') {
|
|
113
|
-
// A named <aside> is never suppressed, even when nested
|
|
113
|
+
// A named <aside> is never suppressed, even when nested. It keeps
|
|
114
114
|
// "complementary" when it has an accessible name, even inside
|
|
115
115
|
// sectioning content. Matches landmark-unique's precedent.
|
|
116
116
|
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
@@ -6,25 +6,25 @@
|
|
|
6
6
|
* @check landmark-contentinfo-is-top-level
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary The contentinfo landmark must not be nested inside another landmark
|
|
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 the page contains at least one contentinfo
|
|
12
12
|
* candidate: explicit role="contentinfo", OR a <footer> with NO role
|
|
13
13
|
* attribute at all, regardless of nesting (see implementation notes on
|
|
14
|
-
* why candidate selection is
|
|
14
|
+
* why candidate selection is unconditional on purpose).
|
|
15
15
|
* @expectation
|
|
16
16
|
* No contentinfo candidate has an ancestor that is itself any landmark
|
|
17
17
|
* region. A contentinfo nested inside another landmark is not a
|
|
18
18
|
* top-level, whole-page footer region and confuses landmark-based
|
|
19
19
|
* navigation for assistive technology users.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* - Not WCAG-normative
|
|
21
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
22
22
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
23
23
|
* header comment for the shared rationale/precedent (this rule mirrors
|
|
24
24
|
* its structure with contentinfo/footer in place of banner/header).
|
|
25
25
|
* - Candidate selection (`isContentinfoCandidate`) requires the element to
|
|
26
26
|
* really carry the contentinfo role, via the suppression-aware
|
|
27
|
-
* `getLandmarkRole
|
|
27
|
+
* `getLandmarkRole`, same reasoning as landmark-banner-is-top-level.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
const id = 'landmark-contentinfo-is-top-level';
|
|
@@ -79,7 +79,7 @@ function runInPage(ctx) {
|
|
|
79
79
|
|
|
80
80
|
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
81
81
|
// question "does this element sit inside a sectioning-content/<main>
|
|
82
|
-
// ancestor that suppresses its conditional implicit role"
|
|
82
|
+
// ancestor that suppresses its conditional implicit role": role-aware
|
|
83
83
|
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
84
84
|
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
85
85
|
// rather than a local tag-only copy. See that function's header comment
|
|
@@ -98,7 +98,7 @@ function runInPage(ctx) {
|
|
|
98
98
|
if (tag === 'main') return 'main';
|
|
99
99
|
if (tag === 'nav') return 'navigation';
|
|
100
100
|
if (tag === 'aside') {
|
|
101
|
-
// A named <aside> is never suppressed, even when nested
|
|
101
|
+
// A named <aside> is never suppressed, even when nested. It keeps
|
|
102
102
|
// "complementary" when it has an accessible name, even inside
|
|
103
103
|
// sectioning content. Matches landmark-unique's precedent.
|
|
104
104
|
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
@@ -127,12 +127,12 @@ function runInPage(ctx) {
|
|
|
127
127
|
return getImplicitLandmarkRole(el);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// Candidate selection is
|
|
131
|
-
// === 'contentinfo'
|
|
130
|
+
// Candidate selection is NOT the same as getLandmarkRole()
|
|
131
|
+
// === 'contentinfo'; see the header comment above. A <footer> is
|
|
132
132
|
// a candidate purely by tag + absence of any role attribute, independent
|
|
133
133
|
// of whether sectioning-ancestor nesting would currently suppress its
|
|
134
134
|
// implicit role; an explicit role="contentinfo" is always a candidate too.
|
|
135
|
-
// A candidate must actually have the contentinfo role
|
|
135
|
+
// A candidate must actually have the contentinfo role: a <footer> inside
|
|
136
136
|
// article/aside/main/nav/section is not one, so flagging it as nested
|
|
137
137
|
// would report a landmark that does not exist.
|
|
138
138
|
function isContentinfoCandidate(el) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check landmark-main-is-top-level
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary The main landmark must not be nested inside another landmark
|
|
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 the page contains at least one main landmark
|
|
12
12
|
* (explicit role="main", or an implicit <main> element).
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
* whole-page main content area and confuses landmark-based navigation
|
|
17
17
|
* for assistive technology users.
|
|
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 (this rule mirrors
|
|
22
22
|
* its structure with main in place of banner/header).
|
|
23
23
|
* - Unlike landmark-banner-is-top-level/landmark-contentinfo-is-top-level
|
|
24
24
|
* (see that file's header comment), candidate selection here doesn't
|
|
25
25
|
* need to be unconditional: `<main>`'s implicit role is unconditional
|
|
26
|
-
* per HTML-AAM
|
|
27
|
-
* it
|
|
26
|
+
* per HTML-AAM. Unlike `<header>`/`<footer>`, nesting never suppresses
|
|
27
|
+
* it, so `getImplicitLandmarkRole`'s `main` branch is never subject to
|
|
28
28
|
* the self-defeating candidate-selection problem those two rules guard
|
|
29
29
|
* against.
|
|
30
30
|
*/
|
|
@@ -81,7 +81,7 @@ function runInPage(ctx) {
|
|
|
81
81
|
|
|
82
82
|
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
83
83
|
// question "does this element sit inside a sectioning-content/<main>
|
|
84
|
-
// ancestor that suppresses its conditional implicit role"
|
|
84
|
+
// ancestor that suppresses its conditional implicit role": role-aware
|
|
85
85
|
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
86
86
|
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
87
87
|
// rather than a local tag-only copy. See that function's header comment
|
|
@@ -100,7 +100,7 @@ function runInPage(ctx) {
|
|
|
100
100
|
if (tag === 'main') return 'main';
|
|
101
101
|
if (tag === 'nav') return 'navigation';
|
|
102
102
|
if (tag === 'aside') {
|
|
103
|
-
// A named <aside> is never suppressed, even when nested
|
|
103
|
+
// A named <aside> is never suppressed, even when nested. It keeps
|
|
104
104
|
// "complementary" when it has an accessible name, even inside
|
|
105
105
|
// sectioning content. Matches landmark-unique's precedent.
|
|
106
106
|
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
@@ -6,19 +6,19 @@
|
|
|
6
6
|
* @check landmark-no-duplicate-banner
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary A page must not have more than one banner landmark
|
|
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 the page contains at least one banner landmark
|
|
12
|
-
* (explicit role="banner", or an implicit, non-nested <header
|
|
12
|
+
* (explicit role="banner", or an implicit, non-nested <header>; see
|
|
13
13
|
* landmark-banner-is-top-level's implementation notes for the
|
|
14
14
|
* shared landmark-detection model).
|
|
15
15
|
* @expectation
|
|
16
16
|
* At most one banner landmark exists on the page. Per WAI-ARIA
|
|
17
17
|
* Authoring Practices, the banner landmark represents site-oriented
|
|
18
|
-
* content that identifies the page as a whole
|
|
18
|
+
* content that identifies the page as a whole, so having more than one
|
|
19
19
|
* is ambiguous for assistive technology users navigating by landmark.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* - Not WCAG-normative
|
|
21
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
22
22
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
23
23
|
* header comment for the shared rationale/precedent.
|
|
24
24
|
* - Flags every banner instance (not just the "extra" ones) when more
|
|
@@ -81,11 +81,11 @@ function runInPage(ctx) {
|
|
|
81
81
|
|
|
82
82
|
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
83
83
|
// question "does this element sit inside a sectioning-content/<main>
|
|
84
|
-
// ancestor that suppresses its conditional implicit role"
|
|
84
|
+
// ancestor that suppresses its conditional implicit role": role-aware
|
|
85
85
|
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
86
86
|
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
87
87
|
// rather than a local tag-only copy. See that function's header comment
|
|
88
|
-
// in src/core/aria-helpers.js for the full algorithm
|
|
88
|
+
// in src/core/aria-helpers.js for the full algorithm, e.g. an
|
|
89
89
|
// <aside role="dialog"> containing its own <header>, where the <header>
|
|
90
90
|
// keeps its banner role.
|
|
91
91
|
function hasSectioningAncestor(el, includeMain) {
|
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
* @check landmark-no-duplicate-contentinfo
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary A page must not have more than one contentinfo landmark
|
|
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 the page contains at least one contentinfo landmark
|
|
12
12
|
* (explicit role="contentinfo", or an implicit, non-nested <footer>).
|
|
13
13
|
* @expectation
|
|
14
|
-
* At most one contentinfo landmark exists on the page
|
|
14
|
+
* At most one contentinfo landmark exists on the page, mirroring
|
|
15
15
|
* landmark-no-duplicate-banner's rationale for contentinfo.
|
|
16
16
|
* @implementation-notes
|
|
17
|
-
* - Not WCAG-normative
|
|
17
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
18
18
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
19
19
|
* header comment for the shared rationale/precedent.
|
|
20
|
-
* - Only landmarks actually exposed to assistive technology can collide
|
|
20
|
+
* - Only landmarks actually exposed to assistive technology can collide,
|
|
21
21
|
* same as the sibling banner/main rules, avoiding hidden-duplicate false
|
|
22
22
|
* positives.
|
|
23
23
|
*/
|
|
@@ -74,7 +74,7 @@ function runInPage(ctx) {
|
|
|
74
74
|
|
|
75
75
|
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
76
76
|
// question "does this element sit inside a sectioning-content/<main>
|
|
77
|
-
// ancestor that suppresses its conditional implicit role"
|
|
77
|
+
// ancestor that suppresses its conditional implicit role": role-aware
|
|
78
78
|
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
79
79
|
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
80
80
|
// rather than a local tag-only copy. See that function's header comment
|
|
@@ -93,7 +93,7 @@ function runInPage(ctx) {
|
|
|
93
93
|
if (tag === 'main') return 'main';
|
|
94
94
|
if (tag === 'nav') return 'navigation';
|
|
95
95
|
if (tag === 'aside') {
|
|
96
|
-
// A named <aside> is never suppressed, even when nested
|
|
96
|
+
// A named <aside> is never suppressed, even when nested. It keeps
|
|
97
97
|
// "complementary" when it has an accessible name, even inside
|
|
98
98
|
// sectioning content. Matches landmark-unique's precedent.
|
|
99
99
|
if (!hasSectioningAncestor(el, false)) return 'complementary';
|