pour-engine 1.28.0 → 1.30.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/README.md +1 -6
- package/config/project.config.js +1 -1
- package/engine/index.js +18 -17
- package/engine/rules/best-practice/accesskeys.js +1 -0
- package/engine/rules/best-practice/button-type.js +1 -0
- package/engine/rules/best-practice/empty-heading.js +1 -0
- package/engine/rules/best-practice/fieldset-legend.js +1 -0
- package/engine/rules/best-practice/heading-order.js +1 -0
- package/engine/rules/best-practice/landmark-one-main.js +1 -0
- package/engine/rules/best-practice/landmark-top-level.js +1 -0
- package/engine/rules/best-practice/landmark-unique.js +1 -0
- package/engine/rules/best-practice/multiple-labels.js +1 -0
- package/engine/rules/best-practice/new-window-link.js +1 -0
- package/engine/rules/best-practice/no-autofocus.js +1 -0
- package/engine/rules/best-practice/page-heading-one.js +1 -0
- package/engine/rules/best-practice/positive-tabindex.js +1 -0
- package/engine/rules/best-practice/redundant-alt-phrase.js +1 -0
- package/engine/rules/best-practice/redundant-aria-label.js +1 -0
- package/engine/rules/best-practice/redundant-aria.js +1 -0
- package/engine/rules/best-practice/redundant-image-alt.js +1 -0
- package/engine/rules/best-practice/redundant-role.js +1 -0
- package/engine/rules/best-practice/redundant-tabindex.js +1 -0
- package/engine/rules/best-practice/region.js +1 -0
- package/engine/rules/wcag/1.1.1-area-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-canvas-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-embed-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-image-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-input-image-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-object-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-svg-img-alt.js +1 -0
- package/engine/rules/wcag/1.2.2-media-captions.js +12 -5
- package/engine/rules/wcag/1.3.1-aria-required-children.js +1 -0
- package/engine/rules/wcag/1.3.1-aria-required-parent.js +1 -0
- package/engine/rules/wcag/1.3.1-definition-list.js +1 -0
- package/engine/rules/wcag/1.3.1-dlitem-parent.js +1 -0
- package/engine/rules/wcag/1.3.1-form-label.js +1 -0
- package/engine/rules/wcag/1.3.1-list-structure.js +1 -0
- package/engine/rules/wcag/1.3.1-listitem-parent.js +1 -0
- package/engine/rules/wcag/1.3.1-p-as-heading.js +1 -0
- package/engine/rules/wcag/1.3.1-table-headers.js +1 -0
- package/engine/rules/wcag/1.3.4-orientation.js +1 -0
- package/engine/rules/wcag/1.3.5-autocomplete-valid.js +1 -0
- package/engine/rules/wcag/1.4.1-link-in-text-block.js +1 -0
- package/engine/rules/wcag/1.4.10-reflow.js +1 -0
- package/engine/rules/wcag/1.4.11-non-text-contrast.js +1 -0
- package/engine/rules/wcag/1.4.12-text-spacing.js +1 -0
- package/engine/rules/wcag/1.4.2-audio-control.js +1 -0
- package/engine/rules/wcag/1.4.3-color-contrast.js +1 -0
- package/engine/rules/wcag/1.4.3-control-contrast.js +1 -0
- package/engine/rules/wcag/1.4.4-meta-viewport.js +1 -0
- package/engine/rules/wcag/1.4.6-contrast-enhanced.js +1 -0
- package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +1 -0
- package/engine/rules/wcag/2.2.1-meta-refresh.js +1 -0
- package/engine/rules/wcag/2.2.2-pause-stop-hide.js +1 -0
- package/engine/rules/wcag/2.2.2-video-loop-motion.js +1 -0
- package/engine/rules/wcag/2.4.1-bypass-blocks.js +1 -0
- package/engine/rules/wcag/2.4.11-focus-not-obscured.js +1 -0
- package/engine/rules/wcag/2.4.2-document-title.js +1 -0
- package/engine/rules/wcag/2.4.3-visual-order-divergence.js +1 -0
- package/engine/rules/wcag/2.4.4-link-name.js +1 -0
- package/engine/rules/wcag/2.4.4-link-text-generic.js +1 -0
- package/engine/rules/wcag/2.4.7-focus-visible.js +1 -0
- package/engine/rules/wcag/2.4.9-link-text-generic-only.js +1 -0
- package/engine/rules/wcag/2.5.3-label-in-name.js +1 -0
- package/engine/rules/wcag/2.5.5-target-size-enhanced.js +1 -0
- package/engine/rules/wcag/2.5.8-target-size.js +1 -0
- package/engine/rules/wcag/3.1.1-html-lang.js +1 -0
- package/engine/rules/wcag/3.1.2-valid-lang-parts.js +1 -0
- package/engine/rules/wcag/3.2.2-on-input-navigation.js +1 -0
- package/engine/rules/wcag/3.3.1-error-message-linkage.js +1 -0
- package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-attr-valid.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-field-name.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-hidden-focus.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-label-misuse.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-valid-refs.js +117 -37
- package/engine/rules/wcag/4.1.2-button-name.js +1 -0
- package/engine/rules/wcag/4.1.2-composite-widget-name.js +1 -0
- package/engine/rules/wcag/4.1.2-dialog-name.js +1 -0
- package/engine/rules/wcag/4.1.2-frame-title.js +1 -0
- package/engine/rules/wcag/4.1.2-label-for-valid.js +1 -0
- package/engine/rules/wcag/4.1.2-nested-interactive.js +1 -0
- package/engine/rules/wcag/4.1.2-role-required-aria.js +1 -0
- package/engine/rules/wcag/4.1.2-summary-name.js +1 -0
- package/engine/rules/wcag/4.1.2-valid-role.js +1 -0
- package/package.json +1 -1
- package/engine/wcag30-draft.js +0 -149
|
@@ -48,6 +48,7 @@ function visualSequence(entries, rightToLeft) {
|
|
|
48
48
|
|
|
49
49
|
export default {
|
|
50
50
|
id: 'visual-order-divergence',
|
|
51
|
+
name: 'Focus order vs visual order',
|
|
51
52
|
impact: 'moderate',
|
|
52
53
|
tags: ['wcag2a', 'wcag243'],
|
|
53
54
|
help: 'CSS reordering should not make Tab jump against the visual order',
|
|
@@ -78,6 +78,7 @@ export function createLinkPurposeRule({ id, impact, tags, help, helpUrl, verdict
|
|
|
78
78
|
|
|
79
79
|
export default createLinkPurposeRule({
|
|
80
80
|
id: 'link-text-generic',
|
|
81
|
+
name: 'Generic link text',
|
|
81
82
|
impact: 'moderate',
|
|
82
83
|
tags: ['wcag2a', 'wcag244'],
|
|
83
84
|
help: 'Link text should describe where the link goes',
|
|
@@ -14,6 +14,7 @@ import { createLinkPurposeRule } from './2.4.4-link-text-generic.js';
|
|
|
14
14
|
|
|
15
15
|
export default createLinkPurposeRule({
|
|
16
16
|
id: 'link-text-generic-only',
|
|
17
|
+
name: 'Self-sufficient link text',
|
|
17
18
|
impact: 'moderate',
|
|
18
19
|
tags: ['wcag2aaa', 'wcag249'],
|
|
19
20
|
help: 'Link text alone must identify where the link goes (AAA)',
|
|
@@ -6,6 +6,7 @@ import { createTargetSizeRule } from './2.5.8-target-size.js';
|
|
|
6
6
|
|
|
7
7
|
export default createTargetSizeRule({
|
|
8
8
|
id: 'target-size-enhanced',
|
|
9
|
+
name: 'Enhanced target size',
|
|
9
10
|
tags: ['wcag2aaa', 'wcag255'],
|
|
10
11
|
help: 'Interactive targets must be at least 44×44 pixels (AAA)',
|
|
11
12
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html',
|
|
@@ -467,6 +467,7 @@ export function createTargetSizeRule({ id, tags, help, helpUrl, min, spacingExce
|
|
|
467
467
|
|
|
468
468
|
export default createTargetSizeRule({
|
|
469
469
|
id: 'target-size',
|
|
470
|
+
name: 'Target size',
|
|
470
471
|
tags: ['wcag22aa', 'wcag258'],
|
|
471
472
|
help: 'Interactive targets must be at least 24×24 pixels',
|
|
472
473
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html',
|
|
@@ -12,6 +12,7 @@ const LANG_PATTERN = /^([a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*|[xXiI](-[a-zA-Z0-9]{1,
|
|
|
12
12
|
|
|
13
13
|
export default {
|
|
14
14
|
id: 'html-lang',
|
|
15
|
+
name: 'Page language',
|
|
15
16
|
impact: 'serious',
|
|
16
17
|
tags: ['wcag2a', 'wcag311'],
|
|
17
18
|
help: 'The <html> element must declare a valid language',
|
|
@@ -6,6 +6,7 @@ const LANG_PATTERN = /^([a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*|[xXiI](-[a-zA-Z0-9]{1,
|
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
id: 'valid-lang-parts',
|
|
9
|
+
name: 'Part language tags',
|
|
9
10
|
impact: 'serious',
|
|
10
11
|
tags: ['wcag2aa', 'wcag312'],
|
|
11
12
|
help: 'lang attributes on page parts must be valid',
|
|
@@ -20,6 +20,7 @@ const NAVIGATES = /\blocation\s*(=|\.\s*(href|assign|replace|reload))|\.(submit|
|
|
|
20
20
|
|
|
21
21
|
export default {
|
|
22
22
|
id: 'on-input-navigation',
|
|
23
|
+
name: 'Unexpected context change',
|
|
23
24
|
impact: 'serious',
|
|
24
25
|
tags: ['wcag2a', 'wcag322', 'wcag321'],
|
|
25
26
|
help: 'Changing or focusing a control must not unexpectedly change context',
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// value except false/undefined — "spelling" and "grammar" count.
|
|
26
26
|
export default {
|
|
27
27
|
id: 'error-message-linkage',
|
|
28
|
+
name: 'Error message linkage',
|
|
28
29
|
impact: 'serious',
|
|
29
30
|
tags: ['wcag2a', 'wcag331'],
|
|
30
31
|
help: 'Fields marked invalid need their error described in text',
|
|
@@ -39,6 +39,7 @@ const HANDLED_ELSEWHERE = new Set(['label', 'labelledby']);
|
|
|
39
39
|
|
|
40
40
|
export default {
|
|
41
41
|
id: 'aria-allowed-attr',
|
|
42
|
+
name: 'Allowed ARIA attributes',
|
|
42
43
|
impact: 'moderate',
|
|
43
44
|
tags: ['wcag2a', 'wcag412'],
|
|
44
45
|
help: 'ARIA attributes must be supported by the element’s role',
|
|
@@ -6,6 +6,7 @@ const FOCUSABLE = 'a[href], area[href], button, input, select, textarea, summary
|
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
id: 'aria-hidden-focus',
|
|
9
|
+
name: 'Focusable hidden content',
|
|
9
10
|
impact: 'serious',
|
|
10
11
|
tags: ['wcag2a', 'wcag412'],
|
|
11
12
|
help: 'Nothing focusable may sit inside aria-hidden="true"',
|
|
@@ -42,6 +42,7 @@ const NAME_FROM_CONTENT = 'a[href], button, summary, h1, h2, h3, h4, h5, h6, th,
|
|
|
42
42
|
|
|
43
43
|
export default {
|
|
44
44
|
id: 'aria-label-misuse',
|
|
45
|
+
name: 'aria-label placement',
|
|
45
46
|
impact: 'moderate',
|
|
46
47
|
tags: ['wcag2a', 'wcag412'],
|
|
47
48
|
help: 'aria-label on a plain container is prohibited by ARIA, so its announcement is not guaranteed',
|
|
@@ -6,8 +6,108 @@ const REF_ATTRIBUTES = [
|
|
|
6
6
|
'aria-activedescendant', 'aria-owns', 'aria-errormessage', 'aria-details',
|
|
7
7
|
];
|
|
8
8
|
|
|
9
|
+
// Deferred-tooltip pattern: libraries (Deque's own docs component included)
|
|
10
|
+
// write aria-describedby="tooltip7" at rest and only CREATE #tooltip7 when
|
|
11
|
+
// the trigger is hovered or focused — which is exactly when a screen reader
|
|
12
|
+
// announces the description, so the pattern works. Only DESCRIPTIVE refs
|
|
13
|
+
// get this benefit of the doubt: a NAME that exists only on hover is broken
|
|
14
|
+
// for rotor navigation even when the deferral works, so aria-labelledby
|
|
15
|
+
// (and the rest) keep failing at rest.
|
|
16
|
+
const PROBEABLE = new Set(['aria-describedby', 'aria-details']);
|
|
17
|
+
// A handful of probes covers any real page; a template bug that dangles
|
|
18
|
+
// fifty refs is broken enough that the un-probed remainder failing as
|
|
19
|
+
// before is the right answer.
|
|
20
|
+
const PROBE_BUDGET = 8;
|
|
21
|
+
const PROBE_WAIT_MS = 250;
|
|
22
|
+
|
|
23
|
+
/** Fire the events a tooltip library listens for, wait for it to react,
|
|
24
|
+
* check whether the ids materialised, then put the page back. */
|
|
25
|
+
async function probeDeferred(element, ids) {
|
|
26
|
+
const over = ['pointerover', 'mouseover', 'mouseenter'];
|
|
27
|
+
const out = ['pointerout', 'mouseout', 'mouseleave'];
|
|
28
|
+
for (const type of over) element.dispatchEvent(new MouseEvent(type, { bubbles: type !== 'mouseenter' }));
|
|
29
|
+
element.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
|
|
30
|
+
try { element.focus?.({ preventScroll: true }); } catch { /* non-focusable */ }
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, PROBE_WAIT_MS));
|
|
32
|
+
const root = element.getRootNode();
|
|
33
|
+
const appeared = ids.every((id) => root.getElementById?.(id));
|
|
34
|
+
// Restore: the audit must leave the page as it found it, and later rules
|
|
35
|
+
// must not meet a tooltip that only exists because we hovered.
|
|
36
|
+
for (const type of out) element.dispatchEvent(new MouseEvent(type, { bubbles: type !== 'mouseleave' }));
|
|
37
|
+
element.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
|
|
38
|
+
try { element.blur?.(); } catch { /* ignore */ }
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
40
|
+
return appeared;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function inspect(element) {
|
|
44
|
+
// ARIA id references cannot cross shadow boundaries: they resolve only
|
|
45
|
+
// within the element's own root (document or shadow tree).
|
|
46
|
+
const root = element.getRootNode();
|
|
47
|
+
// Collapsed-disclosure pattern: a trigger with aria-expanded="false"
|
|
48
|
+
// whose aria-controls panel doesn't exist YET (rendered on expand) is
|
|
49
|
+
// ubiquitous and harmless while collapsed — name computation doesn't
|
|
50
|
+
// touch aria-controls, so nothing resolves wrongly today.
|
|
51
|
+
const collapsed = element.getAttribute('aria-expanded') === 'false';
|
|
52
|
+
const missing = [];
|
|
53
|
+
const ambiguous = [];
|
|
54
|
+
for (const attr of REF_ATTRIBUTES) {
|
|
55
|
+
for (const id of (element.getAttribute(attr) ?? '').split(/\s+/).filter(Boolean)) {
|
|
56
|
+
if (!root.getElementById?.(id)) {
|
|
57
|
+
if (attr === 'aria-controls' && collapsed) continue;
|
|
58
|
+
missing.push({ attr, id });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const count = root.querySelectorAll(`[id="${CSS.escape(id)}"]`).length;
|
|
62
|
+
if (count > 1) {
|
|
63
|
+
ambiguous.push(`${attr}="${id}" — ${count} elements share this id; the reference binds to the FIRST one in the DOM`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { missing, ambiguous };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function outcome({ missing, ambiguous }, probed, element, accessibleName) {
|
|
71
|
+
if (missing.length) {
|
|
72
|
+
// A dangling ref is skipped by the accname computation, so the HARM
|
|
73
|
+
// depends on what remains. A labelledby that dangles while the element
|
|
74
|
+
// still names itself (contents, aria-label) is inert clutter — real,
|
|
75
|
+
// fragile, validator-flagged, but announced correctly today. The same
|
|
76
|
+
// dangle on an otherwise nameless element is the 4.1.2 failure proper:
|
|
77
|
+
// names are consumed at REST (element lists, rotor), so the finding
|
|
78
|
+
// says which of the two this is instead of one blanket accusation.
|
|
79
|
+
const restingName = (accessibleName?.(element) ?? '').trim();
|
|
80
|
+
const detail = missing
|
|
81
|
+
.map(({ attr, id }) => {
|
|
82
|
+
if (attr === 'aria-labelledby') {
|
|
83
|
+
return restingName
|
|
84
|
+
? `aria-labelledby="${id}" points to nothing — the reference is ignored and this element's name ("${restingName.slice(0, 40)}") currently comes from elsewhere; announced correctly today, but the dead reference is fragile`
|
|
85
|
+
: `aria-labelledby="${id}" points to nothing and leaves this element without an accessible name`;
|
|
86
|
+
}
|
|
87
|
+
return `${attr}="${id}" points to nothing — assistive technology silently ignores it`;
|
|
88
|
+
})
|
|
89
|
+
.join('; ');
|
|
90
|
+
return {
|
|
91
|
+
status: 'fail',
|
|
92
|
+
message: `Broken ARIA references: ${detail}.${probed ? ' Probed with focus and hover in case the target is created on demand — it never appeared.' : ''}`,
|
|
93
|
+
fix: 'Correct or remove the reference, or give the target element that id.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (ambiguous.length) {
|
|
97
|
+
// Duplicate ids resolve deterministically to the first copy — often
|
|
98
|
+
// the right one (desktop/mobile double-renders). Real, but a human
|
|
99
|
+
// must judge whether the first copy is the wrong copy.
|
|
100
|
+
return {
|
|
101
|
+
status: 'incomplete',
|
|
102
|
+
message: `Ambiguous ARIA references: ${ambiguous.join('; ')}. If the first copy in the DOM is the intended target, this works today — but it's fragile.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { status: 'pass' };
|
|
106
|
+
}
|
|
107
|
+
|
|
9
108
|
export default {
|
|
10
109
|
id: 'aria-valid-refs',
|
|
110
|
+
name: 'ARIA id references',
|
|
11
111
|
// Moderate, not critical: with 4.1.1 Parsing retired in WCAG 2.2 these
|
|
12
112
|
// are 4.1.2 name/state defects, and aria-controls in particular has weak
|
|
13
113
|
// assistive-technology support — real, but rarely blocking.
|
|
@@ -16,46 +116,26 @@ export default {
|
|
|
16
116
|
help: 'ARIA id references must point to elements that exist',
|
|
17
117
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
|
|
18
118
|
selector: REF_ATTRIBUTES.map((attr) => `[${attr}]`).join(', '),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!root.getElementById?.(id)) {
|
|
33
|
-
if (attr === 'aria-controls' && collapsed) continue;
|
|
34
|
-
missing.push(`${attr}="${id}" points to nothing — assistive technology silently ignores it`);
|
|
119
|
+
async evaluateAll(elements, { accessibleName } = {}) {
|
|
120
|
+
let budget = PROBE_BUDGET;
|
|
121
|
+
const outcomes = new Array(elements.length);
|
|
122
|
+
for (let i = 0; i < elements.length; i++) {
|
|
123
|
+
const element = elements[i];
|
|
124
|
+
const found = inspect(element);
|
|
125
|
+
const deferredOnly = found.missing.length > 0
|
|
126
|
+
&& found.missing.every(({ attr }) => PROBEABLE.has(attr));
|
|
127
|
+
if (deferredOnly && budget > 0) {
|
|
128
|
+
budget--;
|
|
129
|
+
if (await probeDeferred(element, found.missing.map(({ id }) => id))) {
|
|
130
|
+
// The description materialises exactly when it would be announced.
|
|
131
|
+
outcomes[i] = { status: 'pass' };
|
|
35
132
|
continue;
|
|
36
133
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
ambiguous.push(`${attr}="${id}" — ${count} elements share this id; the reference binds to the FIRST one in the DOM`);
|
|
40
|
-
}
|
|
134
|
+
outcomes[i] = outcome(found, true, element, accessibleName);
|
|
135
|
+
continue;
|
|
41
136
|
}
|
|
137
|
+
outcomes[i] = outcome(found, false, element, accessibleName);
|
|
42
138
|
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
status: 'fail',
|
|
46
|
-
message: `Broken ARIA references: ${missing.join('; ')}.`,
|
|
47
|
-
fix: 'Correct or remove the reference, or give the target element that id.',
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
if (ambiguous.length) {
|
|
51
|
-
// Duplicate ids resolve deterministically to the first copy — often
|
|
52
|
-
// the right one (desktop/mobile double-renders). Real, but a human
|
|
53
|
-
// must judge whether the first copy is the wrong copy.
|
|
54
|
-
return {
|
|
55
|
-
status: 'incomplete',
|
|
56
|
-
message: `Ambiguous ARIA references: ${ambiguous.join('; ')}. If the first copy in the DOM is the intended target, this works today — but it's fragile.`,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
return { status: 'pass' };
|
|
139
|
+
return outcomes;
|
|
60
140
|
},
|
|
61
141
|
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// where the binary, assertable failure lives.
|
|
11
11
|
export default {
|
|
12
12
|
id: 'composite-widget-name',
|
|
13
|
+
name: 'Widget item names',
|
|
13
14
|
impact: 'serious',
|
|
14
15
|
tags: ['wcag2a', 'wcag412'],
|
|
15
16
|
help: 'Tabs, menu items, options and tree items need an accessible name',
|
|
@@ -3,6 +3,7 @@ const LABELABLE = 'input, select, textarea, button, meter, output, progress';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
id: 'label-for-valid',
|
|
6
|
+
name: 'Label target references',
|
|
6
7
|
impact: 'serious',
|
|
7
8
|
tags: ['wcag2a', 'wcag412'],
|
|
8
9
|
help: 'label[for] must reference a form control that exists',
|
package/package.json
CHANGED
package/engine/wcag30-draft.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
// The WCAG 3.0 guideline catalog — derived from the W3C Working DRAFT of
|
|
2
|
-
// 3 March 2026 (https://www.w3.org/TR/wcag-3.0/). WCAG 3.0 is a work in
|
|
3
|
-
// progress: the W3C says it "may be updated, replaced, or obsoleted" and it
|
|
4
|
-
// is not appropriate for conformance claims. Everything this module exports
|
|
5
|
-
// is therefore marked draft, and every UI that shows it must say so.
|
|
6
|
-
//
|
|
7
|
-
// The draft has no A/AA/AAA levels and no success criteria; it organizes
|
|
8
|
-
// outcome-style guidelines (with core/supplemental requirements below them)
|
|
9
|
-
// under twelve functional groups. Our automated rules are written against
|
|
10
|
-
// WCAG 2.x; `scToGuideline` maps each 2.x criterion we automate to the
|
|
11
|
-
// draft guideline that covers the same user need, judged by reading both
|
|
12
|
-
// specs side by side. Revisit the mapping with every new draft.
|
|
13
|
-
|
|
14
|
-
export const draftInfo = {
|
|
15
|
-
id: 'wcag30-draft',
|
|
16
|
-
name: 'WCAG 3.0',
|
|
17
|
-
status: 'W3C Working Draft',
|
|
18
|
-
dated: '2026-03-03',
|
|
19
|
-
draft: true,
|
|
20
|
-
url: 'https://www.w3.org/TR/wcag-3.0/',
|
|
21
|
-
note: 'A draft under active development: findings are WCAG 2.x rule results mapped to draft guidelines. Not suitable for conformance claims.',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const g = (num, name, group, anchor) => ({
|
|
25
|
-
num,
|
|
26
|
-
name,
|
|
27
|
-
group,
|
|
28
|
-
url: `https://www.w3.org/TR/wcag-3.0/#${anchor}`,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// All 44 guidelines of the 2026-03-03 draft, in document order.
|
|
32
|
-
export const guidelines = [
|
|
33
|
-
// 2.1 Images and media
|
|
34
|
-
g('2.1.1', 'Image alternatives', 'Images and media', 'image-alternatives'),
|
|
35
|
-
g('2.1.2', 'Media alternatives', 'Images and media', 'media-alternatives'),
|
|
36
|
-
g('2.1.3', 'Non-text alternatives', 'Images and media', 'non-text-alternatives'),
|
|
37
|
-
g('2.1.4', 'Captions', 'Images and media', 'captions'),
|
|
38
|
-
g('2.1.5', 'Audio descriptions', 'Images and media', 'audio-descriptions'),
|
|
39
|
-
g('2.1.6', 'Figure captions', 'Images and media', 'figure-captions'),
|
|
40
|
-
g('2.1.7', 'Single sense', 'Images and media', 'single-sense'),
|
|
41
|
-
// 2.2 Text and wording
|
|
42
|
-
g('2.2.1', 'Text appearance', 'Text and wording', 'text-appearance'),
|
|
43
|
-
g('2.2.2', 'Text-to-speech', 'Text and wording', 'text-to-speech'),
|
|
44
|
-
g('2.2.3', 'Clear language', 'Text and wording', 'clear-language'),
|
|
45
|
-
// 2.3 Interactive components
|
|
46
|
-
g('2.3.1', 'Keyboard focus appearance', 'Interactive components', 'keyboard-focus-appearance'),
|
|
47
|
-
g('2.3.2', 'Pointer focus appearance', 'Interactive components', 'pointer-focus-appearance'),
|
|
48
|
-
g('2.3.3', 'Navigating content', 'Interactive components', 'navigating-content'),
|
|
49
|
-
g('2.3.4', 'Expected behavior', 'Interactive components', 'expected-behavior'),
|
|
50
|
-
g('2.3.5', 'Control information', 'Interactive components', 'control-information'),
|
|
51
|
-
// 2.4 Input / operation
|
|
52
|
-
g('2.4.1', 'Keyboard interface input', 'Input and operation', 'keyboard-interface-input'),
|
|
53
|
-
g('2.4.2', 'Physical or cognitive effort when using keyboard', 'Input and operation', 'physical-or-cognitive-effort-when-using-keyboard'),
|
|
54
|
-
g('2.4.3', 'Pointer input', 'Input and operation', 'pointer-input'),
|
|
55
|
-
g('2.4.4', 'Speech and voice input', 'Input and operation', 'speech-and-voice-input'),
|
|
56
|
-
g('2.4.5', 'Input operation', 'Input and operation', 'input-operation-0'),
|
|
57
|
-
g('2.4.6', 'Authentication', 'Input and operation', 'authentication'),
|
|
58
|
-
// 2.5 Error handling
|
|
59
|
-
g('2.5.1', 'Correct errors', 'Error handling', 'correct-errors'),
|
|
60
|
-
g('2.5.2', 'Prevent errors', 'Error handling', 'prevent-errors'),
|
|
61
|
-
// 2.6 Animation and movement
|
|
62
|
-
g('2.6.1', 'Avoid physical harm', 'Animation and movement', 'avoid-physical-harm'),
|
|
63
|
-
// 2.7 Layout
|
|
64
|
-
g('2.7.1', 'Recognizable layouts', 'Layout', 'recognizable-layouts'),
|
|
65
|
-
g('2.7.2', 'User orientation', 'Layout', 'user-orientation'),
|
|
66
|
-
g('2.7.3', 'Structure', 'Layout', 'structure'),
|
|
67
|
-
g('2.7.4', 'No obstruction', 'Layout', 'no-obstruction'),
|
|
68
|
-
// 2.8 Consistency across views
|
|
69
|
-
g('2.8.1', 'Consistency', 'Consistency across views', 'consistency'),
|
|
70
|
-
// 2.9 Process and task completion
|
|
71
|
-
g('2.9.1', 'Avoid exclusionary cognitive tasks', 'Process and task completion', 'avoid-exclusionary-cognitive-tasks'),
|
|
72
|
-
g('2.9.2', 'Adequate time', 'Process and task completion', 'adequate-time'),
|
|
73
|
-
g('2.9.3', 'Avoid deception', 'Process and task completion', 'avoid-deception'),
|
|
74
|
-
g('2.9.4', 'Retain information', 'Process and task completion', 'retain-information'),
|
|
75
|
-
g('2.9.5', 'Complete tasks', 'Process and task completion', 'complete-tasks'),
|
|
76
|
-
g('2.9.6', 'Unnecessary steps', 'Process and task completion', 'unnecessary-steps'),
|
|
77
|
-
// 2.10 Policy and protection
|
|
78
|
-
g('2.10.1', 'Risk', 'Policy and protection', 'risk'),
|
|
79
|
-
g('2.10.2', 'Algorithms', 'Policy and protection', 'algorithms'),
|
|
80
|
-
// 2.11 Help and feedback
|
|
81
|
-
g('2.11.1', 'Help available', 'Help and feedback', 'help-available'),
|
|
82
|
-
g('2.11.2', 'Feedback', 'Help and feedback', 'feedback'),
|
|
83
|
-
// 2.12 User control
|
|
84
|
-
g('2.12.1', 'Assistive technology control', 'User control', 'assistive-technology-control'),
|
|
85
|
-
g('2.12.2', 'Control text', 'User control', 'control-text'),
|
|
86
|
-
g('2.12.3', 'Adjustable viewport', 'User control', 'adjustable-viewport'),
|
|
87
|
-
g('2.12.4', 'Media control', 'User control', 'media-control'),
|
|
88
|
-
g('2.12.5', 'Content changes', 'User control', 'content-changes'),
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
// WCAG 2.x criterion (as tagged on rules: wcag111 = SC 1.1.1) → the draft
|
|
92
|
-
// guideline covering the same user need. Only criteria with automated rules
|
|
93
|
-
// appear; the judgment is ours, made by reading both documents.
|
|
94
|
-
export const scToGuideline = {
|
|
95
|
-
111: '2.1.1', // Non-text Content → Image alternatives
|
|
96
|
-
122: '2.1.4', // Captions (Prerecorded) → Captions
|
|
97
|
-
131: '2.7.3', // Info and Relationships → Structure
|
|
98
|
-
135: '2.4.5', // Identify Input Purpose → Input operation
|
|
99
|
-
141: '2.2.1', // Use of Color (links in text) → Text appearance
|
|
100
|
-
142: '2.12.4', // Audio Control → Media control
|
|
101
|
-
143: '2.2.1', // Contrast (Minimum) → Text appearance
|
|
102
|
-
144: '2.12.3', // Resize Text (viewport zoom) → Adjustable viewport
|
|
103
|
-
146: '2.2.1', // Contrast (Enhanced) → Text appearance
|
|
104
|
-
211: '2.4.1', // Keyboard → Keyboard interface input
|
|
105
|
-
221: '2.9.2', // Timing Adjustable → Adequate time
|
|
106
|
-
222: '2.12.4', // Pause, Stop, Hide → Media control
|
|
107
|
-
241: '2.3.3', // Bypass Blocks → Navigating content
|
|
108
|
-
242: '2.7.2', // Page Titled → User orientation
|
|
109
|
-
244: '2.3.5', // Link Purpose (In Context) → Control information
|
|
110
|
-
255: '2.4.3', // Target Size (Enhanced) → Pointer input
|
|
111
|
-
258: '2.4.3', // Target Size (Minimum) → Pointer input
|
|
112
|
-
311: '2.2.2', // Language of Page → Text-to-speech
|
|
113
|
-
312: '2.2.2', // Language of Parts → Text-to-speech
|
|
114
|
-
332: '2.3.5', // Labels or Instructions → Control information
|
|
115
|
-
412: '2.12.1', // Name, Role, Value → Assistive technology control
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const guidelineByNum = new Map(guidelines.map((entry) => [entry.num, entry]));
|
|
119
|
-
const partlyAutomated = new Set(Object.values(scToGuideline));
|
|
120
|
-
|
|
121
|
-
/** The draft guideline (num/name/group/url) a rule's wcagNNN tag maps to. */
|
|
122
|
-
export function guidelineForTags(tags) {
|
|
123
|
-
for (const tag of tags ?? []) {
|
|
124
|
-
const sc = tag.match(/^wcag(\d{3,4})$/)?.[1];
|
|
125
|
-
if (sc && scToGuideline[sc]) return guidelineByNum.get(scToGuideline[sc]);
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The whole draft as a review checklist. Every guideline is listed — the
|
|
132
|
-
* draft is too young for any of it to be silently skipped — shaped like the
|
|
133
|
-
* 2.2 manual-review entries so existing UIs render it unchanged: `level` has
|
|
134
|
-
* no meaning in the draft (no conformance levels yet) and `principle`
|
|
135
|
-
* carries the draft's functional group.
|
|
136
|
-
*/
|
|
137
|
-
export function manualReviewChecklist() {
|
|
138
|
-
return guidelines.map(({ num, name, group, url }) => ({
|
|
139
|
-
num,
|
|
140
|
-
name,
|
|
141
|
-
level: 'draft',
|
|
142
|
-
principle: group,
|
|
143
|
-
url,
|
|
144
|
-
automation: partlyAutomated.has(num) ? 'partial' : 'manual',
|
|
145
|
-
draft: true,
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export default { draftInfo, guidelines, scToGuideline, guidelineForTags, manualReviewChecklist };
|