pour-engine 1.45.0 → 1.46.1
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/config/project.config.js +1 -1
- package/engine/README.md +10 -8
- package/engine/index.js +10 -9
- package/engine/lib/contrast.js +78 -3
- package/engine/rules/wcag/1.4.3-color-contrast.js +11 -1
- package/engine/rules/wcag/2.4.7-focus-visible.js +49 -6
- package/engine/wcag3.js +96 -85
- package/package.json +1 -1
package/config/project.config.js
CHANGED
package/engine/README.md
CHANGED
|
@@ -113,18 +113,20 @@ pour engine therefore reports on **every** in-scope criterion, one of two ways:
|
|
|
113
113
|
|
|
114
114
|
### WCAG 3 Working Draft
|
|
115
115
|
|
|
116
|
-
A
|
|
117
|
-
|
|
116
|
+
A check set tag, `wcag3-core` or `wcag3-supplemental`, reports against the
|
|
117
|
+
WCAG 3.0 Working Draft of 10 September 2026 instead (`wcag3.js`). The
|
|
118
118
|
rules are still WCAG 2 rules: each is matched, by reading both documents,
|
|
119
119
|
to the draft requirement its failure also fails, and a rule with no such
|
|
120
120
|
requirement does not run (`UNMAPPED_RULES` gives each reason). Where the
|
|
121
121
|
draft leaves a value to be determined, text contrast among them, the
|
|
122
|
-
rule's WCAG 2 value stands in. `
|
|
123
|
-
|
|
124
|
-
`wcag3
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
rule's WCAG 2 value stands in. `wcag3-core` runs the rules matched to core
|
|
123
|
+
requirements, the ones the draft says must all be met to conform;
|
|
124
|
+
`wcag3-supplemental` adds the rules matched to supplemental requirements.
|
|
125
|
+
`results.standard` is stamped draft and names the set, `manualReview`
|
|
126
|
+
becomes the set's requirements, and each rule result carries `wcag3`.
|
|
127
|
+
The draft's Bronze, Silver and Gold sit above conformance and each needs
|
|
128
|
+
a number of supplemental requirements and assertions it has yet to
|
|
129
|
+
decide, so no check set stands for one. WCAG 2 stays the default.
|
|
128
130
|
|
|
129
131
|
The checklist accounts for the criteria; it does not mean every page state
|
|
130
132
|
or possible failure has been tested. A snapshot audit still needs human
|
package/engine/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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 { ruleMatchesTags,
|
|
7
|
+
import { ruleMatchesTags, checkSetFromTags, manualReviewChecklist, requirementsForRule, standardFor } from './wcag3.js';
|
|
8
8
|
import { isVisible, isRendered, cssPath, htmlSnippet, ownText, collectRoots, resetDOMCaches, releaseDOMCaches } from './lib/dom.js';
|
|
9
9
|
import { accessibleName } from './lib/accessible-name.js';
|
|
10
10
|
import { resetAuditCaches } from './lib/contrast.js';
|
|
@@ -82,10 +82,11 @@ function toResultNode(element, outcome) {
|
|
|
82
82
|
* @param {Document|Element} context - what to scan
|
|
83
83
|
* @param {{ tags?: string[], exclude?: string, signal?: AbortSignal }} options -
|
|
84
84
|
* rule selection by tag (wcag2a…wcag22aa, best-practice), empty selects
|
|
85
|
-
* every rule; a WCAG 3
|
|
86
|
-
* the rules
|
|
87
|
-
* report against the WCAG 3.0 Working Draft:
|
|
88
|
-
* stamped draft, the manual checklist becomes the
|
|
85
|
+
* every rule; a WCAG 3 check set tag (wcag3-core, wcag3-supplemental)
|
|
86
|
+
* selects the rules matched to that set's draft requirements and
|
|
87
|
+
* reframes the report against the WCAG 3.0 Working Draft:
|
|
88
|
+
* results.standard is stamped draft, the manual checklist becomes the
|
|
89
|
+
* set's requirements,
|
|
89
90
|
* and each rule result carries the requirements it maps to (wcag3); exclude is a CSS selector — elements matching it, or inside
|
|
90
91
|
* a match (including across shadow boundaries), are left out of every
|
|
91
92
|
* rule; signal aborts the run between rules (throws AbortError)
|
|
@@ -97,17 +98,17 @@ function toResultNode(element, outcome) {
|
|
|
97
98
|
*/
|
|
98
99
|
export async function run(context = document, options = {}, onProgress) {
|
|
99
100
|
const auditStarted = performance.now();
|
|
100
|
-
const
|
|
101
|
+
const checkSet = checkSetFromTags(options.tags);
|
|
101
102
|
const results = {
|
|
102
103
|
testEngine: { name, version },
|
|
103
104
|
timestamp: new Date().toISOString(),
|
|
104
105
|
url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
|
|
105
|
-
standard:
|
|
106
|
+
standard: checkSet ? standardFor(checkSet) : null,
|
|
106
107
|
violations: [],
|
|
107
108
|
passes: [],
|
|
108
109
|
incomplete: [],
|
|
109
110
|
inapplicable: [],
|
|
110
|
-
manualReview:
|
|
111
|
+
manualReview: checkSet ? manualReviewChecklist(checkSet) : manualReviewCriteria(options.tags),
|
|
111
112
|
ruleTimings: [],
|
|
112
113
|
};
|
|
113
114
|
|
|
@@ -254,7 +255,7 @@ export async function run(context = document, options = {}, onProgress) {
|
|
|
254
255
|
};
|
|
255
256
|
// In a WCAG 3 audit, the draft requirements this rule speaks to (empty
|
|
256
257
|
// for a best-practice rule, which maps to none).
|
|
257
|
-
if (
|
|
258
|
+
if (checkSet) ruleResult.wcag3 = requirementsForRule(rule.id);
|
|
258
259
|
if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
|
|
259
260
|
if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
|
|
260
261
|
if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
|
package/engine/lib/contrast.js
CHANGED
|
@@ -145,6 +145,7 @@ let uncoveredEffectCache = new WeakMap();
|
|
|
145
145
|
let blendBackdropCache = new WeakMap();
|
|
146
146
|
let labelReferrersCache = new WeakMap();
|
|
147
147
|
let stackingContextCache = new WeakMap();
|
|
148
|
+
let backgroundRulesCache = new WeakMap();
|
|
148
149
|
const HAS_IMAGE = Symbol('background-image in chain');
|
|
149
150
|
|
|
150
151
|
export function resetAuditCaches() {
|
|
@@ -161,6 +162,7 @@ export function resetAuditCaches() {
|
|
|
161
162
|
blendBackdropCache = new WeakMap();
|
|
162
163
|
labelReferrersCache = new WeakMap();
|
|
163
164
|
stackingContextCache = new WeakMap();
|
|
165
|
+
backgroundRulesCache = new WeakMap();
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/**
|
|
@@ -981,6 +983,14 @@ export function filmedContrastBounds(foreground, background, film) {
|
|
|
981
983
|
};
|
|
982
984
|
}
|
|
983
985
|
|
|
986
|
+
/** A layer's own opacity. 0 is a real value, not a missing one: consent
|
|
987
|
+
* dimmers commonly fade to opacity 0 and stay in the DOM once dismissed
|
|
988
|
+
* (CSS Color 4, the opacity property: 0 is fully transparent). */
|
|
989
|
+
const opacityOf = (style) => {
|
|
990
|
+
const opacity = parseFloat(style.opacity);
|
|
991
|
+
return Number.isNaN(opacity) ? 1 : opacity;
|
|
992
|
+
};
|
|
993
|
+
|
|
984
994
|
/** A layer qualifies as a scrim over `element` when it is translucent paint
|
|
985
995
|
* (or a backdrop-filter) covering most of the viewport and the whole
|
|
986
996
|
* element — the shape of a modal/loading veil, not a badge or header.
|
|
@@ -994,7 +1004,7 @@ function scrimIn(layersAbove, element, win) {
|
|
|
994
1004
|
if (layer.contains(element) || element.contains(layer)) continue;
|
|
995
1005
|
const style = getComputedStyle(layer);
|
|
996
1006
|
const color = parseColor(style.backgroundColor);
|
|
997
|
-
const alpha = (color ? color.a : 0) * (
|
|
1007
|
+
const alpha = (color ? color.a : 0) * opacityOf(style);
|
|
998
1008
|
const hasBackdropFilter = style.backdropFilter && style.backdropFilter !== 'none';
|
|
999
1009
|
// Fully opaque covers simply hide the text (nothing to judge through);
|
|
1000
1010
|
// near-invisible tints don't meaningfully change presented contrast.
|
|
@@ -1027,7 +1037,7 @@ export function scrimPaint(layers) {
|
|
|
1027
1037
|
if (style.backgroundImage !== 'none') return null;
|
|
1028
1038
|
const color = parseColor(style.backgroundColor);
|
|
1029
1039
|
if (!color) return null;
|
|
1030
|
-
const alpha = color.a * (
|
|
1040
|
+
const alpha = color.a * opacityOf(style);
|
|
1031
1041
|
if (alpha <= 0) continue;
|
|
1032
1042
|
paints.push({ ...color, a: alpha });
|
|
1033
1043
|
}
|
|
@@ -1179,7 +1189,7 @@ export function opaquePanelRects(doc) {
|
|
|
1179
1189
|
// hazard that scrolling cannot escape.
|
|
1180
1190
|
if (!panelRectsCache.veil && (style.position === 'fixed' || style.position === 'sticky')
|
|
1181
1191
|
&& style.visibility !== 'hidden') {
|
|
1182
|
-
const alpha = (color ? color.a : 0) * (
|
|
1192
|
+
const alpha = (color ? color.a : 0) * opacityOf(style);
|
|
1183
1193
|
const hasBackdropFilter = style.backdropFilter && style.backdropFilter !== 'none';
|
|
1184
1194
|
const coversViewport = rect.width >= 0.9 * win.innerWidth && rect.height >= 0.9 * win.innerHeight;
|
|
1185
1195
|
if (coversViewport && (hasBackdropFilter || (alpha >= 0.15 && alpha < 1))) {
|
|
@@ -1988,6 +1998,71 @@ function rootHasFirstLineRules(root) {
|
|
|
1988
1998
|
return has;
|
|
1989
1999
|
}
|
|
1990
2000
|
|
|
2001
|
+
/**
|
|
2002
|
+
* The stylesheet rules behind lazy-loaded backgrounds, per root per audit:
|
|
2003
|
+
* `gates` hold background images back until a loader marks the element
|
|
2004
|
+
* loaded (`background-image: none` on a selector that excludes a lazy-load
|
|
2005
|
+
* class, as in `.section:not(.lazyloaded) { background-image: none }`), and
|
|
2006
|
+
* `images` give an element a background image of its own. Rules under a
|
|
2007
|
+
* media or support condition count only while it holds; an unreadable
|
|
2008
|
+
* cross-origin sheet contributes nothing.
|
|
2009
|
+
*/
|
|
2010
|
+
const LAZY_EXCLUSION = /:not\(\s*\.[\w-]*lazy[\w-]*\s*\)/i;
|
|
2011
|
+
const PAINTS_IMAGE = /url\(|gradient\(|image-set\(|var\(/i;
|
|
2012
|
+
function backgroundRules(root) {
|
|
2013
|
+
let found = backgroundRulesCache.get(root);
|
|
2014
|
+
if (found) return found;
|
|
2015
|
+
found = { gates: [], images: [] };
|
|
2016
|
+
const win = (root.ownerDocument ?? root).defaultView;
|
|
2017
|
+
const scan = (rules) => {
|
|
2018
|
+
for (const rule of rules) {
|
|
2019
|
+
if (rule.styleSheet) {
|
|
2020
|
+
if (rule.media?.mediaText && !win.matchMedia(rule.media.mediaText).matches) continue;
|
|
2021
|
+
try { scan(rule.styleSheet.cssRules); } catch { /* unreadable import */ }
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
if (rule.media) {
|
|
2025
|
+
if (!win.matchMedia(rule.media.mediaText).matches) continue;
|
|
2026
|
+
} else if (win.CSSSupportsRule && rule instanceof win.CSSSupportsRule) {
|
|
2027
|
+
if (!win.CSS.supports(rule.conditionText)) continue;
|
|
2028
|
+
}
|
|
2029
|
+
if (rule.selectorText && rule.style) {
|
|
2030
|
+
const image = rule.style.getPropertyValue('background-image').trim();
|
|
2031
|
+
if (image === 'none' && LAZY_EXCLUSION.test(rule.selectorText)) found.gates.push(rule.selectorText);
|
|
2032
|
+
else if (PAINTS_IMAGE.test(image || rule.style.getPropertyValue('background'))) found.images.push(rule.selectorText);
|
|
2033
|
+
}
|
|
2034
|
+
if (rule.cssRules) scan(rule.cssRules);
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
for (const sheet of [...(root.styleSheets ?? []), ...(root.adoptedStyleSheets ?? [])]) {
|
|
2038
|
+
try { scan(sheet.cssRules); } catch { /* cross-origin */ }
|
|
2039
|
+
}
|
|
2040
|
+
backgroundRulesCache.set(root, found);
|
|
2041
|
+
return found;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const matchesAny = (node, selectors) => selectors.some((selector) => {
|
|
2045
|
+
try { return node.matches(selector); } catch { return false; }
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* Is the background behind this text still to be lazy-loaded? True when an
|
|
2050
|
+
* element on its chain is given a background image by the page's styles, a
|
|
2051
|
+
* lazy-load gate matches it, and it computes no background image now. Page
|
|
2052
|
+
* builders defer section backgrounds this way until the section nears the
|
|
2053
|
+
* viewport, so text the visitor has not scrolled to yet is measured against
|
|
2054
|
+
* a placeholder they never see: the loader restores the image first.
|
|
2055
|
+
*/
|
|
2056
|
+
export function lazyWithheldBackground(element) {
|
|
2057
|
+
for (let node = element; node?.nodeType === 1; node = flatParentOf(node)) {
|
|
2058
|
+
const { gates, images } = backgroundRules(node.getRootNode());
|
|
2059
|
+
if (!gates.length || !matchesAny(node, gates)) continue;
|
|
2060
|
+
if (getComputedStyle(node).backgroundImage !== 'none') continue;
|
|
2061
|
+
if (PAINTS_IMAGE.test(node.style?.backgroundImage ?? '') || matchesAny(node, images)) return true;
|
|
2062
|
+
}
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1991
2066
|
/**
|
|
1992
2067
|
* Colours that repaint part of this element's own text through ::first-line
|
|
1993
2068
|
* or ::first-letter, when they differ from the colour the element itself
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
opacityAnimating, restingOpacity, mediaRects, inZeroClipSubtree,
|
|
7
7
|
paintedBackdrop, opaquePanelRects, viewportVeil, textShadowHalo, textShadowNegligible,
|
|
8
8
|
pseudoBackdropForText, filmedContrastBounds, backgroundColorSource, scrimPaint, applyOverlays,
|
|
9
|
-
showRatio, asRgb, splitBackgroundLayers, backgroundLayerUrl, sampleGridFor, opacityGroupPaint, pseudoTextColors, BOLD_WEIGHT, hasPaintEffects, isolatedBlendBackdrop, labelReferrers, beneathOpaqueAncestor } from '../../lib/contrast.js';
|
|
9
|
+
showRatio, asRgb, splitBackgroundLayers, backgroundLayerUrl, sampleGridFor, opacityGroupPaint, pseudoTextColors, BOLD_WEIGHT, hasPaintEffects, isolatedBlendBackdrop, labelReferrers, beneathOpaqueAncestor, lazyWithheldBackground } from '../../lib/contrast.js';
|
|
10
10
|
|
|
11
11
|
/** The first url() among a background-image list's layers, or null. */
|
|
12
12
|
const firstLayerUrl = (css) => splitBackgroundLayers(css ?? '').map(backgroundLayerUrl).find(Boolean) ?? null;
|
|
@@ -1508,6 +1508,16 @@ export function createContrastRule({ id, name, tags, help, helpUrl, thresholds }
|
|
|
1508
1508
|
rule.evaluate = async (element, helpers) => {
|
|
1509
1509
|
const verdict = await judge(element, helpers);
|
|
1510
1510
|
if (verdict.status !== 'fail') return verdict;
|
|
1511
|
+
// The background this was measured against is a placeholder: the page
|
|
1512
|
+
// holds its background image back until a lazy loader marks it loaded,
|
|
1513
|
+
// which happens before the visitor scrolls it into view. A failure has
|
|
1514
|
+
// to be shown against the paint the visitor sees.
|
|
1515
|
+
if (lazyWithheldBackground(element)) {
|
|
1516
|
+
return {
|
|
1517
|
+
status: 'incomplete',
|
|
1518
|
+
message: 'This text sits in a section whose background image is lazy-loaded and has not loaded yet, so it was measured against a background visitors never see. Scroll the page to this text and run the audit again, or check it by eye.',
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1511
1521
|
// A miss on a glyph that is not words: see glyphNotWords. Every fail
|
|
1512
1522
|
// path passes through here, so this is said once. A glyph still owes
|
|
1513
1523
|
// 3:1 under 1.4.11 when it is needed to identify a control, so one that
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// WCAG SC 2.4.7 Focus Visible (Level AA)
|
|
2
|
-
// A stylesheet that strips focus outlines without
|
|
3
|
-
// indicator is the classic 2.4.7 failure (F78).
|
|
4
|
-
// unreadable and skipped; a replacement indicator can
|
|
5
|
-
// rule entirely — so this flags for review, it never
|
|
2
|
+
// A stylesheet or style attribute that strips focus outlines without
|
|
3
|
+
// providing a replacement indicator is the classic 2.4.7 failure (F78).
|
|
4
|
+
// Cross-origin sheets are unreadable and skipped; a replacement indicator can
|
|
5
|
+
// live in a different rule entirely — so this flags for review, it never
|
|
6
|
+
// asserts a fail.
|
|
7
|
+
import { isRendered } from '../../lib/dom.js';
|
|
8
|
+
|
|
6
9
|
// The focus pseudo-classes themselves, and nothing else: :focus-within is a
|
|
7
10
|
// PARENT's state and stripping ":focus" out of it would leave "-within".
|
|
8
11
|
const FOCUS_TOKEN = /:focus(?:-visible)?(?![\w-])/g;
|
|
@@ -33,7 +36,8 @@ function focusSuppressors(doc) {
|
|
|
33
36
|
// human question.
|
|
34
37
|
let sheet = null;
|
|
35
38
|
const inspect = (rule) => {
|
|
36
|
-
if (!rule.selectorText
|
|
39
|
+
if (!rule.selectorText) return;
|
|
40
|
+
if (!/:focus/.test(rule.selectorText)) { inspectResting(rule); return; }
|
|
37
41
|
// `:focus:not(:focus-visible) { outline: none }` is the PUBLISHED pattern
|
|
38
42
|
// for showing the ring to keyboard users only: it strips the indicator
|
|
39
43
|
// precisely in the state where the browser has decided not to show focus,
|
|
@@ -65,7 +69,38 @@ function focusSuppressors(doc) {
|
|
|
65
69
|
provider.order > suspect.order && covered(subject, provider)));
|
|
66
70
|
if (!restored) suspects.push(suspect.selector);
|
|
67
71
|
}
|
|
72
|
+
allProviders.push(...sheet.providers);
|
|
73
|
+
};
|
|
74
|
+
// A rule with no focus state in it removes the indicator too: author
|
|
75
|
+
// styles outrank the browser's own :focus-visible ring whatever their
|
|
76
|
+
// specificity, so `* { outline: none }` or `a, button { outline: 0 }`
|
|
77
|
+
// leaves a focused link with no ring (measured in Chromium 2026-09-24).
|
|
78
|
+
// Such a rule matters only where it reaches something Tab lands on, and a
|
|
79
|
+
// box-shadow or border in the same rule is not a replacement: it is there
|
|
80
|
+
// at rest, so it marks nothing when focus arrives.
|
|
81
|
+
const resting = [];
|
|
82
|
+
const allProviders = [];
|
|
83
|
+
const tabbable = (el) => !el.disabled && (el.tabIndex >= 0 || el.isContentEditable) && isRendered(el);
|
|
84
|
+
const reachesTabbable = (part) => {
|
|
85
|
+
if (part.includes('::')) return false; // a pseudo-element's outline is not the control's ring
|
|
86
|
+
try { return [...doc.querySelectorAll(part)].some(tabbable); } catch { return false; }
|
|
87
|
+
};
|
|
88
|
+
const inspectResting = (rule) => {
|
|
89
|
+
const style = rule.style;
|
|
90
|
+
if (!style || !rule.selectorText) return;
|
|
91
|
+
const styles = [style, ...[...(rule.cssRules ?? [])]
|
|
92
|
+
.filter((child) => child.style && !child.selectorText).map((child) => child.style)];
|
|
93
|
+
if (!styles.map(outlineOf).some(removesOutline)) return;
|
|
94
|
+
const parts = subjects(rule.selectorText).filter(reachesTabbable);
|
|
95
|
+
if (parts.length) resting.push({ selector: rule.selectorText, subjects: parts });
|
|
68
96
|
};
|
|
97
|
+
// A focus rule outranks a resting one only when its specificity is
|
|
98
|
+
// higher, which order in the sheet cannot change. `a:focus-visible` always
|
|
99
|
+
// beats `a`; a bare `:focus-visible` (0,1,0) beats only selectors made of
|
|
100
|
+
// type names and `*`, and ties or loses to `.btn` or `.nav .btn`.
|
|
101
|
+
const typeOnly = (subject) => !/[#.[:]/.test(subject);
|
|
102
|
+
const outranked = (subject) => allProviders.some((provider) => provider.subjects.some((p) =>
|
|
103
|
+
p === subject || ((p === '' || p === '*') && typeOnly(subject))));
|
|
69
104
|
// Inspect each rule, THEN descend into any children. Testing `rule.cssRules`
|
|
70
105
|
// first and skipping used to walk straight past every plain style rule:
|
|
71
106
|
// since CSS Nesting shipped, a CSSStyleRule carries its own cssRules list,
|
|
@@ -84,6 +119,14 @@ function focusSuppressors(doc) {
|
|
|
84
119
|
try { scan(styleSheet.cssRules); } catch { /* cross-origin sheet */ }
|
|
85
120
|
settle();
|
|
86
121
|
}
|
|
122
|
+
for (const suspect of resting) {
|
|
123
|
+
if (!suspect.subjects.every(outranked)) suspects.push(suspect.selector);
|
|
124
|
+
}
|
|
125
|
+
// A style attribute outranks every selector, so no focus rule without
|
|
126
|
+
// !important gives the ring back to `<a style="outline: none">`.
|
|
127
|
+
for (const el of doc.querySelectorAll('[style]')) {
|
|
128
|
+
if (removesOutline(outlineOf(el.style)) && tabbable(el)) suspects.push(`the style attribute of <${el.localName}>`);
|
|
129
|
+
}
|
|
87
130
|
return suspects;
|
|
88
131
|
}
|
|
89
132
|
|
|
@@ -102,7 +145,7 @@ export default {
|
|
|
102
145
|
const shown = suspects.slice(0, 3).join(', ');
|
|
103
146
|
return {
|
|
104
147
|
status: 'incomplete',
|
|
105
|
-
message: `${suspects.length} CSS rule(s) remove the focus outline
|
|
148
|
+
message: `${suspects.length} CSS rule(s) or style attribute(s) remove the focus outline with no replacement found (e.g. ${shown}). If nothing else draws a visible indicator, keyboard users lose their place. Tab through the page to check.`,
|
|
106
149
|
};
|
|
107
150
|
},
|
|
108
151
|
};
|
package/engine/wcag3.js
CHANGED
|
@@ -7,21 +7,18 @@
|
|
|
7
7
|
// surface that shows it must say so. WCAG 2 stays the default everywhere.
|
|
8
8
|
//
|
|
9
9
|
// The draft replaces success criteria and A/AA/AAA with three kinds of
|
|
10
|
-
// provision under each guideline: core requirements
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// The draft tags each assertion Content or Organization in principle but
|
|
23
|
-
// not yet one by one; an assertion about a style guide, policy or process
|
|
24
|
-
// the organization keeps is read as Organization, any other as Content.
|
|
10
|
+
// provision under each guideline: core requirements, supplemental
|
|
11
|
+
// requirements and assertions (documented statements about how content is
|
|
12
|
+
// made or how an organization works, which no tool can check). To conform
|
|
13
|
+
// (3.2), every core requirement must be met; supplemental requirements and
|
|
14
|
+
// assertions are not needed to conform. The draft's Reporting section (4,
|
|
15
|
+
// Exploratory) puts Bronze, Silver and Gold above conformance, each taking
|
|
16
|
+
// a number of supplemental requirements and assertions still to be
|
|
17
|
+
// decided, so none of them is a set of checks a tool can run. pour offers
|
|
18
|
+
// two check sets instead, and no tier:
|
|
19
|
+
// core: the rules matched to core requirements
|
|
20
|
+
// supplemental: those rules plus the ones matched to supplemental
|
|
21
|
+
// requirements
|
|
25
22
|
//
|
|
26
23
|
// No rule here was written for WCAG 3. The rules test WCAG 2 criteria;
|
|
27
24
|
// RULE_REQUIREMENTS names, rule by rule, the draft requirement that a
|
|
@@ -41,28 +38,32 @@ export const draftInfo = {
|
|
|
41
38
|
note: 'An unfinished standard. Findings come from WCAG 2 rules matched to draft requirements, with WCAG 2 values where the draft has none yet. Not for conformance claims.',
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
export const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
// The check sets, cumulative, and the provision types each takes in.
|
|
42
|
+
export const CHECK_SETS = ['core', 'supplemental'];
|
|
43
|
+
const SET_TYPES = {
|
|
44
|
+
core: new Set(['core']),
|
|
45
|
+
supplemental: new Set(['core', 'supplemental']),
|
|
46
|
+
};
|
|
47
|
+
const SET_SCOPES = {
|
|
48
|
+
core: 'core requirements',
|
|
49
|
+
supplemental: 'core and supplemental requirements',
|
|
52
50
|
};
|
|
51
|
+
// Names the first WCAG 3 draft mode gave these sets, read the way they ran
|
|
52
|
+
// then, so a saved setting, a script or a tag list that still uses one
|
|
53
|
+
// selects the same rules. They are never shown.
|
|
54
|
+
const RENAMED_SETS = { bronze: 'core', silver: 'supplemental', gold: 'supplemental' };
|
|
53
55
|
|
|
54
56
|
const TYPE_LABELS = {
|
|
55
57
|
core: 'Core',
|
|
56
58
|
supplemental: 'Supplemental',
|
|
57
|
-
|
|
58
|
-
organization: 'Assertion',
|
|
59
|
+
assertion: 'Assertion',
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
// Every requirement and assertion of the draft, in document order, under
|
|
62
63
|
// its guideline. The one recommended practice (2.7.2.7) is left out: the
|
|
63
|
-
// draft says recommended practices are not needed to conform
|
|
64
|
-
//
|
|
65
|
-
//
|
|
64
|
+
// draft says recommended practices are not needed to conform. Guidelines
|
|
65
|
+
// with nothing past Exploratory (2.1.2 Figure captions, 2.12.2 Control
|
|
66
|
+
// text) have no provisions to list.
|
|
66
67
|
const RAW = [
|
|
67
68
|
g('2.1.1', 'Image alternatives'),
|
|
68
69
|
p('2.1.1.1', 'Images detectable', 'core', 'x2-1-1-1-images-detectable'),
|
|
@@ -81,9 +82,9 @@ const RAW = [
|
|
|
81
82
|
p('2.1.4.7', 'Speaker language identified in transcripts', 'supplemental', 'x2-1-4-7-speaker-language-identified-in-transcripts'),
|
|
82
83
|
p('2.1.4.8', 'Sounds identified in transcripts', 'core', 'x2-1-4-8-sounds-identified-in-transcripts'),
|
|
83
84
|
p('2.1.4.9', 'Visual information identified in transcripts', 'core', 'x2-1-4-9-visual-information-identified-in-transcripts'),
|
|
84
|
-
p('2.1.4.10', 'Transcripts style guide', '
|
|
85
|
-
p('2.1.4.11', 'Transcripts usability testing', '
|
|
86
|
-
p('2.1.4.12', 'Transcripts reviewed by content authors', '
|
|
85
|
+
p('2.1.4.10', 'Transcripts style guide', 'assertion', 'x2-1-4-10-transcripts-style-guide'),
|
|
86
|
+
p('2.1.4.11', 'Transcripts usability testing', 'assertion', 'x2-1-4-11-transcripts-usability-testing'),
|
|
87
|
+
p('2.1.4.12', 'Transcripts reviewed by content authors', 'assertion', 'x2-1-4-12-transcripts-reviewed-by-content-authors'),
|
|
87
88
|
g('2.1.5', 'Captions'),
|
|
88
89
|
p('2.1.5.1', 'Captions adjustable', 'supplemental', 'x2-1-5-1-captions-adjustable'),
|
|
89
90
|
p('2.1.5.2', 'Captions available (prerecorded)', 'core', 'x2-1-5-2-captions-available-prerecorded'),
|
|
@@ -98,9 +99,9 @@ const RAW = [
|
|
|
98
99
|
p('2.1.5.11', 'Speakers identified in captions', 'supplemental', 'x2-1-5-11-speakers-identified-in-captions'),
|
|
99
100
|
p('2.1.5.12', 'Speaker language identified in captions', 'supplemental', 'x2-1-5-12-speaker-language-identified-in-captions'),
|
|
100
101
|
p('2.1.5.13', 'Sounds identified in captions', 'core', 'x2-1-5-13-sounds-identified-in-captions'),
|
|
101
|
-
p('2.1.5.14', 'Captions style guide', '
|
|
102
|
-
p('2.1.5.15', 'Captions usability testing', '
|
|
103
|
-
p('2.1.5.16', 'Captions reviewed by content authors', '
|
|
102
|
+
p('2.1.5.14', 'Captions style guide', 'assertion', 'x2-1-5-14-captions-style-guide'),
|
|
103
|
+
p('2.1.5.15', 'Captions usability testing', 'assertion', 'x2-1-5-15-captions-usability-testing'),
|
|
104
|
+
p('2.1.5.16', 'Captions reviewed by content authors', 'assertion', 'x2-1-5-16-captions-reviewed-by-content-authors'),
|
|
104
105
|
g('2.1.6', 'Audio descriptions'),
|
|
105
106
|
p('2.1.6.1', 'Audio descriptions available (prerecorded)', 'core', 'x2-1-6-1-audio-descriptions-available-prerecorded'),
|
|
106
107
|
p('2.1.6.2', 'Audio descriptions equivalent (prerecorded)', 'core', 'x2-1-6-2-audio-descriptions-equivalent-prerecorded'),
|
|
@@ -114,13 +115,13 @@ const RAW = [
|
|
|
114
115
|
p('2.1.6.10', 'Speaker language identified in audio descriptions', 'supplemental', 'x2-1-6-10-speaker-language-identified-in-audio-descriptions'),
|
|
115
116
|
p('2.1.6.11', 'Sounds identified in audio descriptions', 'core', 'x2-1-6-11-sounds-identified-in-audio-descriptions'),
|
|
116
117
|
p('2.1.6.12', 'Visual information identified in audio descriptions', 'core', 'x2-1-6-12-visual-information-identified-in-audio-descriptions'),
|
|
117
|
-
p('2.1.6.13', 'Audio descriptions style guide', '
|
|
118
|
-
p('2.1.6.14', 'Audio descriptions usability testing', '
|
|
119
|
-
p('2.1.6.15', 'Audio descriptions reviewed by content authors', '
|
|
118
|
+
p('2.1.6.13', 'Audio descriptions style guide', 'assertion', 'x2-1-6-13-audio-descriptions-style-guide'),
|
|
119
|
+
p('2.1.6.14', 'Audio descriptions usability testing', 'assertion', 'x2-1-6-14-audio-descriptions-usability-testing'),
|
|
120
|
+
p('2.1.6.15', 'Audio descriptions reviewed by content authors', 'assertion', 'x2-1-6-15-audio-descriptions-reviewed-by-content-authors'),
|
|
120
121
|
g('2.1.7', 'Sign language'),
|
|
121
122
|
p('2.1.7.1', 'Sign language available (prerecorded)', 'supplemental', 'x2-1-7-1-sign-language-available-prerecorded'),
|
|
122
123
|
p('2.1.7.2', 'Sign language controllable', 'supplemental', 'x2-1-7-2-sign-language-controllable'),
|
|
123
|
-
p('2.1.7.3', 'Sign language policy (live)', '
|
|
124
|
+
p('2.1.7.3', 'Sign language policy (live)', 'assertion', 'x2-1-7-3-sign-language-policy-live'),
|
|
124
125
|
g('2.1.8', 'Single sense'),
|
|
125
126
|
p('2.1.8.1', 'Hue not relied on', 'core', 'x2-1-8-1-hue-not-relied-on'),
|
|
126
127
|
p('2.1.8.2', 'Graphical object contrast sufficient', 'core', 'x2-1-8-2-graphical-object-contrast-sufficient'),
|
|
@@ -128,8 +129,8 @@ const RAW = [
|
|
|
128
129
|
p('2.1.8.4', 'Sound not relied on', 'core', 'x2-1-8-4-sound-not-relied-on'),
|
|
129
130
|
p('2.1.8.5', 'Spatial audio not relied on', 'core', 'x2-1-8-5-spatial-audio-not-relied-on'),
|
|
130
131
|
g('2.1.9', 'Accessible media player'),
|
|
131
|
-
p('2.1.9.1', 'Accessible video player selected', '
|
|
132
|
-
p('2.1.9.2', 'Accessible audio player selected', '
|
|
132
|
+
p('2.1.9.1', 'Accessible video player selected', 'assertion', 'x2-1-9-1-accessible-video-player-selected'),
|
|
133
|
+
p('2.1.9.2', 'Accessible audio player selected', 'assertion', 'x2-1-9-2-accessible-audio-player-selected'),
|
|
133
134
|
g('2.2.1', 'Text appearance'),
|
|
134
135
|
p('2.2.1.1', 'Blocks of text readable (minimum)', 'core', 'x2-2-1-1-blocks-of-text-readable-minimum'),
|
|
135
136
|
p('2.2.1.2', 'Text style readable (minimum)', 'core', 'x2-2-1-2-text-style-readable-minimum'),
|
|
@@ -154,13 +155,13 @@ const RAW = [
|
|
|
154
155
|
p('2.2.3.5', 'Diacritics available', 'core', 'x2-2-3-5-diacritics-available'),
|
|
155
156
|
p('2.2.3.6', 'No nested clauses', 'supplemental', 'x2-2-3-6-no-nested-clauses'),
|
|
156
157
|
p('2.2.3.7', 'No unnecessary words', 'supplemental', 'x2-2-3-7-no-unnecessary-words'),
|
|
157
|
-
p('2.2.3.8', 'Clear language review', '
|
|
158
|
-
p('2.2.3.9', 'Visual aids review', '
|
|
158
|
+
p('2.2.3.8', 'Clear language review', 'assertion', 'x2-2-3-8-clear-language-review'),
|
|
159
|
+
p('2.2.3.9', 'Visual aids review', 'assertion', 'x2-2-3-9-visual-aids-review'),
|
|
159
160
|
g('2.3.1', 'Keyboard focus appearance'),
|
|
160
161
|
p('2.3.1.1', 'Default focus indicator used', 'supplemental', 'x2-3-1-1-default-focus-indicator-used'),
|
|
161
162
|
p('2.3.1.2', 'Focus indicator contrast sufficient', 'core', 'x2-3-1-2-focus-indicator-contrast-sufficient'),
|
|
162
163
|
p('2.3.1.3', 'Focus indicator size sufficient', 'supplemental', 'x2-3-1-3-focus-indicator-size-sufficient'),
|
|
163
|
-
p('2.3.1.4', 'Focus indicator style guide', '
|
|
164
|
+
p('2.3.1.4', 'Focus indicator style guide', 'assertion', 'x2-3-1-4-focus-indicator-style-guide'),
|
|
164
165
|
g('2.3.2', 'Pointer focus appearance'),
|
|
165
166
|
p('2.3.2.1', 'Pointer activation indicated (minimum)', 'core', 'x2-3-2-1-pointer-activation-indicated-minimum'),
|
|
166
167
|
p('2.3.2.2', 'Pointer activation indicated (enhanced)', 'supplemental', 'x2-3-2-2-pointer-activation-indicated-enhanced'),
|
|
@@ -174,9 +175,9 @@ const RAW = [
|
|
|
174
175
|
p('2.3.3.2', 'Focus retained', 'supplemental', 'x2-3-3-2-focus-retained'),
|
|
175
176
|
p('2.3.3.3', 'Focus order meaningful', 'core', 'x2-3-3-3-focus-order-meaningful'),
|
|
176
177
|
g('2.3.4', 'Expected behavior'),
|
|
177
|
-
p('2.3.4.1', 'Consistent interactions', '
|
|
178
|
+
p('2.3.4.1', 'Consistent interactions', 'assertion', 'x2-3-4-1-consistent-interactions'),
|
|
178
179
|
p('2.3.4.2', 'Consistent control location', 'supplemental', 'x2-3-4-2-consistent-control-location'),
|
|
179
|
-
p('2.3.4.3', 'Conventional pattern used', '
|
|
180
|
+
p('2.3.4.3', 'Conventional pattern used', 'assertion', 'x2-3-4-3-conventional-pattern-used'),
|
|
180
181
|
g('2.3.5', 'Control information'),
|
|
181
182
|
p('2.3.5.1', 'Interactive element contrast sufficient', 'core', 'x2-3-5-1-interactive-element-contrast-sufficient'),
|
|
182
183
|
p('2.3.5.2', 'Interactive element names available', 'core', 'x2-3-5-2-interactive-element-names-available'),
|
|
@@ -197,7 +198,7 @@ const RAW = [
|
|
|
197
198
|
g('2.4.2', 'Physical or cognitive effort when using keyboard'),
|
|
198
199
|
p('2.4.2.1', 'Navigation keys described', 'supplemental', 'x2-4-2-1-navigation-keys-described'),
|
|
199
200
|
p('2.4.2.2', 'No repetitive adjacent interactive elements', 'supplemental', 'x2-4-2-2-no-repetitive-adjacent-interactive-elements'),
|
|
200
|
-
p('2.4.2.3', 'Keyboard effort comparable', '
|
|
201
|
+
p('2.4.2.3', 'Keyboard effort comparable', 'assertion', 'x2-4-2-3-keyboard-effort-comparable'),
|
|
201
202
|
g('2.4.3', 'Pointer input'),
|
|
202
203
|
p('2.4.3.1', 'Pointer activation controllable', 'core', 'x2-4-3-1-pointer-activation-controllable'),
|
|
203
204
|
p('2.4.3.2', 'Simple pointer input available', 'core', 'x2-4-3-2-simple-pointer-input-available'),
|
|
@@ -207,7 +208,7 @@ const RAW = [
|
|
|
207
208
|
g('2.4.4', 'Speech and voice input'),
|
|
208
209
|
p('2.4.4.1', 'Speech not relied on', 'core', 'x2-4-4-1-speech-not-relied-on'),
|
|
209
210
|
p('2.4.4.2', 'Real-time text available', 'core', 'x2-4-4-2-real-time-text-available'),
|
|
210
|
-
p('2.4.4.3', 'Generated speech testing', '
|
|
211
|
+
p('2.4.4.3', 'Generated speech testing', 'assertion', 'x2-4-4-3-generated-speech-testing'),
|
|
211
212
|
g('2.4.5', 'Input operation'),
|
|
212
213
|
p('2.4.5.1', 'Hover or focus content dismissible', 'core', 'x2-4-5-1-hover-or-focus-content-dismissible'),
|
|
213
214
|
p('2.4.5.2', 'Hover content persistent', 'core', 'x2-4-5-2-hover-content-persistent'),
|
|
@@ -231,7 +232,7 @@ const RAW = [
|
|
|
231
232
|
p('2.5.2.1', 'Errors preventable', 'core', 'x2-5-2-1-errors-preventable'),
|
|
232
233
|
p('2.5.2.2', 'Submission status notified', 'supplemental', 'x2-5-2-2-submission-status-notified'),
|
|
233
234
|
p('2.5.2.3', 'Data entry validated', 'supplemental', 'x2-5-2-3-data-entry-validated'),
|
|
234
|
-
p('2.5.2.4', 'Error prevention review', '
|
|
235
|
+
p('2.5.2.4', 'Error prevention review', 'assertion', 'x2-5-2-4-error-prevention-review'),
|
|
235
236
|
g('2.6.1', 'Avoid physical harm'),
|
|
236
237
|
p('2.6.1.1', 'No flashing over threshold', 'core', 'x2-6-1-1-no-flashing-over-threshold'),
|
|
237
238
|
p('2.6.1.2', 'No flashing over threshold (no exceptions)', 'supplemental', 'x2-6-1-2-no-flashing-over-threshold-no-exceptions'),
|
|
@@ -240,12 +241,12 @@ const RAW = [
|
|
|
240
241
|
p('2.6.1.5', 'Trigger warning available', 'core', 'x2-6-1-5-trigger-warning-available'),
|
|
241
242
|
p('2.6.1.6', 'Haptic stimulation adjustable', 'core', 'x2-6-1-6-haptic-stimulation-adjustable'),
|
|
242
243
|
p('2.6.1.7', 'Audio shifting adjustable', 'core', 'x2-6-1-7-audio-shifting-adjustable'),
|
|
243
|
-
p('2.6.1.8', 'Safe content review', '
|
|
244
|
+
p('2.6.1.8', 'Safe content review', 'assertion', 'x2-6-1-8-safe-content-review'),
|
|
244
245
|
g('2.7.1', 'Recognizable layouts'),
|
|
245
|
-
p('2.7.1.1', 'Conventional layout review', '
|
|
246
|
+
p('2.7.1.1', 'Conventional layout review', 'assertion', 'x2-7-1-1-conventional-layout-review'),
|
|
246
247
|
g('2.7.2', 'User orientation'),
|
|
247
248
|
p('2.7.2.1', 'Page/view title available', 'core', 'x2-7-2-1-page-view-title-available'),
|
|
248
|
-
p('2.7.2.2', 'Location within product review', '
|
|
249
|
+
p('2.7.2.2', 'Location within product review', 'assertion', 'x2-7-2-2-location-within-product-review'),
|
|
249
250
|
p('2.7.2.3', 'All steps listed', 'core', 'x2-7-2-3-all-steps-listed'),
|
|
250
251
|
p('2.7.2.4', 'Current step indicated', 'core', 'x2-7-2-4-current-step-indicated'),
|
|
251
252
|
p('2.7.2.5', 'Page/view change notified', 'core', 'x2-7-2-5-page-view-change-notified'),
|
|
@@ -257,8 +258,8 @@ const RAW = [
|
|
|
257
258
|
p('2.7.3.4', 'Heading structure available', 'supplemental', 'x2-7-3-4-heading-structure-available'),
|
|
258
259
|
p('2.7.3.5', 'Order detectable', 'core', 'x2-7-3-5-order-detectable'),
|
|
259
260
|
p('2.7.3.6', 'Blocks of content available (enhanced)', 'supplemental', 'x2-7-3-6-blocks-of-content-available-enhanced'),
|
|
260
|
-
p('2.7.3.7', 'Clear structure review', '
|
|
261
|
-
p('2.7.3.8', 'Key information usability testing', '
|
|
261
|
+
p('2.7.3.7', 'Clear structure review', 'assertion', 'x2-7-3-7-clear-structure-review'),
|
|
262
|
+
p('2.7.3.8', 'Key information usability testing', 'assertion', 'x2-7-3-8-key-information-usability-testing'),
|
|
262
263
|
g('2.7.4', 'No obstruction'),
|
|
263
264
|
p('2.7.4.1', 'Overlay content dismissible', 'core', 'x2-7-4-1-overlay-content-dismissible'),
|
|
264
265
|
g('2.8.1', 'Consistency'),
|
|
@@ -271,12 +272,12 @@ const RAW = [
|
|
|
271
272
|
g('2.9.2', 'Adequate time'),
|
|
272
273
|
p('2.9.2.1', 'Timeout adjustable', 'supplemental', 'x2-9-2-1-timeout-adjustable'),
|
|
273
274
|
p('2.9.2.2', 'No time limits', 'supplemental', 'x2-9-2-2-no-time-limits'),
|
|
274
|
-
p('2.9.2.3', 'No unnecessary time limits', '
|
|
275
|
+
p('2.9.2.3', 'No unnecessary time limits', 'assertion', 'x2-9-2-3-no-unnecessary-time-limits'),
|
|
275
276
|
p('2.9.2.4', 'Time limits conveyed', 'supplemental', 'x2-9-2-4-time-limits-conveyed'),
|
|
276
277
|
g('2.9.3', 'Avoid deception'),
|
|
277
278
|
p('2.9.3.1', 'Preselections visible', 'core', 'x2-9-3-1-preselections-visible'),
|
|
278
|
-
p('2.9.3.2', 'Deceptive practices usability testing', '
|
|
279
|
-
p('2.9.3.3', 'Deceptive messaging expert review', '
|
|
279
|
+
p('2.9.3.2', 'Deceptive practices usability testing', 'assertion', 'x2-9-3-2-deceptive-practices-usability-testing'),
|
|
280
|
+
p('2.9.3.3', 'Deceptive messaging expert review', 'assertion', 'x2-9-3-3-deceptive-messaging-expert-review'),
|
|
280
281
|
g('2.9.4', 'Retain information'),
|
|
281
282
|
p('2.9.4.1', 'Going back supported', 'supplemental', 'x2-9-4-1-going-back-supported'),
|
|
282
283
|
p('2.9.4.2', 'No redundant entry', 'supplemental', 'x2-9-4-2-no-redundant-entry'),
|
|
@@ -286,22 +287,22 @@ const RAW = [
|
|
|
286
287
|
p('2.9.5.2', 'Information requirements available at start', 'supplemental', 'x2-9-5-2-information-requirements-available-at-start'),
|
|
287
288
|
p('2.9.5.3', 'Process instructions available', 'supplemental', 'x2-9-5-3-process-instructions-available'),
|
|
288
289
|
g('2.9.6', 'Unnecessary steps'),
|
|
289
|
-
p('2.9.6.1', 'Usability testing for unnecessary steps', '
|
|
290
|
+
p('2.9.6.1', 'Usability testing for unnecessary steps', 'assertion', 'x2-9-6-1-usability-testing-for-unnecessary-steps'),
|
|
290
291
|
g('2.10.1', 'Risk'),
|
|
291
292
|
p('2.10.1.1', 'Consequences of choices explained', 'core', 'x2-10-1-1-consequences-of-choices-explained'),
|
|
292
293
|
p('2.10.1.2', 'Consequences explained before agreement', 'supplemental', 'x2-10-1-2-consequences-explained-before-agreement'),
|
|
293
|
-
p('2.10.1.3', 'Diverse disabilities considered', '
|
|
294
|
-
p('2.10.1.4', 'Algorithm inclusivity review', '
|
|
294
|
+
p('2.10.1.3', 'Diverse disabilities considered', 'assertion', 'x2-10-1-3-diverse-disabilities-considered'),
|
|
295
|
+
p('2.10.1.4', 'Algorithm inclusivity review', 'assertion', 'x2-10-1-4-algorithm-inclusivity-review'),
|
|
295
296
|
g('2.10.2', 'Algorithms'),
|
|
296
|
-
p('2.10.2.1', 'Inclusive data set', '
|
|
297
|
-
p('2.10.2.2', 'No harm from algorithms', '
|
|
297
|
+
p('2.10.2.1', 'Inclusive data set', 'assertion', 'x2-10-2-1-inclusive-data-set'),
|
|
298
|
+
p('2.10.2.2', 'No harm from algorithms', 'assertion', 'x2-10-2-2-no-harm-from-algorithms'),
|
|
298
299
|
g('2.11.1', 'Help available'),
|
|
299
300
|
p('2.11.1.1', 'Consistent help available', 'supplemental', 'x2-11-1-1-consistent-help-available'),
|
|
300
301
|
p('2.11.1.2', 'Contextual help available', 'supplemental', 'x2-11-1-2-contextual-help-available'),
|
|
301
302
|
p('2.11.1.3', 'Disabled controls explained', 'supplemental', 'x2-11-1-3-disabled-controls-explained'),
|
|
302
303
|
p('2.11.1.4', 'Sensory characteristics not relied on', 'core', 'x2-11-1-4-sensory-characteristics-not-relied-on'),
|
|
303
|
-
p('2.11.1.5', 'Supported decision-making review', '
|
|
304
|
-
p('2.11.1.6', 'Help usability testing', '
|
|
304
|
+
p('2.11.1.5', 'Supported decision-making review', 'assertion', 'x2-11-1-5-supported-decision-making-review'),
|
|
305
|
+
p('2.11.1.6', 'Help usability testing', 'assertion', 'x2-11-1-6-help-usability-testing'),
|
|
305
306
|
g('2.11.2', 'Feedback'),
|
|
306
307
|
p('2.11.2.1', 'Feedback mechanism available', 'supplemental', 'x2-11-2-1-feedback-mechanism-available'),
|
|
307
308
|
g('2.12.1', 'Assistive technology control'),
|
|
@@ -437,13 +438,20 @@ export const UNMAPPED_RULES = {
|
|
|
437
438
|
'on-input-navigation': 'The draft\'s change notifications (2.7.2.5, 2.12.5) cover different triggers from a change of context on input.',
|
|
438
439
|
};
|
|
439
440
|
|
|
440
|
-
/** The
|
|
441
|
-
|
|
442
|
-
|
|
441
|
+
/** The check set a value names, 'core' or 'supplemental', or null. */
|
|
442
|
+
export function checkSetOf(value) {
|
|
443
|
+
const name = String(value ?? '').toLowerCase();
|
|
444
|
+
return CHECK_SETS.includes(name) ? name : RENAMED_SETS[name] ?? null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** The WCAG 3 check set a tag list asks for, wcag3-core or
|
|
448
|
+
* wcag3-supplemental, the larger when both are present; null for a
|
|
449
|
+
* WCAG 2 audit. */
|
|
450
|
+
export function checkSetFromTags(tags) {
|
|
443
451
|
let found = null;
|
|
444
452
|
for (const tag of tags ?? []) {
|
|
445
|
-
const
|
|
446
|
-
if (
|
|
453
|
+
const set = /^wcag3-/.test(tag) ? checkSetOf(tag.slice(6)) : null;
|
|
454
|
+
if (set && (!found || CHECK_SETS.indexOf(set) > CHECK_SETS.indexOf(found))) found = set;
|
|
447
455
|
}
|
|
448
456
|
return found;
|
|
449
457
|
}
|
|
@@ -456,33 +464,35 @@ export function requirementsForRule(ruleId) {
|
|
|
456
464
|
});
|
|
457
465
|
}
|
|
458
466
|
|
|
459
|
-
/** Does the
|
|
460
|
-
export function
|
|
461
|
-
const types =
|
|
467
|
+
/** Does the check set take in any requirement this rule tests? */
|
|
468
|
+
export function ruleInCheckSet(ruleId, set) {
|
|
469
|
+
const types = SET_TYPES[set];
|
|
462
470
|
return Boolean(types) && (RULE_REQUIREMENTS[ruleId] ?? [])
|
|
463
471
|
.some((num) => types.has(provisionByNum.get(num).type));
|
|
464
472
|
}
|
|
465
473
|
|
|
466
474
|
/**
|
|
467
475
|
* Rule selection shared by every runner. A tag list selects the rules that
|
|
468
|
-
* carry any of its tags; a WCAG 3
|
|
469
|
-
*
|
|
476
|
+
* carry any of its tags; a WCAG 3 check set tag also selects the rules
|
|
477
|
+
* matched to its requirements. An empty or absent list selects every rule.
|
|
470
478
|
*/
|
|
471
479
|
export function ruleMatchesTags(rule, tags) {
|
|
472
480
|
if (!tags?.length) return true;
|
|
473
481
|
if (rule.tags.some((tag) => tags.includes(tag))) return true;
|
|
474
|
-
const
|
|
475
|
-
return Boolean(
|
|
482
|
+
const set = checkSetFromTags(tags);
|
|
483
|
+
return Boolean(set) && ruleInCheckSet(rule.id, set);
|
|
476
484
|
}
|
|
477
485
|
|
|
478
486
|
/**
|
|
479
|
-
* The
|
|
480
|
-
* entries: `level` carries the provision type and `principle`
|
|
481
|
-
* guideline. Every
|
|
487
|
+
* The check set's requirements as a human checklist, shaped like the WCAG 2
|
|
488
|
+
* manual-review entries: `level` carries the provision type and `principle`
|
|
489
|
+
* the guideline. Every requirement in the set is listed, since no rule was
|
|
482
490
|
* written for any of them; the ones a rule speaks to are 'partial'.
|
|
491
|
+
* Assertions are in neither set: they are not needed to conform, and only
|
|
492
|
+
* the people who make the content can make them.
|
|
483
493
|
*/
|
|
484
|
-
export function manualReviewChecklist(
|
|
485
|
-
const types =
|
|
494
|
+
export function manualReviewChecklist(set) {
|
|
495
|
+
const types = SET_TYPES[set];
|
|
486
496
|
if (!types) return [];
|
|
487
497
|
const partly = new Set(Object.values(RULE_REQUIREMENTS).flat());
|
|
488
498
|
return provisions
|
|
@@ -498,12 +508,13 @@ export function manualReviewChecklist(tier) {
|
|
|
498
508
|
}));
|
|
499
509
|
}
|
|
500
510
|
|
|
501
|
-
/** The results header for a WCAG 3 audit
|
|
502
|
-
|
|
503
|
-
|
|
511
|
+
/** The results header for a WCAG 3 audit: the draft, the check set and
|
|
512
|
+
* what it covers, as "core requirements". */
|
|
513
|
+
export function standardFor(set) {
|
|
514
|
+
return { ...draftInfo, checkSet: set, scope: SET_SCOPES[set] };
|
|
504
515
|
}
|
|
505
516
|
|
|
506
517
|
export default {
|
|
507
|
-
draftInfo,
|
|
508
|
-
|
|
518
|
+
draftInfo, CHECK_SETS, provisions, RULE_REQUIREMENTS, UNMAPPED_RULES,
|
|
519
|
+
checkSetOf, checkSetFromTags, requirementsForRule, ruleInCheckSet, ruleMatchesTags, manualReviewChecklist, standardFor,
|
|
509
520
|
};
|
package/package.json
CHANGED