@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
|
@@ -6,28 +6,29 @@
|
|
|
6
6
|
* @check presentation-role-conflict
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary role="presentation"/"none" must not be combined with a global ARIA naming attribute or focusability
|
|
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 elements with an explicit role="presentation" or
|
|
12
|
-
* role="none", OR an <img alt="">
|
|
13
|
-
* presentation role per HTML-AAM,
|
|
14
|
-
* at all
|
|
12
|
+
* role="none", OR an <img alt=""> carrying no explicit role of its own
|
|
13
|
+
* (empty alt gives an <img> an implicit presentation role per HTML-AAM,
|
|
14
|
+
* even with no explicit role attribute at all: `img[alt=''],
|
|
15
|
+
* [role="none"], [role="presentation"]`).
|
|
15
16
|
* @expectation
|
|
16
17
|
* The element does not also carry a WAI-ARIA *global* state/property
|
|
17
18
|
* (aria-label, aria-hidden, aria-describedby, aria-live, aria-current,
|
|
18
|
-
*
|
|
19
|
+
* ...; the full global-attribute set, not just the naming ones), AND
|
|
19
20
|
* is not focusable. Per the WAI-ARIA spec's Presentational Roles
|
|
20
21
|
* Conflict Resolution section, a presentational role is "restored" to
|
|
21
|
-
* the element's implicit semantic role when either condition holds
|
|
22
|
+
* the element's implicit semantic role when either condition holds:
|
|
22
23
|
* the presentation/none role silently stops working, contradicting the
|
|
23
24
|
* author's evident intent to hide the element from the accessibility
|
|
24
25
|
* tree.
|
|
25
26
|
* @implementation-notes
|
|
26
|
-
* - Not WCAG-normative
|
|
27
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
27
28
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
28
29
|
* header comment for the shared rationale/precedent.
|
|
29
30
|
* - `aria-hidden="true"` (the exact valid truthy value) on the
|
|
30
|
-
* presentational element itself is
|
|
31
|
+
* presentational element itself is EXCLUDED as a trigger on purpose,
|
|
31
32
|
* even though it's a global ARIA attribute: it removes the element from
|
|
32
33
|
* the accessibility tree unconditionally, so the "role restoration"
|
|
33
34
|
* this rule warns about never actually reaches assistive tech, making a
|
|
@@ -38,18 +39,23 @@
|
|
|
38
39
|
* exemption (see the code comment at the check site).
|
|
39
40
|
* - The conflicting-attribute set is the full list of ARIA attributes
|
|
40
41
|
* marked `global: true`, not a narrower naming-only list.
|
|
41
|
-
* -
|
|
42
|
+
* - Not applying an implicit-role applicability gate on purpose, one that
|
|
42
43
|
* would make the check inapplicable to role="presentation" on elements
|
|
43
44
|
* with no native implicit role to suppress (e.g. `<div
|
|
44
|
-
* role="presentation" aria-hidden="true"
|
|
45
|
+
* role="presentation" aria-hidden="true">`: a <div> has no native role,
|
|
45
46
|
* so there's nothing for the presentational role to "conflict" with).
|
|
46
47
|
* surea11y stays broader/more cautious here rather than narrower, which
|
|
47
|
-
* is the safer direction to diverge in.
|
|
48
|
-
*
|
|
49
|
-
* in ROADMAP.md §7 item 9.
|
|
48
|
+
* is the safer direction to diverge in. Adding that gate later would
|
|
49
|
+
* need a native-implicit-role table, which doesn't exist yet.
|
|
50
50
|
* - Focusability is computed via helpers.getFocusableInfo (native +
|
|
51
|
-
* tabindex), same helper aria-hidden-focus already relies on
|
|
51
|
+
* tabindex), same helper aria-hidden-focus already relies on. A
|
|
52
52
|
* `:disabled` or otherwise non-focusable element is not flagged.
|
|
53
|
+
* - An `<img alt="">` carrying an explicit role of its own (e.g. `<img
|
|
54
|
+
* alt="" role="img" aria-label="Logo">`) is out of scope: the explicit
|
|
55
|
+
* role wins over the presentation role empty alt would otherwise confer,
|
|
56
|
+
* so nothing presentational is left to conflict with. An explicit role
|
|
57
|
+
* whose tokens are all unknown confers nothing either, and the empty alt
|
|
58
|
+
* still applies.
|
|
53
59
|
*/
|
|
54
60
|
|
|
55
61
|
const id = 'presentation-role-conflict';
|
|
@@ -77,7 +83,7 @@ function runInPage(ctx) {
|
|
|
77
83
|
const { helpers, rule } = ctx;
|
|
78
84
|
|
|
79
85
|
// The full set of ARIA attributes marked `global: true` per the WAI-ARIA
|
|
80
|
-
// spec
|
|
86
|
+
// spec. Any of these present on a presentational element restores its
|
|
81
87
|
// implicit role, not just the naming ones.
|
|
82
88
|
const CONFLICTING_ATTRS = [
|
|
83
89
|
'aria-atomic',
|
|
@@ -109,6 +115,26 @@ function runInPage(ctx) {
|
|
|
109
115
|
const getFocusableInfo =
|
|
110
116
|
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
111
117
|
|
|
118
|
+
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
119
|
+
|
|
120
|
+
// The role attribute holds a fallback list; the first token naming a real
|
|
121
|
+
// role wins, and unknown tokens are skipped over. Returns '' when the
|
|
122
|
+
// element has no role attribute or none of its tokens name a role: the
|
|
123
|
+
// cases where an <img alt=""> keeps the presentation role empty alt gives
|
|
124
|
+
// it.
|
|
125
|
+
function getEffectiveRoleToken(el) {
|
|
126
|
+
const raw = el.getAttribute ? el.getAttribute('role') : null;
|
|
127
|
+
if (!raw) return '';
|
|
128
|
+
const tokens = String(raw).trim().toLowerCase().split(/\s+/);
|
|
129
|
+
for (const token of tokens) {
|
|
130
|
+
if (!token) continue;
|
|
131
|
+
if (token === 'presentation' || token === 'none') return token;
|
|
132
|
+
const known = ariaHelpers ? ariaHelpers.isValidConcreteRole(token) : true;
|
|
133
|
+
if (known) return token;
|
|
134
|
+
}
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
|
|
112
138
|
const nodes = helpers.queryAllSmart
|
|
113
139
|
? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]')
|
|
114
140
|
: helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]');
|
|
@@ -119,30 +145,37 @@ function runInPage(ctx) {
|
|
|
119
145
|
for (const el of nodes) {
|
|
120
146
|
if (!el || !el.getAttribute) continue;
|
|
121
147
|
|
|
148
|
+
// Only reachable via the img[alt=""] branch of the selector: an explicit
|
|
149
|
+
// role other than presentation/none overrides the presentation role that
|
|
150
|
+
// empty alt would confer, leaving no presentational intent to conflict
|
|
151
|
+
// with.
|
|
152
|
+
const roleToken = getEffectiveRoleToken(el);
|
|
153
|
+
if (roleToken && roleToken !== 'presentation' && roleToken !== 'none') continue;
|
|
154
|
+
|
|
122
155
|
applicableCount += 1;
|
|
123
156
|
|
|
124
157
|
// Presence, not value truthiness: the WAI-ARIA role-conflict-resolution
|
|
125
158
|
// rule triggers on a global ARIA attribute being SPECIFIED at all, even
|
|
126
|
-
// with an empty value
|
|
159
|
+
// with an empty value, e.g. <img alt="" aria-hidden="">, where
|
|
127
160
|
// aria-hidden="" (empty string) is still a specified attribute. A
|
|
128
161
|
// truthy-value check would miss this.
|
|
129
162
|
let present = CONFLICTING_ATTRS.filter((attr) =>
|
|
130
163
|
el.hasAttribute ? el.hasAttribute(attr) : el.getAttribute(attr) != null
|
|
131
164
|
);
|
|
132
165
|
|
|
133
|
-
// aria-hidden="true" (the exact, valid truthy value
|
|
166
|
+
// aria-hidden="true" (the exact, valid truthy value, not the
|
|
134
167
|
// empty-string case above, which never actually hides anything) is a
|
|
135
168
|
// special case: it removes the element and its subtree from the
|
|
136
169
|
// accessibility tree unconditionally, independent of role. That makes
|
|
137
170
|
// the "role restoration" this rule warns about ("...which restores its
|
|
138
171
|
// implicit role and cancels the presentational intent") factually
|
|
139
|
-
// inert
|
|
172
|
+
// inert. No AT will ever expose the restored role OR any of the other
|
|
140
173
|
// conflicting attributes (aria-label, aria-describedby, ...) present
|
|
141
174
|
// alongside it, since the whole element stays out of the tree
|
|
142
175
|
// regardless. This pattern is extremely common (e.g. <svg
|
|
143
|
-
// role="presentation" aria-hidden="true"> decorative icons
|
|
176
|
+
// role="presentation" aria-hidden="true"> decorative icons, a
|
|
144
177
|
// defensive belt-and-suspenders double-hide, not an authoring mistake).
|
|
145
|
-
// Focusability is NOT covered by this exemption
|
|
178
|
+
// Focusability is NOT covered by this exemption: a keyboard user can
|
|
146
179
|
// still tab onto an aria-hidden="true" focusable element (the
|
|
147
180
|
// aria-hidden-focus anti-pattern), a real, independent hazard
|
|
148
181
|
// aria-hidden does nothing to prevent.
|
|
@@ -165,12 +198,9 @@ function runInPage(ctx) {
|
|
|
165
198
|
const parts = present.slice();
|
|
166
199
|
if (isFocusable) parts.push('focusable');
|
|
167
200
|
|
|
168
|
-
// No
|
|
201
|
+
// No role token means this matched via the img[alt=""]
|
|
169
202
|
// implicit-presentation case.
|
|
170
|
-
const role =
|
|
171
|
-
String(el.getAttribute('role') || '')
|
|
172
|
-
.trim()
|
|
173
|
-
.toLowerCase() || 'presentation';
|
|
203
|
+
const role = roleToken || 'presentation';
|
|
174
204
|
|
|
175
205
|
occurrences.push(
|
|
176
206
|
helpers.reportOccurrence(el, {
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* @check region
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Page content should be contained within a landmark region
|
|
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 any element under <body> that directly carries visible text
|
|
12
|
-
* (or other own content
|
|
12
|
+
* (or other own content, see @implementation-notes) and is not itself a
|
|
13
13
|
* landmark, live region, dialog, button, <svg>, <iframe>/<frame>, or a
|
|
14
14
|
* resolvable skip-link.
|
|
15
15
|
* @expectation
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
* search), so assistive technology users navigating by landmark do not
|
|
19
19
|
* miss content that was never placed inside one.
|
|
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 and the landmark-
|
|
24
24
|
* detection model.
|
|
25
25
|
* - Recursive tree walk, not a direct-<body>-children-only scan: a
|
|
26
26
|
* direct-children-only scope is nearly inert on the single most common
|
|
27
27
|
* real-world page shape, a modern framework's single root mount div
|
|
28
|
-
* (`<body><div id="root">...everything...</div></body>`)
|
|
28
|
+
* (`<body><div id="root">...everything...</div></body>`). That shape
|
|
29
29
|
* gives at most one candidate for the entire page and either misses
|
|
30
30
|
* every real gap inside it or collapses the whole page into one
|
|
31
31
|
* undifferentiated report.
|
|
@@ -37,30 +37,30 @@
|
|
|
37
37
|
* live region, dialog, button, <svg>, <iframe>/<frame>, or a
|
|
38
38
|
* resolvable skip-link), mark it and every ancestor up to <body> as
|
|
39
39
|
* "has a stopper" and don't recurse further into it (an <iframe>/
|
|
40
|
-
* <frame> is additionally reported as its own occurrence
|
|
40
|
+
* <frame> is additionally reported as its own occurrence, since its
|
|
41
41
|
* content is opaque to this engine, so from the outer page's
|
|
42
42
|
* perspective it IS unplaced content).
|
|
43
43
|
* 3. Otherwise, if the node has OWN content (a direct child text node,
|
|
44
|
-
* being an inherently visual element, or an aria-label)
|
|
45
|
-
* non-recursively
|
|
46
|
-
* children never short-circuits the walk into its descendants
|
|
44
|
+
* being an inherently visual element, or an aria-label), checked
|
|
45
|
+
* non-recursively so a plain wrapper <div> with only nested
|
|
46
|
+
* children never short-circuits the walk into its descendants,
|
|
47
47
|
* collect it as a candidate and stop recursing into it.
|
|
48
48
|
* 4. Otherwise recurse into its element children.
|
|
49
49
|
* Each collected candidate is then walked back UP through parents while
|
|
50
50
|
* the parent has no "stopper" marker and isn't <body> itself, collapsing
|
|
51
51
|
* contiguous unplaced content into one occurrence per real gap instead
|
|
52
|
-
* of reporting every individual text-bearing leaf
|
|
52
|
+
* of reporting every individual text-bearing leaf. This is what keeps
|
|
53
53
|
* the walk from being noisy on ordinary pages that mix landmarked and
|
|
54
54
|
* stray content.
|
|
55
55
|
* - "Stopper" exemptions (button, dialog, <svg>, resolvable skip-links)
|
|
56
|
-
* are a
|
|
56
|
+
* are a scope choice, not an oversight: these
|
|
57
57
|
* are extremely common real-world patterns (floating action buttons,
|
|
58
58
|
* modal dialogs, decorative/icon SVGs, "skip to content" links) that
|
|
59
59
|
* aren't the kind of "content organization" gap this rule exists to
|
|
60
60
|
* catch, and flagging them would reintroduce the false-positive noise
|
|
61
61
|
* the original narrow scope was trying to avoid.
|
|
62
|
-
* - The "own content" check for aria-label
|
|
63
|
-
* aria-labelledby
|
|
62
|
+
* - The "own content" check for aria-label does NOT resolve
|
|
63
|
+
* aria-labelledby, a known, narrow scope gap (an element named only via aria-labelledby, with no
|
|
64
64
|
* own text/aria-label, and no other content anywhere in its subtree,
|
|
65
65
|
* could be silently skipped) accepted to avoid a full accessible-name
|
|
66
66
|
* computation (recursive itself) inside an already-recursive structural
|
|
@@ -137,11 +137,11 @@ function runInPage(ctx) {
|
|
|
137
137
|
|
|
138
138
|
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
139
139
|
// question "does this element sit inside a sectioning-content/<main>
|
|
140
|
-
// ancestor that suppresses its conditional implicit role"
|
|
140
|
+
// ancestor that suppresses its conditional implicit role": role-aware
|
|
141
141
|
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
142
142
|
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
143
143
|
// rather than a local tag-only copy. See that function's header comment
|
|
144
|
-
// in src/core/aria-helpers.js for the full algorithm
|
|
144
|
+
// in src/core/aria-helpers.js for the full algorithm, e.g. an
|
|
145
145
|
// <aside role="dialog"> containing its own <header>, where the <header>
|
|
146
146
|
// keeps its banner role.
|
|
147
147
|
function hasSectioningAncestor(el, includeMain) {
|
|
@@ -189,7 +189,7 @@ function runInPage(ctx) {
|
|
|
189
189
|
const SKIP_TAGS = new Set(['script', 'style', 'template', 'noscript', 'link', 'meta', 'title']);
|
|
190
190
|
|
|
191
191
|
// Roles/attributes that make an element its own self-contained
|
|
192
|
-
// announced area
|
|
192
|
+
// announced area: not literally a WAI-ARIA landmark, but not "content
|
|
193
193
|
// that needs a landmark" either.
|
|
194
194
|
const LIVE_REGION_ROLES = new Set(['alert', 'status', 'log', 'marquee', 'timer']);
|
|
195
195
|
|
|
@@ -217,8 +217,8 @@ function runInPage(ctx) {
|
|
|
217
217
|
return false;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
// A "skip to content" link is
|
|
221
|
-
//
|
|
220
|
+
// A "skip to content" link is placed outside the main content flow
|
|
221
|
+
// on purpose, at the very top of the page. Exempting it (when its
|
|
222
222
|
// fragment actually resolves to a real target, not a dead "#"
|
|
223
223
|
// placeholder) avoids flagging a helpful, common accessibility pattern
|
|
224
224
|
// as the very thing this rule is meant to catch.
|
|
@@ -249,8 +249,8 @@ function runInPage(ctx) {
|
|
|
249
249
|
|
|
250
250
|
const VISUAL_CONTENT_TAGS = new Set(['img', 'video', 'audio', 'canvas', 'object', 'embed']);
|
|
251
251
|
|
|
252
|
-
// Non-recursive "does THIS element, on its own, carry content" check
|
|
253
|
-
//
|
|
252
|
+
// Non-recursive "does THIS element, on its own, carry content" check.
|
|
253
|
+
// Mirrors only the direct-content half of getContentNameInfo on purpose,
|
|
254
254
|
// not a full name-from-content recursion: the whole point is to keep
|
|
255
255
|
// recursing through plain wrapper elements (a framework's root mount
|
|
256
256
|
// <div> included) until reaching the actual content-bearing node, rather
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check scope-attr-valid
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary The scope attribute must have a valid value
|
|
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 elements with a non-empty scope attribute.
|
|
12
12
|
* @expectation
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* assistive technology, silently losing the row/column header
|
|
16
16
|
* association it was meant to declare.
|
|
17
17
|
* @implementation-notes
|
|
18
|
-
* - Not WCAG-normative
|
|
18
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
19
19
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
20
20
|
* header comment for the shared rationale/precedent.
|
|
21
21
|
*/
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 2.1.1, 2.1.3
|
|
11
11
|
* @applicability
|
|
12
|
-
*
|
|
12
|
+
* Scoped on purpose to a fixed set of likely-to-scroll container tags
|
|
13
13
|
* (div, section, article, aside, main, nav, pre, table, blockquote, ul,
|
|
14
14
|
* ol, textarea) with computed `overflow-x`/`overflow-y` of `auto` or
|
|
15
|
-
* `scroll
|
|
15
|
+
* `scroll`, not every element on the page, to keep this deterministic
|
|
16
16
|
* and performant (same style of scope-down as `region`).
|
|
17
17
|
* @expectation
|
|
18
18
|
* A region whose CSS declares it may scroll (`auto`/`scroll`) should be
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
* scroll from), or the region itself carries a non-negative `tabindex`.
|
|
23
23
|
* @implementation-notes
|
|
24
24
|
* - jsdom does not perform layout, so `scrollHeight`/`clientHeight` are
|
|
25
|
-
* not available to confirm the region's content actually overflows
|
|
25
|
+
* not available to confirm the region's content actually overflows,
|
|
26
26
|
* only that the CSS declares it *may*. Many elements declare
|
|
27
27
|
* `overflow: auto` defensively without their content ever actually
|
|
28
28
|
* overflowing, which would be a false positive if treated as a hard
|
|
29
29
|
* `fail`. For that reason this is authored as `type: 'manual'`
|
|
30
|
-
* (cantTell-capped, never fail) rather than `automatic
|
|
31
|
-
* layout-dependent gap as `iframe-focusable-content`'s
|
|
30
|
+
* (cantTell-capped, never fail) rather than `automatic`, the same class
|
|
31
|
+
* of layout-dependent gap as `iframe-focusable-content`'s
|
|
32
32
|
* `contentDocument` limitation.
|
|
33
33
|
* - "Has a focusable descendant" is a presence check (link/button/form
|
|
34
34
|
* control/`[tabindex]`/`iframe`/`[contenteditable]`), not a full
|
|
35
|
-
* focusability computation (disabled state, visibility, etc.)
|
|
36
|
-
*
|
|
35
|
+
* focusability computation (disabled state, visibility, etc.). Kept
|
|
36
|
+
* simple on purpose, to keep this rule self-contained and fast.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
const id = 'scrollable-region-focusable';
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* @check skip-link
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary A "skip" link must resolve to a real, usable target
|
|
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 <a href="#fragment"> elements whose accessible name
|
|
12
12
|
* matches a common "skip to ..." / "jump to ..." authoring convention
|
|
13
|
-
* (case-insensitive "skip" or "jump to" in the name)
|
|
13
|
+
* (case-insensitive "skip" or "jump to" in the name), the recognizable
|
|
14
14
|
* pattern for a skip-navigation link, not every same-page anchor link
|
|
15
15
|
* on the page. "jump to" is included alongside "skip" since real skip
|
|
16
16
|
* links use both conventions (e.g. a "Jump to section" link, which a
|
|
17
17
|
* purely positional match would catch but a "skip"-only text pattern
|
|
18
|
-
* would miss). Text-pattern matching itself stays
|
|
19
|
-
* implementation-notes)
|
|
18
|
+
* would miss). Text-pattern matching itself stays intentional (see
|
|
19
|
+
* implementation-notes); this only widens the known-convention list.
|
|
20
20
|
* @expectation
|
|
21
21
|
* The link's fragment resolves to a real element in the document
|
|
22
22
|
* (via a matching id, or a legacy <a name="...">), and that target is
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* whose target is missing or effectively unusable does not provide a
|
|
26
26
|
* reliable bypass destination.
|
|
27
27
|
* @implementation-notes
|
|
28
|
-
* - Not WCAG-normative
|
|
28
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
29
29
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
30
30
|
* header comment for the shared rationale/precedent.
|
|
31
31
|
* - Keyed on the "skip" text-pattern convention rather than positional
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to inline <svg> elements that already carry a text alternative:
|
|
14
|
+
* non-empty <title> or <desc> text, a non-empty aria-label, or an
|
|
15
|
+
* aria-labelledby that resolves to non-empty text. <desc> counts here as
|
|
16
|
+
* something to review even though it never contributes to the accessible
|
|
17
|
+
* name: that distinction is svg-text-alternative-present's. The element
|
|
18
|
+
* must be included in the accessibility tree, and
|
|
19
|
+
* role="presentation"/"none" takes it out of scope unless it is focusable,
|
|
20
|
+
* which restores its role.
|
|
12
21
|
* @expectation
|
|
13
22
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
23
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check tabindex
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary tabindex should not be greater than 0
|
|
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 elements with a tabindex attribute whose value parses as
|
|
12
12
|
* a valid integer.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* page changes and usually indicates the natural DOM order should be
|
|
17
17
|
* fixed instead.
|
|
18
18
|
* @implementation-notes
|
|
19
|
-
* - Not WCAG-normative
|
|
19
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
20
20
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
21
21
|
* header comment for the shared rationale/precedent.
|
|
22
22
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check table-duplicate-name
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary A table's caption must not duplicate its summary attribute
|
|
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 <table> elements that have both a <caption> with text
|
|
12
12
|
* content and a (deprecated but still encountered) summary attribute.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* same thing, assistive technology that surfaces both announces the
|
|
17
17
|
* same text twice for one table.
|
|
18
18
|
* @implementation-notes
|
|
19
|
-
* - Not WCAG-normative
|
|
19
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
20
20
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
21
21
|
* header comment for the shared rationale/precedent.
|
|
22
22
|
* - Narrowly scoped to caption-vs-summary duplication specifically, not a
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* @implementation-notes
|
|
23
23
|
* - Heuristic (a legitimately narrow first data row is possible, if
|
|
24
24
|
* uncommon), so authored as `type: 'manual'` (cantTell-capped, never
|
|
25
|
-
* fail) rather than a hard fail
|
|
25
|
+
* fail) rather than a hard fail, the same conservative posture as
|
|
26
26
|
* `p-as-heading`.
|
|
27
27
|
* - Does not attempt full colspan/rowspan-aware column counting; "more
|
|
28
28
|
* than one cell" on another row is used as the multi-column signal,
|
|
@@ -121,7 +121,7 @@ function runInPage(ctx) {
|
|
|
121
121
|
selector: stableSelector,
|
|
122
122
|
html,
|
|
123
123
|
summary:
|
|
124
|
-
'This table has no <caption>, but its first row is a single cell sitting above multi-cell rows
|
|
124
|
+
'This table has no <caption>, but its first row is a single cell sitting above multi-cell rows, so it may be acting as a fake caption.',
|
|
125
125
|
hint: 'If this cell is meant to describe the table, use a real <caption> element instead of a lone first-row cell.',
|
|
126
126
|
i18n: {
|
|
127
127
|
summaryKey: 'tableFakeCaption_summary_cantTell',
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
* @expectation
|
|
14
14
|
* SC 1.2.2 requires captions for prerecorded synchronized media, but
|
|
15
15
|
* only when the video actually has an audio track that conveys
|
|
16
|
-
* information (a silent/decorative video needs none)
|
|
16
|
+
* information (a silent/decorative video needs none), which cannot be
|
|
17
17
|
* verified from static markup alone (jsdom does not decode media).
|
|
18
18
|
* This rule is therefore `type: 'manual'` (cantTell-capped, never
|
|
19
19
|
* fail), matching the precedent set by
|
|
20
20
|
* `media-alternative-transcript-evidence` for the same class
|
|
21
|
-
* of "normatively mapped but not
|
|
21
|
+
* of "normatively mapped but not statically verifiable" gap. A <video>
|
|
22
22
|
* with a `<track kind="captions">` (or `kind="subtitles"`, commonly
|
|
23
23
|
* used interchangeably in the wild even though captions and subtitles
|
|
24
24
|
* serve technically distinct purposes) whose `src` is non-empty is not
|
|
25
25
|
* flagged; everything else is flagged for human review.
|
|
26
26
|
* @implementation-notes
|
|
27
|
-
* - Does not attempt to verify the referenced track file's content
|
|
27
|
+
* - Does not attempt to verify the referenced track file's content,
|
|
28
28
|
* only that a captions/subtitles track is declared with a non-empty
|
|
29
29
|
* `src`.
|
|
30
30
|
*/
|
|
@@ -4,7 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @check manual-review
|
|
7
|
-
*
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Manual review of keyboard navigation and focus order
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 2.1.1
|
|
11
|
+
* @sc 2.4.3
|
|
12
|
+
* @sc 2.4.7
|
|
13
|
+
* @type manual
|
|
14
|
+
* @applicability
|
|
15
|
+
* Applies to every run, whatever the page contains. Keyboard operability
|
|
16
|
+
* and focus order are properties of the page as a whole, and no markup
|
|
17
|
+
* pattern rules the question out.
|
|
18
|
+
* @expectation
|
|
19
|
+
* Always cantTell, carrying one occurrence at the scan root. Whether focus
|
|
20
|
+
* can leave every component, whether the tab order follows the reading
|
|
21
|
+
* order, and whether the focus indicator stays visible in use all need a
|
|
22
|
+
* person driving the page. The rule exists so that need is stated in the
|
|
23
|
+
* results rather than left for the reader to remember.
|
|
8
24
|
*/
|
|
9
25
|
|
|
10
26
|
const id = 'manual-review';
|