@surea11y/core 1.5.0 → 1.7.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 +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- 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 +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- 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 +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -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 +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -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 +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- 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 → role-img-text-alternative-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 +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- 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 +563 -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-complementary-is-top-level-manual.js +231 -0
- 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/password-paste-enabled-manual.js +255 -0
- 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 +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/bin/surea11y-core.js +0 -20
|
@@ -9,29 +9,38 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 2.4.4
|
|
11
11
|
* @applicability
|
|
12
|
-
* Elements matching `a[href]` with a
|
|
13
|
-
* name (programmatic first, then "name
|
|
14
|
-
* two-step resolution as `link-name-present
|
|
15
|
-
*
|
|
12
|
+
* Elements matching `a[href], area[href], [role="link"]` with a
|
|
13
|
+
* non-empty computed accessible name (programmatic first, then "name
|
|
14
|
+
* from content", same two-step resolution as `link-name-present`,
|
|
15
|
+
* same selector too). Links with no name at all are
|
|
16
|
+
* `link-name-present`'s concern, not this rule's.
|
|
16
17
|
* @expectation
|
|
17
18
|
* The link's full accessible name, normalized (trimmed, case-folded,
|
|
18
19
|
* trailing punctuation stripped), is not an exact match for a known
|
|
19
20
|
* non-descriptive phrase ("click here", "read more", "more", "here",
|
|
20
|
-
* "details", "link", etc
|
|
21
|
-
*
|
|
21
|
+
* "details", "link", etc., WCAG technique F84's known failure pattern
|
|
22
|
+
* for SC 2.4.4) or a bare file-format/type name ("HTML", "PDF", "EPUB",
|
|
23
|
+
* ...) with no adjacent context (an aria-describedby target, the
|
|
24
|
+
* enclosing list item/table cell/paragraph's own text, or (format
|
|
25
|
+
* names only) a table's first-row header) naming what it belongs to.
|
|
22
26
|
* @implementation-notes
|
|
23
|
-
* -
|
|
24
|
-
* phrase
|
|
27
|
+
* - EXACT match only, on purpose, against small, well-established
|
|
28
|
+
* phrase lists, not a substring/contains check. "Read more about our
|
|
25
29
|
* privacy policy" does not match "read more"; only the bare phrase
|
|
26
30
|
* alone does. This keeps false positives near zero at the cost of
|
|
27
31
|
* not catching every possible non-descriptive phrasing (e.g. "click
|
|
28
|
-
* this", a legitimate but uncommon variant, is not in
|
|
29
|
-
* -
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* a
|
|
34
|
-
*
|
|
32
|
+
* this", a legitimate but uncommon variant, is not in either list).
|
|
33
|
+
* - Adjacent-context detection climbs through a bare wrapping list into
|
|
34
|
+
* an outer list item, so "Ulysses" heading a list of per-format
|
|
35
|
+
* download links still counts as that list's context. The table-header
|
|
36
|
+
* signal is format-name-only: a header cell can be present and still
|
|
37
|
+
* say nothing about what a generic "Download" link in the same row
|
|
38
|
+
* leads to, so the boilerplate-phrase list doesn't get that credit.
|
|
39
|
+
* - Still `type: 'manual'` (cantTell-capped, never fail): finding no
|
|
40
|
+
* adjacent context is a strong signal, not proof that context doesn't
|
|
41
|
+
* exist elsewhere (a preceding heading several rows up, page-level
|
|
42
|
+
* framing) or that the surrounding text actually disambiguates.
|
|
43
|
+
* Flagging is for review, not a definitive violation.
|
|
35
44
|
* - Reuses the same accessible-name computation as `link-name-present`
|
|
36
45
|
* (`getAccessibleNameInfo` then `getContentNameInfo` as fallback), so
|
|
37
46
|
* this benefits from the same "name from content" recursion fix
|
|
@@ -43,7 +52,7 @@ const id = 'link-name-quality';
|
|
|
43
52
|
const meta = {
|
|
44
53
|
title: 'Link text should be descriptive, not generic',
|
|
45
54
|
description:
|
|
46
|
-
'Flags links whose full accessible name is a known non-descriptive phrase (e.g. "click here", "read more", "more"), for manual review of whether the purpose is clear
|
|
55
|
+
'Flags links whose full accessible name is a known non-descriptive phrase (e.g. "click here", "read more", "more") or a bare file-format name (e.g. "HTML", "PDF") with no adjacent context naming what it leads to, for manual review of whether the purpose is clear.',
|
|
47
56
|
i18n: {
|
|
48
57
|
titleKey: 'linkNameQuality_title',
|
|
49
58
|
descriptionKey: 'linkNameQuality_description'
|
|
@@ -92,6 +101,29 @@ function runInPage(ctx) {
|
|
|
92
101
|
'info'
|
|
93
102
|
]);
|
|
94
103
|
|
|
104
|
+
const FORMAT_NAME_LINK_TEXT = new Set([
|
|
105
|
+
'html',
|
|
106
|
+
'pdf',
|
|
107
|
+
'epub',
|
|
108
|
+
'txt',
|
|
109
|
+
'plain text',
|
|
110
|
+
'doc',
|
|
111
|
+
'docx',
|
|
112
|
+
'xml',
|
|
113
|
+
'zip',
|
|
114
|
+
'mp3',
|
|
115
|
+
'mp4',
|
|
116
|
+
'csv',
|
|
117
|
+
'xls',
|
|
118
|
+
'xlsx',
|
|
119
|
+
'ppt',
|
|
120
|
+
'pptx',
|
|
121
|
+
'json',
|
|
122
|
+
'rtf'
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const CONTEXT_BLOCK_TAGS = new Set(['td', 'th', 'p', 'dd', 'blockquote', 'figcaption', 'dt']);
|
|
126
|
+
|
|
95
127
|
function normalize(s) {
|
|
96
128
|
return (s == null ? '' : String(s))
|
|
97
129
|
.replace(/\s+/g, ' ')
|
|
@@ -101,7 +133,84 @@ function runInPage(ctx) {
|
|
|
101
133
|
.trim();
|
|
102
134
|
}
|
|
103
135
|
|
|
104
|
-
|
|
136
|
+
function ownDirectText(el) {
|
|
137
|
+
let out = '';
|
|
138
|
+
const kids = el.childNodes || [];
|
|
139
|
+
for (let i = 0; i < kids.length; i++) {
|
|
140
|
+
const n = kids[i];
|
|
141
|
+
if (n.nodeType === 3) out += n.nodeValue || '';
|
|
142
|
+
}
|
|
143
|
+
return out.replace(/\s+/g, ' ').trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isSubstantiveContext(text) {
|
|
147
|
+
return !!text && text.length >= 3 && /\p{L}/u.test(text);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Direct text of the nearest enclosing list item/table cell/paragraph,
|
|
151
|
+
// climbing through a wrapping <ul>/<ol> into an outer <li> when the
|
|
152
|
+
// immediate one carries none of its own (a heading li wrapping a
|
|
153
|
+
// nested list of links, e.g. "Ulysses" above per-format download
|
|
154
|
+
// links).
|
|
155
|
+
function nearestBlockContextText(el) {
|
|
156
|
+
let node = el.parentElement;
|
|
157
|
+
let liHops = 0;
|
|
158
|
+
while (node) {
|
|
159
|
+
const tag = (node.tagName || '').toLowerCase();
|
|
160
|
+
if (tag === 'li') {
|
|
161
|
+
const text = ownDirectText(node);
|
|
162
|
+
if (text) return text;
|
|
163
|
+
liHops += 1;
|
|
164
|
+
if (liHops >= 4) return '';
|
|
165
|
+
const list = node.parentElement;
|
|
166
|
+
node = list ? list.parentElement : null;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (CONTEXT_BLOCK_TAGS.has(tag)) return ownDirectText(node);
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function describedByContextText(el) {
|
|
176
|
+
const describedBy = el.getAttribute ? el.getAttribute('aria-describedby') : null;
|
|
177
|
+
if (!describedBy || !describedBy.trim() || !helpers.getTextFromIdRefs) return '';
|
|
178
|
+
try {
|
|
179
|
+
const info = helpers.getTextFromIdRefs(describedBy, ctx);
|
|
180
|
+
return info && info.text ? info.text.replace(/\s+/g, ' ').trim() : '';
|
|
181
|
+
} catch {
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// A table's first-row header text, when the link sits in a later row of
|
|
187
|
+
// the same table -- naming the row's subject is exactly what turns a
|
|
188
|
+
// bare format name ("HTML") into a link whose destination is clear.
|
|
189
|
+
function firstRowHeaderText(el) {
|
|
190
|
+
const cell = el.closest ? el.closest('td, th') : null;
|
|
191
|
+
if (!cell) return '';
|
|
192
|
+
const table = cell.closest ? cell.closest('table') : null;
|
|
193
|
+
if (!table || !table.rows || !table.rows.length) return '';
|
|
194
|
+
const headerRow = table.rows[0];
|
|
195
|
+
const cellRow = cell.closest ? cell.closest('tr') : null;
|
|
196
|
+
if (!cellRow || headerRow === cellRow) return '';
|
|
197
|
+
const ths = headerRow.querySelectorAll ? headerRow.querySelectorAll('th') : [];
|
|
198
|
+
if (!ths.length) return '';
|
|
199
|
+
return Array.prototype.map
|
|
200
|
+
.call(ths, (th) => th.textContent || '')
|
|
201
|
+
.join(' ')
|
|
202
|
+
.replace(/\s+/g, ' ')
|
|
203
|
+
.trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasAdequateContext(el, tier) {
|
|
207
|
+
if (isSubstantiveContext(describedByContextText(el))) return true;
|
|
208
|
+
if (isSubstantiveContext(nearestBlockContextText(el))) return true;
|
|
209
|
+
if (tier === 'format' && isSubstantiveContext(firstRowHeaderText(el))) return true;
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const selector = 'a[href], area[href], [role="link"]';
|
|
105
214
|
const nodes = helpers.queryAllSmart
|
|
106
215
|
? helpers.queryAllSmart(selector)
|
|
107
216
|
: helpers.queryAll(selector);
|
|
@@ -131,27 +240,47 @@ function runInPage(ctx) {
|
|
|
131
240
|
|
|
132
241
|
applicableCount += 1;
|
|
133
242
|
|
|
134
|
-
|
|
243
|
+
const isGeneric = GENERIC_LINK_TEXT.has(normalized);
|
|
244
|
+
const isFormatName = !isGeneric && FORMAT_NAME_LINK_TEXT.has(normalized);
|
|
245
|
+
if (!isGeneric && !isFormatName) continue;
|
|
246
|
+
|
|
247
|
+
const tier = isGeneric ? 'generic' : 'format';
|
|
248
|
+
if (hasAdequateContext(el, tier)) continue;
|
|
135
249
|
|
|
136
250
|
const eligInfo = helpers.getEligibilityInfo
|
|
137
251
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
138
252
|
: null;
|
|
139
253
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
254
|
+
const name = rawName.trim();
|
|
255
|
+
const reportOpts = isGeneric
|
|
256
|
+
? {
|
|
257
|
+
summary: `This link's accessible name ("${name}") is a generic, non-descriptive phrase.`,
|
|
258
|
+
hint: 'Make the link text itself describe its destination/purpose (e.g. "Download the 2026 pricing guide" instead of "Download"), or confirm the surrounding context already makes the purpose clear.',
|
|
259
|
+
i18n: {
|
|
260
|
+
summaryKey: 'linkNameQuality_summary_cantTell',
|
|
261
|
+
hintKey: 'linkNameQuality_hint_cantTell',
|
|
262
|
+
params: { name }
|
|
263
|
+
},
|
|
264
|
+
data: {
|
|
265
|
+
details: { reasonCode: 'GENERIC_LINK_TEXT', normalizedName: normalized },
|
|
266
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
267
|
+
}
|
|
152
268
|
}
|
|
153
|
-
|
|
154
|
-
|
|
269
|
+
: {
|
|
270
|
+
summary: `This link's accessible name ("${name}") names a file format/type but not the document it belongs to.`,
|
|
271
|
+
hint: 'Name the document in the link text or in nearby text/a heading the link is associated with (e.g. "Download the annual report (HTML)" instead of a bare "HTML").',
|
|
272
|
+
i18n: {
|
|
273
|
+
summaryKey: 'linkNameQuality_summary_cantTell_formatName',
|
|
274
|
+
hintKey: 'linkNameQuality_hint_cantTell_formatName',
|
|
275
|
+
params: { name }
|
|
276
|
+
},
|
|
277
|
+
data: {
|
|
278
|
+
details: { reasonCode: 'AMBIGUOUS_FORMAT_NAME', normalizedName: normalized },
|
|
279
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
occurrences.push(helpers.reportOccurrence(el, reportOpts));
|
|
155
284
|
}
|
|
156
285
|
|
|
157
286
|
if (applicableCount === 0) {
|
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
const id = 'media-alternative-transcript-evidence';
|
|
19
19
|
|
|
20
20
|
const meta = {
|
|
21
|
-
title: 'Time-based media: transcript
|
|
21
|
+
title: 'Time-based media: transcript or text alternative evidence',
|
|
22
22
|
description:
|
|
23
|
-
'Finds
|
|
24
|
-
'This rule is conservative and returns cantTell when evidence is missing or unverified.',
|
|
23
|
+
'Finds audio and video elements where a transcript or other text alternative is not strongly evidenced in the page content. This rule is conservative and reports cantTell when evidence is missing or cannot be verified.',
|
|
25
24
|
i18n: {
|
|
26
25
|
titleKey: 'mediaTranscriptPresent_title',
|
|
27
26
|
descriptionKey: 'mediaTranscriptPresent_description'
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check meta-viewport-large
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Viewport meta tag should allow zooming up to 500% (AAA-level)
|
|
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 <meta name="viewport"> elements that carry a non-empty
|
|
12
12
|
* content attribute.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* enforces the AA 200% minimum as a hard, WCAG-normative fail); this
|
|
18
18
|
* rule is advisory best-practice guidance toward the higher AAA bar.
|
|
19
19
|
* @implementation-notes
|
|
20
|
-
* - Not WCAG-normative
|
|
20
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
21
21
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
22
22
|
* header comment for the shared rationale/precedent.
|
|
23
23
|
* - Distinct, atomic decision from meta-viewport-zoom-enabled
|
|
@@ -18,31 +18,31 @@
|
|
|
18
18
|
* The element also carries at least one keyboard-reachable inline
|
|
19
19
|
* handler: `onkeydown`, `onkeyup`, `onkeypress` (the direct keyboard-
|
|
20
20
|
* event equivalents), or `onfocus`/`onblur` (the standard substitute
|
|
21
|
-
* for hover-triggered behavior
|
|
21
|
+
* for hover-triggered behavior: focus/blur are the keyboard-
|
|
22
22
|
* navigable analog to mouseover/mouseout, per WCAG technique G90).
|
|
23
23
|
* Otherwise the element's mouse-driven behavior (a hover tooltip, a
|
|
24
24
|
* custom dropdown, a drag interaction) has no way to be triggered by a
|
|
25
25
|
* keyboard-only user.
|
|
26
26
|
* @implementation-notes
|
|
27
27
|
* - Authored as `type: 'manual'` (cantTell-capped, never fail), not
|
|
28
|
-
* `automatic`: this can only see inline `on*="..."` HTML attributes
|
|
29
|
-
*
|
|
28
|
+
* `automatic`: this can only see inline `on*="..."` HTML attributes.
|
|
29
|
+
* A keyboard handler attached elsewhere via `addEventListener` (the
|
|
30
30
|
* norm in most modern frameworks) is invisible to a static markup
|
|
31
31
|
* scan and would make a `fail` a false positive. Surfaced by a diff
|
|
32
|
-
* against a legacy ruleset's WCAG2AA rules
|
|
33
|
-
*
|
|
32
|
+
* against a legacy ruleset's WCAG2AA rules; this is a real, well-known
|
|
33
|
+
* WCAG 2.1.1 anti-pattern
|
|
34
34
|
* (technique G90/F54) that nothing else in this rule set checks.
|
|
35
|
-
* -
|
|
36
|
-
*
|
|
35
|
+
* - Does NOT treat `onclick` as a keyboard-equivalent excuse on purpose:
|
|
36
|
+
* whether `onclick` is keyboard-reachable depends on the
|
|
37
37
|
* element's separate focusability (native interactive tag or
|
|
38
|
-
* `tabindex`), which this rule does not attempt to cross-check
|
|
38
|
+
* `tabindex`), which this rule does not attempt to cross-check, and
|
|
39
39
|
* for the specific hover-triggered handlers this rule targets
|
|
40
40
|
* (`onmouseover`/`onmouseout`/etc.), `onclick` isn't actually an
|
|
41
41
|
* equivalent interaction model regardless of focusability (hover and
|
|
42
42
|
* click are different gestures with different semantics).
|
|
43
43
|
* - Only inline HTML attribute handlers are detectable; JS-attached
|
|
44
44
|
* listeners (`addEventListener('mouseover', ...)`) are invisible to a
|
|
45
|
-
* static DOM scan
|
|
45
|
+
* static DOM scan. That's a documented limitation, not an oversight.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
48
|
const id = 'mouse-only-event-handlers';
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* SC 1.4.2 only applies when audio plays automatically for MORE than 3
|
|
15
15
|
* seconds; clip duration is not knowable from static markup (jsdom does
|
|
16
16
|
* not decode media), so this rule cannot determine whether the SC even
|
|
17
|
-
* applies to a given element. It is
|
|
18
|
-
* 'manual'` (cantTell-capped, never fail) rather than guessing: an
|
|
17
|
+
* applies to a given element. It is authored as `type:
|
|
18
|
+
* 'manual'` (cantTell-capped, never fail) on purpose rather than guessing: an
|
|
19
19
|
* autoplaying unmuted element with no `controls` attribute (the native,
|
|
20
20
|
* statically-verifiable mechanism to pause/stop or adjust volume) is
|
|
21
21
|
* flagged for human review rather than treated as a deterministic
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* audible, so the SC's condition ("plays automatically... audio")
|
|
29
29
|
* does not apply.
|
|
30
30
|
* - Custom (JS-built) controls that don't use the native `controls`
|
|
31
|
-
* attribute cannot be detected statically
|
|
31
|
+
* attribute cannot be detected statically. That's a documented limitation,
|
|
32
32
|
* same class as `iframe-focusable-content`'s `contentDocument` gap.
|
|
33
33
|
* - Not gated on `isAccTreeEligible`: unlike most rules, a `display:none`
|
|
34
34
|
* or `aria-hidden` audio/video element still plays audible sound in a
|
|
@@ -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 <object> elements that already carry a text alternative:
|
|
14
|
+
* fallback text content, a non-empty aria-label, an aria-labelledby that
|
|
15
|
+
* resolves to non-empty text, or a non-empty title. An <object> with none
|
|
16
|
+
* of those is object-text-alternative-present's failure, not a quality
|
|
17
|
+
* question. The element must be included in the accessibility tree, and
|
|
18
|
+
* role="presentation"/"none" takes it out of scope unless it is focusable,
|
|
19
|
+
* 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
|
*/
|
|
@@ -17,18 +17,18 @@
|
|
|
17
17
|
* Text styled to visually read as a heading (bold, larger-than-body
|
|
18
18
|
* size, short) should be marked up with a real heading element
|
|
19
19
|
* (`<h1>`-`<h6>` or `role="heading"`) so its structural role is
|
|
20
|
-
* programmatically determinable
|
|
20
|
+
* programmatically determinable, the same 1.3.1 concern as any other
|
|
21
21
|
* "structure conveyed through presentation only" issue.
|
|
22
22
|
* @implementation-notes
|
|
23
23
|
* - This is a stylistic heuristic (bold + large + short), not a
|
|
24
|
-
* deterministic structural check
|
|
24
|
+
* deterministic structural check: a short bold sentence is not
|
|
25
25
|
* necessarily wrong as a `<p>`. Authored as `type: 'manual'`
|
|
26
26
|
* (cantTell-capped, never fail) to avoid false-flagging legitimate
|
|
27
27
|
* emphasis, matching this repo's other heuristic-heavy Tier 2/3
|
|
28
28
|
* rules (e.g. `scrollable-region-focusable`).
|
|
29
29
|
* - Uses an absolute 18px size threshold rather than comparing against
|
|
30
|
-
* surrounding text (unlike `link-in-text-block`)
|
|
31
|
-
*
|
|
30
|
+
* surrounding text (unlike `link-in-text-block`); it's simpler on
|
|
31
|
+
* purpose, since "looks like a heading" is closer to an absolute
|
|
32
32
|
* judgment than a relative-contrast one.
|
|
33
33
|
*/
|
|
34
34
|
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* @check page-has-heading-one
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary The page should have at least one level-one heading
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
|
-
* Always applicable to any HTML document with a <body> element
|
|
11
|
+
* Always applicable to any HTML document with a <body> element:
|
|
12
12
|
* "does the page have an h1" is a whole-page concern, matching
|
|
13
13
|
* bypass-blocks-present's pattern of evaluating the document
|
|
14
14
|
* directly.
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
* heading has no clear entry point for assistive technology users
|
|
19
19
|
* navigating by heading.
|
|
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
|
* - Filters candidates through `isAccTreeEligible` (hidden/aria-hidden/
|
|
25
25
|
* display:none/inert elements don't count as "the page has a heading
|
|
26
|
-
* one"), matching `landmark-one-main`'s own precedent
|
|
26
|
+
* one"), matching `landmark-one-main`'s own precedent. A raw
|
|
27
27
|
* `document.querySelectorAll` would wrongly credit an `<h1>` that sits
|
|
28
|
-
* inside a `display:none` ancestor,
|
|
28
|
+
* inside a `display:none` ancestor, unreachable by sighted and
|
|
29
29
|
* screen reader users alike, as satisfying this check. This does NOT
|
|
30
30
|
* regress purely-visually-clipped-but-AT-exposed headings (e.g. an `<h1>`
|
|
31
31
|
* hidden via clip-path/off-screen positioning, `visibility:visible`, no
|
|
32
|
-
* `aria-hidden`)
|
|
32
|
+
* `aria-hidden`): `isAccTreeEligible` only excludes elements actually
|
|
33
33
|
* removed from the accessibility tree, not ones merely clipped from the
|
|
34
34
|
* visual viewport.
|
|
35
35
|
*/
|
|
@@ -2,13 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check page-title-patterns
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Manual review: page titles that may not describe their page
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 2.4.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to a run over a whole document whose <title> resolves to
|
|
13
|
+
* non-empty text; a missing or empty title is page-title-present's
|
|
14
|
+
* failure, not a pattern to review. A run narrowed by contextSelector or
|
|
15
|
+
* by engineOptions.fragment is notApplicable, as is a title matching none
|
|
16
|
+
* of the patterns below.
|
|
17
|
+
* @expectation
|
|
18
|
+
* The title carries none of the conservative low-descriptiveness signals:
|
|
19
|
+
* one of the generic titles home, homepage, welcome, untitled, page or
|
|
20
|
+
* document; fewer than eight characters; or a template shape pairing a
|
|
21
|
+
* generic token with a brand, such as "Home | Brand". When the
|
|
22
|
+
* crawl.pageTitles probe supplies at least ten pages, cross-page signals
|
|
23
|
+
* are used instead: one title repeated across distinct URLs, or a prefix
|
|
24
|
+
* or suffix of twelve characters or more shared across the set. Every
|
|
25
|
+
* signal is reported as cantTell: whether a title describes its page is a
|
|
26
|
+
* judgment, so the rule never fails on a pattern alone.
|
|
27
|
+
*/
|
|
28
|
+
|
|
5
29
|
const id = 'page-title-patterns';
|
|
6
30
|
|
|
7
31
|
const meta = {
|
|
8
|
-
title: 'Page title patterns that may
|
|
32
|
+
title: 'Page title patterns that may be insufficiently descriptive',
|
|
9
33
|
description:
|
|
10
|
-
'
|
|
11
|
-
'This rule is conservative and does not fail based on patterns alone.',
|
|
34
|
+
'Identifies page title patterns that may indicate low descriptiveness, such as generic, duplicated, or overly templated titles. This rule provides review signals and does not fail automatically.',
|
|
12
35
|
i18n: {
|
|
13
36
|
titleKey: 'pageTitlePatterns_title',
|
|
14
37
|
descriptionKey: 'pageTitlePatterns_description'
|