pour-engine 1.29.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 +15 -16
- 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 +1 -0
- 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
package/README.md
CHANGED
|
@@ -21,10 +21,6 @@ can verify are returned as a `manualReview` checklist instead of being
|
|
|
21
21
|
silently skipped, and findings the engine can't judge conclusively land in
|
|
22
22
|
`incomplete` rather than being guessed at.
|
|
23
23
|
|
|
24
|
-
An experimental WCAG 3.0 draft mode (`engine/wcag30-draft.js`) reframes
|
|
25
|
-
results against the draft's guidelines — clearly stamped as a draft, because
|
|
26
|
-
no tool can honestly claim conformance to an unfinished standard.
|
|
27
|
-
|
|
28
24
|
## Getting started
|
|
29
25
|
|
|
30
26
|
```sh
|
|
@@ -39,7 +35,6 @@ const results = await run(document, {
|
|
|
39
35
|
// optional: filter rules by tag. Tags are cumulative — WCAG 2.2 AA means
|
|
40
36
|
// every A/AA rule from 2.0 through 2.2. Omit to run everything.
|
|
41
37
|
tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'],
|
|
42
|
-
// standard: 'wcag30-draft', // optional: experimental WCAG 3.0 draft mode
|
|
43
38
|
});
|
|
44
39
|
|
|
45
40
|
console.log(results.violations, results.passes, results.incomplete);
|
|
@@ -89,7 +84,7 @@ static HTML strings. For Node-based testing, drive it through a real browser
|
|
|
89
84
|
(`1.1.1-image-alt.js`), plus the catalog index.
|
|
90
85
|
- `engine/lib/` — shared helpers: DOM traversal, accessible-name
|
|
91
86
|
computation, contrast math, role model.
|
|
92
|
-
- `engine/wcag22.js
|
|
87
|
+
- `engine/wcag22.js` — the WCAG catalog.
|
|
93
88
|
- `config/project.config.js` — package-local stand-in for the pour
|
|
94
89
|
monorepo's project config (engine name + version only).
|
|
95
90
|
|
package/config/project.config.js
CHANGED
package/engine/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import config from '../config/project.config.js';
|
|
5
5
|
import rules from './rules/index.js';
|
|
6
6
|
import wcagCatalog from './wcag22.js';
|
|
7
|
-
import wcag30Draft from './wcag30-draft.js';
|
|
8
7
|
import { isVisible, isRendered, cssPath, htmlSnippet, ownText, collectRoots } from './lib/dom.js';
|
|
9
8
|
import { accessibleName } from './lib/accessible-name.js';
|
|
10
9
|
import { resetAuditCaches } from './lib/contrast.js';
|
|
@@ -79,16 +78,11 @@ function toResultNode(element, outcome) {
|
|
|
79
78
|
/**
|
|
80
79
|
* Run the audit.
|
|
81
80
|
* @param {Document|Element} context - what to scan
|
|
82
|
-
* @param {{ tags?: string[], exclude?: string,
|
|
81
|
+
* @param {{ tags?: string[], exclude?: string, signal?: AbortSignal }} options -
|
|
83
82
|
* rule selection by tag (wcag2a…wcag22aa, best-practice), empty selects
|
|
84
83
|
* every rule; exclude is a CSS selector — elements matching it, or inside
|
|
85
84
|
* a match (including across shadow boundaries), are left out of every
|
|
86
|
-
* rule;
|
|
87
|
-
* Working Draft (results.standard is stamped draft, the manual checklist
|
|
88
|
-
* becomes the draft guidelines, and each rule result carries the draft
|
|
89
|
-
* guideline it maps to) — the rules themselves stay WCAG 2.x, which is
|
|
90
|
-
* the only honest claim a tool can make about a draft;
|
|
91
|
-
* signal aborts the run between rules (throws AbortError)
|
|
85
|
+
* rule; signal aborts the run between rules (throws AbortError)
|
|
92
86
|
* @param {(progress: object) => void} [onProgress] - called before each rule
|
|
93
87
|
* starts ({rule, running}), during a rule at each renderer yield with a
|
|
94
88
|
* fractional done (completed rules + this rule's evaluated-element share,
|
|
@@ -97,17 +91,15 @@ function toResultNode(element, outcome) {
|
|
|
97
91
|
*/
|
|
98
92
|
export async function run(context = document, options = {}, onProgress) {
|
|
99
93
|
const auditStarted = performance.now();
|
|
100
|
-
const draft30 = options.standard === 'wcag30-draft';
|
|
101
94
|
const results = {
|
|
102
95
|
testEngine: { name, version },
|
|
103
96
|
timestamp: new Date().toISOString(),
|
|
104
97
|
url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
|
|
105
|
-
standard: draft30 ? wcag30Draft.draftInfo : null,
|
|
106
98
|
violations: [],
|
|
107
99
|
passes: [],
|
|
108
100
|
incomplete: [],
|
|
109
101
|
inapplicable: [],
|
|
110
|
-
manualReview:
|
|
102
|
+
manualReview: manualReviewCriteria(options.tags),
|
|
111
103
|
ruleTimings: [],
|
|
112
104
|
};
|
|
113
105
|
|
|
@@ -223,17 +215,15 @@ export async function run(context = document, options = {}, onProgress) {
|
|
|
223
215
|
|
|
224
216
|
const ruleResult = {
|
|
225
217
|
id: rule.id,
|
|
218
|
+
// Human display name ("Form field labels"); the id stays the
|
|
219
|
+
// machine handle everywhere (selectors, exports, docs anchors).
|
|
220
|
+
name: rule.name ?? rule.id,
|
|
226
221
|
impact: rule.impact,
|
|
227
222
|
tags: rule.tags,
|
|
228
223
|
help: rule.help,
|
|
229
224
|
description: rule.help,
|
|
230
225
|
helpUrl: rule.helpUrl,
|
|
231
226
|
};
|
|
232
|
-
if (draft30) {
|
|
233
|
-
// Which draft guideline this 2.x rule speaks to (null for rules with
|
|
234
|
-
// no draft counterpart, e.g. best-practice extras).
|
|
235
|
-
ruleResult.wcag30 = wcag30Draft.guidelineForTags(rule.tags);
|
|
236
|
-
}
|
|
237
227
|
if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
|
|
238
228
|
if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
|
|
239
229
|
if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
|
|
@@ -252,6 +242,15 @@ export async function run(context = document, options = {}, onProgress) {
|
|
|
252
242
|
ms,
|
|
253
243
|
counts: { ...liveCounts },
|
|
254
244
|
typeCounts: { ...liveTypeCounts },
|
|
245
|
+
// The completed rule's actual findings, so a UI can BUILD its list
|
|
246
|
+
// as rules finish rather than delivering everything at the end.
|
|
247
|
+
// Present only when the rule found something.
|
|
248
|
+
...(buckets.fail.length || buckets.incomplete.length ? {
|
|
249
|
+
findings: {
|
|
250
|
+
...(buckets.fail.length ? { violation: { ...ruleResult, nodes: buckets.fail } } : {}),
|
|
251
|
+
...(buckets.incomplete.length ? { incomplete: { ...ruleResult, nodes: buckets.incomplete } } : {}),
|
|
252
|
+
},
|
|
253
|
+
} : {}),
|
|
255
254
|
});
|
|
256
255
|
}
|
|
257
256
|
|
|
@@ -11,6 +11,7 @@ const NAME_FROM_CONTENT = new Set(['button', 'link', 'menuitem', 'menuitemcheckb
|
|
|
11
11
|
|
|
12
12
|
export default {
|
|
13
13
|
id: 'redundant-aria-label',
|
|
14
|
+
name: 'Redundant aria-label',
|
|
14
15
|
impact: 'minor',
|
|
15
16
|
tags: ['best-practice'],
|
|
16
17
|
help: 'aria-label should not repeat the visible text',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// WCAG SC 1.1.1 Non-text Content (Level A) · SC 2.4.4 Link Purpose (In Context) (Level A)
|
|
2
2
|
export default {
|
|
3
3
|
id: 'area-alt',
|
|
4
|
+
name: 'Image map alt text',
|
|
4
5
|
impact: 'critical',
|
|
5
6
|
tags: ['wcag2a', 'wcag111', 'wcag244'],
|
|
6
7
|
help: 'Image map areas must have alternative text',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// WCAG SC 1.1.1 Non-text Content (Level A) · SC 4.1.2 Name, Role, Value (Level A)
|
|
2
2
|
export default {
|
|
3
3
|
id: 'input-image-alt',
|
|
4
|
+
name: 'Image button alt text',
|
|
4
5
|
impact: 'critical',
|
|
5
6
|
tags: ['wcag2a', 'wcag111', 'wcag412'],
|
|
6
7
|
help: 'Image inputs need alt text describing their action',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// WCAG SC 1.2.2 Captions (Prerecorded) (Level A)
|
|
2
2
|
export default {
|
|
3
3
|
id: 'media-captions',
|
|
4
|
+
name: 'Video captions',
|
|
4
5
|
impact: 'critical',
|
|
5
6
|
tags: ['wcag2a', 'wcag122'],
|
|
6
7
|
help: 'Video content must have captions',
|
|
@@ -21,17 +22,23 @@ export default {
|
|
|
21
22
|
if (!element.currentSrc && !element.getAttribute('src') && !element.querySelector('source')) {
|
|
22
23
|
return { status: 'pass' };
|
|
23
24
|
}
|
|
24
|
-
// The decorative no-audio-path signature
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// the file may carry is never presented to anyone; audio that cannot be
|
|
25
|
+
// The decorative no-audio-path signature: muted with no controls. With
|
|
26
|
+
// no controls there is no user path to unmute, so whatever audio the
|
|
27
|
+
// file may carry is never presented to anyone; audio that cannot be
|
|
28
28
|
// presented is not audio content under 1.2.2, and asking a human to
|
|
29
29
|
// verify captions for it is noise (measured on the benchmark corpus:
|
|
30
30
|
// our own site's aria-hidden hero loops, ffprobe-confirmed to carry no
|
|
31
31
|
// audio stream, plus the same family on four news/university homes).
|
|
32
|
+
// HOW playback starts is deliberately not part of the signature: an
|
|
33
|
+
// earlier autoplay condition missed the two other members of the same
|
|
34
|
+
// family, measured on our own site — a crossfade B-roll take that only
|
|
35
|
+
// ever plays via script (no autoplay attribute, so the review fired
|
|
36
|
+
// exactly when the script had wired its src), and heroes whose
|
|
37
|
+
// autoplay attribute a reduced-motion handler removes. Muteness and
|
|
38
|
+
// controls decide whether audio can reach anyone; autoplay never did.
|
|
32
39
|
// Properties, not attributes: script-muted heroes count too. A muted
|
|
33
40
|
// video WITH controls keeps its review — one click unmutes it.
|
|
34
|
-
if (element.muted &&
|
|
41
|
+
if (element.muted && !element.controls) {
|
|
35
42
|
return { status: 'pass' };
|
|
36
43
|
}
|
|
37
44
|
// Captions may be burned in or provided by the player — a human must check.
|
|
@@ -20,6 +20,7 @@ const IMPLICIT_CONTAINER = { ul: 'list', ol: 'list', menu: 'list', table: 'table
|
|
|
20
20
|
|
|
21
21
|
export default {
|
|
22
22
|
id: 'aria-required-parent',
|
|
23
|
+
name: 'Required ARIA parent',
|
|
23
24
|
impact: 'critical',
|
|
24
25
|
tags: ['wcag2a', 'wcag131'],
|
|
25
26
|
help: 'ARIA child roles must be inside their required container role',
|
|
@@ -3,6 +3,7 @@ import { parseColor, contrastRatio } from '../../lib/contrast.js';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
id: 'link-in-text-block',
|
|
6
|
+
name: 'Link distinction in text',
|
|
6
7
|
impact: 'serious',
|
|
7
8
|
tags: ['wcag2a', 'wcag141'],
|
|
8
9
|
help: 'Links inside text must be distinguishable by more than colour',
|
|
@@ -10,6 +10,7 @@ import { parseColor, contrastRatio, composite, effectiveBackground, showRatio }
|
|
|
10
10
|
|
|
11
11
|
export default {
|
|
12
12
|
id: 'non-text-contrast',
|
|
13
|
+
name: 'Control & icon contrast',
|
|
13
14
|
impact: 'serious',
|
|
14
15
|
tags: ['wcag21aa', 'wcag1411'],
|
|
15
16
|
help: 'Form fields and icons need a 3:1 visible boundary',
|
|
@@ -916,6 +916,7 @@ export function createContrastRule({ id, tags, help, helpUrl, thresholds }) {
|
|
|
916
916
|
|
|
917
917
|
export default createContrastRule({
|
|
918
918
|
id: 'color-contrast',
|
|
919
|
+
name: 'Text contrast',
|
|
919
920
|
tags: ['wcag2aa', 'wcag143'],
|
|
920
921
|
help: 'Text must have sufficient contrast against its background',
|
|
921
922
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html',
|
|
@@ -4,6 +4,7 @@ import { createContrastRule } from './1.4.3-color-contrast.js';
|
|
|
4
4
|
|
|
5
5
|
export default createContrastRule({
|
|
6
6
|
id: 'color-contrast-enhanced',
|
|
7
|
+
name: 'Enhanced text contrast',
|
|
7
8
|
tags: ['wcag2aaa', 'wcag146'],
|
|
8
9
|
help: 'Text must have enhanced contrast against its background (AAA)',
|
|
9
10
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced.html',
|
|
@@ -43,6 +43,7 @@ function hasTabbableContent(node) {
|
|
|
43
43
|
|
|
44
44
|
export default {
|
|
45
45
|
id: 'scrollable-region-focusable',
|
|
46
|
+
name: 'Keyboard-reachable scroll areas',
|
|
46
47
|
impact: 'serious',
|
|
47
48
|
tags: ['wcag2a', 'wcag211', 'wcag213'], // 2.1.3 is 2.1.1's AAA twin — same check satisfies both
|
|
48
49
|
help: 'Scrollable regions must be reachable by keyboard',
|
|
@@ -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',
|
|
@@ -107,6 +107,7 @@ function outcome({ missing, ambiguous }, probed, element, accessibleName) {
|
|
|
107
107
|
|
|
108
108
|
export default {
|
|
109
109
|
id: 'aria-valid-refs',
|
|
110
|
+
name: 'ARIA id references',
|
|
110
111
|
// Moderate, not critical: with 4.1.1 Parsing retired in WCAG 2.2 these
|
|
111
112
|
// are 4.1.2 name/state defects, and aria-controls in particular has weak
|
|
112
113
|
// assistive-technology support — real, but rarely blocking.
|
|
@@ -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 };
|