pour-engine 1.40.0 → 1.42.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/config/project.config.js +1 -1
- package/engine/AUDIT.html +24 -690
- package/engine/COVERAGE.md +17 -15
- package/engine/README.md +10 -5
- package/engine/index.js +34 -8
- package/engine/lib/accessible-name.js +69 -63
- package/engine/lib/contrast.js +111 -10
- package/engine/lib/dom.js +78 -18
- package/engine/lib/roles.js +5 -2
- package/engine/rules/best-practice/fieldset-legend.js +11 -5
- package/engine/rules/best-practice/region.js +1 -1
- package/engine/rules/index.js +29 -0
- package/engine/rules/wcag/1.1.1-area-alt.js +6 -1
- package/engine/rules/wcag/1.1.1-image-alt.js +2 -1
- package/engine/rules/wcag/1.1.1-svg-img-alt.js +14 -17
- package/engine/rules/wcag/1.2.3-video-audio-description.js +42 -0
- package/engine/rules/wcag/1.3.1-aria-required-children.js +20 -8
- package/engine/rules/wcag/1.3.1-aria-required-parent.js +7 -10
- package/engine/rules/wcag/1.3.1-form-label.js +5 -29
- package/engine/rules/wcag/1.3.1-list-structure.js +4 -3
- package/engine/rules/wcag/1.3.1-table-headers.js +23 -2
- package/engine/rules/wcag/1.3.4-orientation.js +117 -48
- package/engine/rules/wcag/1.3.5-autocomplete-valid.js +21 -8
- package/engine/rules/wcag/1.3.6-region-purpose.js +35 -0
- package/engine/rules/wcag/1.4.1-link-in-text-block.js +19 -3
- package/engine/rules/wcag/1.4.10-reflow.js +3 -3
- package/engine/rules/wcag/1.4.11-non-text-contrast.js +9 -1
- package/engine/rules/wcag/1.4.12-text-spacing.js +15 -2
- package/engine/rules/wcag/1.4.3-color-contrast.js +79 -7
- package/engine/rules/wcag/1.4.3-control-contrast.js +35 -6
- package/engine/rules/wcag/1.4.4-meta-viewport.js +7 -2
- package/engine/rules/wcag/1.4.8-text-justified.js +49 -0
- package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +5 -5
- package/engine/rules/wcag/2.2.1-meta-refresh.js +10 -4
- package/engine/rules/wcag/2.2.2-pause-stop-hide.js +5 -5
- package/engine/rules/wcag/2.2.4-meta-refresh-no-exceptions.js +50 -0
- package/engine/rules/wcag/2.4.1-bypass-blocks.js +2 -2
- package/engine/rules/wcag/2.4.10-section-heading.js +86 -0
- package/engine/rules/wcag/2.4.11-focus-not-obscured.js +134 -78
- package/engine/rules/wcag/2.4.12-focus-not-obscured-enhanced.js +17 -0
- package/engine/rules/wcag/2.4.2-document-title.js +7 -8
- package/engine/rules/wcag/2.4.4-link-text-generic.js +6 -5
- package/engine/rules/wcag/2.4.6-heading-label-placeholder.js +69 -0
- package/engine/rules/wcag/2.4.9-link-text-generic-only.js +8 -23
- package/engine/rules/wcag/2.5.1-pointer-gesture-alternative.js +98 -0
- package/engine/rules/wcag/2.5.8-target-size.js +161 -45
- package/engine/rules/wcag/3.3.4-financial-form-confirmation.js +32 -0
- package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +10 -13
- package/engine/rules/wcag/3.3.9-captcha-alternative.js +35 -0
- package/engine/rules/wcag/4.1.2-aria-attr-valid.js +4 -0
- package/engine/rules/wcag/4.1.2-aria-field-name.js +6 -5
- package/engine/rules/wcag/4.1.2-aria-valid-refs.js +8 -3
- package/engine/rules/wcag/4.1.2-button-name.js +1 -1
- package/engine/rules/wcag/4.1.2-composite-widget-name.js +1 -1
- package/engine/rules/wcag/4.1.2-invoker-target.js +2 -1
- package/engine/rules/wcag/4.1.2-label-for-valid.js +2 -2
- package/engine/rules/wcag/4.1.2-valid-role.js +6 -1
- package/engine/wcag22.js +19 -19
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// WCAG SC 2.4.12 Focus Not Obscured (Enhanced) (Level AAA)
|
|
2
|
+
// 2.4.11's geometry with the containment requirement relaxed to any
|
|
3
|
+
// overlap: the enhanced criterion allows NO part of a focused component to
|
|
4
|
+
// be hidden by author-created content, and its Understanding names the
|
|
5
|
+
// sticky footer that clips the bottom of a focused link as the failure.
|
|
6
|
+
// Same review-not-fail discipline as the minimum rule: the snapshot proves
|
|
7
|
+
// where an element sits now, not where focus will find it.
|
|
8
|
+
import { createFocusObscuredRule } from './2.4.11-focus-not-obscured.js';
|
|
9
|
+
|
|
10
|
+
export default createFocusObscuredRule({
|
|
11
|
+
id: 'focus-not-obscured-enhanced',
|
|
12
|
+
name: 'Fully unobscured focus',
|
|
13
|
+
tags: ['wcag22aaa', 'wcag2412'],
|
|
14
|
+
help: 'No part of a focused element may be hidden behind overlays (AAA)',
|
|
15
|
+
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-enhanced.html',
|
|
16
|
+
partial: true,
|
|
17
|
+
});
|
|
@@ -3,18 +3,17 @@
|
|
|
3
3
|
// merely PRESENT satisfies the markup, not the criterion, so the two
|
|
4
4
|
// well-known undescriptive shapes are checked as well.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Familiar template strings are review candidates, not proof of a missing
|
|
7
|
+
// topic: a document can genuinely describe the HTML document title or an
|
|
8
|
+
// artwork named Untitled 1.
|
|
9
9
|
// Tier two, sent to a human: single generic words and framework scaffold
|
|
10
10
|
// defaults. Almost always a leftover, but a gallery page really can be about
|
|
11
11
|
// an artwork called "Untitled", and "Example Domain" really is the subject
|
|
12
12
|
// of example.com. Those are somebody's call to make, not the engine's.
|
|
13
13
|
// "New document", "New page" and "New tab" sit in tier two as well: a
|
|
14
14
|
// document editor's creation screen, a wiki's new-page form or a
|
|
15
|
-
// browser-style app's new-tab page is about exactly that
|
|
16
|
-
//
|
|
17
|
-
// placeholder (2026-08-25 overnight audit).
|
|
15
|
+
// browser-style app's new-tab page is about exactly that. Even numbered
|
|
16
|
+
// editor-style titles remain a question about the actual subject.
|
|
18
17
|
//
|
|
19
18
|
// Deliberately NOT judged: whether a real-looking title matches the page it
|
|
20
19
|
// sits on. That needs someone who can read the page, and guessing at it
|
|
@@ -63,8 +62,8 @@ export default {
|
|
|
63
62
|
const normalized = title.replace(/\s+/g, ' ').toLowerCase();
|
|
64
63
|
if (PLACEHOLDER.has(normalized) || /^(?:untitled|new (?:document|page|tab))[\s-]*\d+$/.test(normalized)) {
|
|
65
64
|
return {
|
|
66
|
-
status: '
|
|
67
|
-
message: `“${title}”
|
|
65
|
+
status: 'incomplete',
|
|
66
|
+
message: `“${title}” resembles a template title. Check whether it describes this page's actual topic or purpose; the same words can also be a legitimate subject or name.`,
|
|
68
67
|
fix: 'Put this page\'s own subject in the title first, then the site name.',
|
|
69
68
|
};
|
|
70
69
|
}
|
|
@@ -13,11 +13,9 @@
|
|
|
13
13
|
// it sits in. A "Read more" under a headline naming the
|
|
14
14
|
// article is a judgment call about whether that context really
|
|
15
15
|
// reaches the user, so this REVIEWS. It never asserts.
|
|
16
|
-
// 2.4.9 (AAA)
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// ambiguous to everyone, which is the exception failing to
|
|
20
|
-
// apply rather than saving it, so this FAILS.
|
|
16
|
+
// 2.4.9 (AAA) requires a mechanism for purpose to be available from link
|
|
17
|
+
// text alone. A page may let users expand its link names,
|
|
18
|
+
// so a generic name in this snapshot still needs review.
|
|
21
19
|
//
|
|
22
20
|
// Judged on the ACCESSIBLE NAME, not the text content: a link written as
|
|
23
21
|
// <a href="…" aria-label="Read more about the budget">Read more</a> is
|
|
@@ -31,6 +29,8 @@
|
|
|
31
29
|
// normalizeName below): apostrophes become spaces, so "plus d informations"
|
|
32
30
|
// is how "plus d'informations" — straight or typographic — must be listed.
|
|
33
31
|
|
|
32
|
+
import { effectiveRole, implicitRole } from '../../lib/roles.js';
|
|
33
|
+
|
|
34
34
|
const GENERIC = new Set([
|
|
35
35
|
// English
|
|
36
36
|
'read more', 'read more about this', 'more', 'much more', 'learn more',
|
|
@@ -83,6 +83,7 @@ export function createLinkPurposeRule({ id, impact, tags, help, helpUrl, verdict
|
|
|
83
83
|
// link-name's finding, not this one.
|
|
84
84
|
selector: 'a[href], [role="link"]',
|
|
85
85
|
evaluate(element, { accessibleName }) {
|
|
86
|
+
if ((effectiveRole(element) ?? implicitRole(element)) !== 'link') return { status: 'pass' };
|
|
86
87
|
const name = accessibleName(element);
|
|
87
88
|
if (!name) return { status: 'pass' }; // nameless: link-name reports it
|
|
88
89
|
const normalized = normalizeName(name);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// WCAG SC 2.4.6 Headings and Labels (Level AA)
|
|
2
|
+
// "Headings and labels describe topic or purpose." The criterion does not
|
|
3
|
+
// require a heading or label to exist; it requires that one that IS there
|
|
4
|
+
// says what its section or field is about. Whether real words do that
|
|
5
|
+
// needs a reader. What the DOM can show is the opposite case: a heading or
|
|
6
|
+
// label whose text is a page builder's scaffold string ("Add Your Heading
|
|
7
|
+
// Text Here", "Lorem ipsum", "Heading 1") left in place, which describes
|
|
8
|
+
// nothing by construction. Same two tiers as the page-title rule:
|
|
9
|
+
// - scaffold strings and lorem ipsum FAIL: no page is about them;
|
|
10
|
+
// - a single generic word ("Heading", "Title", "Label") is REVIEWED: an
|
|
11
|
+
// article can genuinely be titled "Untitled" and a design-system page
|
|
12
|
+
// can be about headings.
|
|
13
|
+
// Empty headings belong to the best-practice empty-heading rule; missing
|
|
14
|
+
// labels belong to 3.3.2 and 4.1.2. This rule reads visible text only and
|
|
15
|
+
// never judges the meaning of ordinary words.
|
|
16
|
+
const SCAFFOLD = new Set([
|
|
17
|
+
'add your heading text here', 'add a heading', 'add heading', 'your heading here', 'heading here',
|
|
18
|
+
'your title goes here', 'your title here', 'title goes here', 'title here', 'insert title here',
|
|
19
|
+
'insert heading here', 'heading text', 'heading goes here', 'section title here',
|
|
20
|
+
'placeholder', 'placeholder text', 'placeholder heading', 'label text', 'your label here',
|
|
21
|
+
'text here', 'enter text here', 'your text here', 'type here', 'sample heading', 'sample text',
|
|
22
|
+
'dummy text', 'dummy heading', 'todo', 'tbd', 'xxx', 'xxxx', 'asdf',
|
|
23
|
+
'this is a heading', 'this is a title', 'this is the heading', 'this is a label',
|
|
24
|
+
'new heading', 'new label', 'new section',
|
|
25
|
+
]);
|
|
26
|
+
const GENERIC = new Set([
|
|
27
|
+
'heading', 'title', 'subtitle', 'headline', 'label', 'untitled', 'header', 'text', 'default',
|
|
28
|
+
'section', 'section title', 'section heading', 'page title', 'field', 'input',
|
|
29
|
+
]);
|
|
30
|
+
// "Heading 1" … "Heading 6", "h1" … "h6", "Title 2": the numbered defaults
|
|
31
|
+
// editors and builders insert.
|
|
32
|
+
const NUMBERED = /^(?:heading|title|label|h)\s*[1-6]$/;
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
id: 'heading-label-placeholder',
|
|
36
|
+
name: 'Placeholder headings and labels',
|
|
37
|
+
impact: 'moderate',
|
|
38
|
+
tags: ['wcag2aa', 'wcag246'],
|
|
39
|
+
help: 'Headings and labels must describe their topic or purpose, not be template text',
|
|
40
|
+
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html',
|
|
41
|
+
selector: 'h1, h2, h3, h4, h5, h6, [role="heading"], label, legend',
|
|
42
|
+
evaluate(element) {
|
|
43
|
+
const role = element.getAttribute('role');
|
|
44
|
+
if (/^h[1-6]$/i.test(element.tagName) && role && role !== 'heading') return { status: 'pass' };
|
|
45
|
+
const text = (element.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
46
|
+
if (!text) return { status: 'pass' };
|
|
47
|
+
const normalized = text.toLowerCase().replace(/[\s.:!…-]+$/, '');
|
|
48
|
+
const kind = /^h[1-6]$/i.test(element.tagName) || role === 'heading' ? 'heading' : 'label';
|
|
49
|
+
if (SCAFFOLD.has(normalized) || normalized.startsWith('lorem ipsum') || NUMBERED.test(normalized)) {
|
|
50
|
+
return {
|
|
51
|
+
status: 'fail',
|
|
52
|
+
message: `“${text}” is template text left in a ${kind}. It describes nothing, so a screen-reader user scanning by ${kind}s, or anyone reading the form, learns nothing from it.`,
|
|
53
|
+
fix: kind === 'heading'
|
|
54
|
+
? 'Replace it with words that name what the section is about.'
|
|
55
|
+
: 'Replace it with words that name what the field asks for.',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (GENERIC.has(normalized)) {
|
|
59
|
+
return {
|
|
60
|
+
status: 'incomplete',
|
|
61
|
+
message: `“${text}” is a generic ${kind}. Check whether it describes the topic of its section or the purpose of its field; the same word can also be a real subject or name.`,
|
|
62
|
+
fix: kind === 'heading'
|
|
63
|
+
? 'Name the section\'s actual topic in the heading.'
|
|
64
|
+
: 'Name what the field asks for in the label.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { status: 'pass' };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -4,28 +4,13 @@
|
|
|
4
4
|
// threshold: the link text ALONE has to identify the purpose, and context
|
|
5
5
|
// is not allowed to make up the difference.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// everyone — which is the exception failing to apply rather than rescuing
|
|
11
|
-
// it. Nothing about the surrounding page can change that reading, so there
|
|
12
|
-
// is nothing here for a human to settle.
|
|
7
|
+
// The criterion allows a mechanism that expands link names. F84 requires
|
|
8
|
+
// checking that no such mechanism exists, which a name-only snapshot cannot
|
|
9
|
+
// prove. Semantic ambiguity and alternative presentation remain review.
|
|
13
10
|
//
|
|
14
|
-
//
|
|
15
|
-
// (
|
|
16
|
-
//
|
|
17
|
-
// term that names its destination exactly (one-sided decoration like
|
|
18
|
-
// python.org's CSS-generated ">>>More" prompt still asserts);
|
|
19
|
-
// · a name that is exactly "go" — every corpus hit was a brand logo
|
|
20
|
-
// ("Go" is the language's name), and a brand name identifies the link.
|
|
21
|
-
//
|
|
22
|
-
// A third, beside "go" (2026-08-25 overnight audit): the French entries
|
|
23
|
-
// "plus" and "suite" are ordinary English words and, more to the point,
|
|
24
|
-
// common English plan and product names (a "Plus" tier, a "Suite"). A
|
|
25
|
-
// pricing link named "Plus" identifies its destination the way the Go logo
|
|
26
|
-
// does, so outside a page declared French they review rather than assert.
|
|
27
|
-
// On a link whose closest lang is French the generic reading ("more",
|
|
28
|
-
// "continued") is the only one and the assertion stands.
|
|
11
|
+
// The review wording distinguishes recognizable proper-name ambiguities
|
|
12
|
+
// (Go, Plus, Suite and quoted code terms) from ordinary generic phrases.
|
|
13
|
+
// Neither branch treats a phrase-list match as semantic proof.
|
|
29
14
|
import { createLinkPurposeRule } from './2.4.4-link-text-generic.js';
|
|
30
15
|
|
|
31
16
|
const FRENCH_ONLY_ENGLISH_NAMES = new Set(['plus', 'suite']);
|
|
@@ -50,8 +35,8 @@ export default createLinkPurposeRule({
|
|
|
50
35
|
fix: 'If the name really is generic here, put the destination in the link text, e.g. "Read more about the 2026 budget".',
|
|
51
36
|
};
|
|
52
37
|
return {
|
|
53
|
-
status: '
|
|
54
|
-
message: `“${name}”
|
|
38
|
+
status: 'incomplete',
|
|
39
|
+
message: `“${name}” appears generic when read alone. Check whether it identifies the destination in this context or whether a mechanism makes the link text descriptive. 2.4.9 permits such a mechanism; the initial link name alone cannot establish a failure.`,
|
|
55
40
|
fix: 'Name the destination in the link text, e.g. "Read more about the 2026 budget".',
|
|
56
41
|
};
|
|
57
42
|
},
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// WCAG SC 2.5.1 Pointer Gestures (Level A)
|
|
2
|
+
// Functionality operated by a path-based gesture (a swipe, a drag along a
|
|
3
|
+
// track, a drawn shape) or a multipoint gesture (a pinch) must also work
|
|
4
|
+
// with a single pointer and no path, unless the gesture is essential. The
|
|
5
|
+
// gesture logic itself lives in script, which a scan cannot read. But a
|
|
6
|
+
// page that implements its own swipe, pan or pinch has to tell the
|
|
7
|
+
// browser to stand aside first, and it does that in one visible place: the
|
|
8
|
+
// CSS touch-action property. `touch-action: none` hands every touch to
|
|
9
|
+
// the script; `pan-y` keeps vertical scrolling native and hands horizontal
|
|
10
|
+
// swipes (the carousel) to the script; `pan-x` the reverse; a value
|
|
11
|
+
// without `pinch-zoom` hands the pinch (the map) to the script. Those
|
|
12
|
+
// elements are the honest candidates. Whether the swipe or pinch has a
|
|
13
|
+
// single-pointer twin (next and previous buttons, zoom buttons) is not
|
|
14
|
+
// decidable from markup, so this asks and never asserts.
|
|
15
|
+
//
|
|
16
|
+
// Kept out, and why:
|
|
17
|
+
// - `auto` and `manipulation`: nothing is handed to script beyond the
|
|
18
|
+
// double-tap zoom delay.
|
|
19
|
+
// - `pan-x pan-y` (both axes native, only the pinch withheld): measured
|
|
20
|
+
// on the corpus as a scroll-snap carousel and "no pinch zoom" wrappers,
|
|
21
|
+
// never a pinch handler; the browser still does all the panning.
|
|
22
|
+
// - range inputs and role="slider": the Understanding says a slider that
|
|
23
|
+
// drifts off its track is not path-based, and G216 covers it.
|
|
24
|
+
// - draggable="true" elements: 2.5.7's question (drag-alternative).
|
|
25
|
+
// - elements under 100 by 60 px: a gesture surface that small is not one.
|
|
26
|
+
// Only the outermost candidate in a subtree is reported, once; descendants
|
|
27
|
+
// that inherit or repeat the declaration ride with it. Stylesheets a
|
|
28
|
+
// cross-origin policy hides are not a problem here: the computed value on
|
|
29
|
+
// the element is read, whatever sheet set it.
|
|
30
|
+
const HANDLED = new Set(['auto', 'manipulation']);
|
|
31
|
+
|
|
32
|
+
function handsGestureToScript(value) {
|
|
33
|
+
if (!value || HANDLED.has(value)) return false;
|
|
34
|
+
const tokens = value.split(/\s+/);
|
|
35
|
+
if (tokens.includes('none')) return true;
|
|
36
|
+
const panX = tokens.some((t) => /^pan-(x|left|right)$/.test(t));
|
|
37
|
+
const panY = tokens.some((t) => /^pan-(y|up|down)$/.test(t));
|
|
38
|
+
// One axis withheld is a swipe handler (pan-y is the carousel; pan-x the
|
|
39
|
+
// vertical one); both axes native is the browser scrolling as usual.
|
|
40
|
+
return !panX || !panY;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
id: 'pointer-gesture-alternative',
|
|
45
|
+
name: 'Pointer gesture alternative',
|
|
46
|
+
impact: 'serious',
|
|
47
|
+
tags: ['wcag21a', 'wcag251'],
|
|
48
|
+
help: 'Swipe, pan and pinch gestures need a single-pointer alternative',
|
|
49
|
+
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/pointer-gestures.html',
|
|
50
|
+
selector: '*',
|
|
51
|
+
visibleOnly: false,
|
|
52
|
+
evaluateAll(elements, { isRendered }) {
|
|
53
|
+
const candidates = new Set();
|
|
54
|
+
for (const element of elements) {
|
|
55
|
+
const tag = element.tagName;
|
|
56
|
+
if (tag === 'HTML' || tag === 'BODY' || tag === 'SCRIPT' || tag === 'STYLE' || tag === 'LINK' || tag === 'META' || tag === 'TEMPLATE') continue;
|
|
57
|
+
// Cheap gates first: the style read is paid only by rendered boxes
|
|
58
|
+
// that could be a gesture surface at all.
|
|
59
|
+
if (element.getAttribute('draggable') === 'true' || element.getAttribute('role') === 'slider' || (tag === 'INPUT' && element.type === 'range')) continue;
|
|
60
|
+
const rect = element.getBoundingClientRect();
|
|
61
|
+
if (rect.width < 100 || rect.height < 60) continue;
|
|
62
|
+
if (!isRendered(element)) continue;
|
|
63
|
+
const value = getComputedStyle(element).touchAction;
|
|
64
|
+
if (!handsGestureToScript(value)) continue;
|
|
65
|
+
candidates.add(element);
|
|
66
|
+
}
|
|
67
|
+
// Outermost only, and one per row of siblings: a carousel's track,
|
|
68
|
+
// slides and images all inherit the same declaration (and a cloned
|
|
69
|
+
// slide strip repeats it eleven times, measured on a corpus home), and
|
|
70
|
+
// one question per gesture surface is enough.
|
|
71
|
+
const outermost = new Set();
|
|
72
|
+
const reportedParents = new Set();
|
|
73
|
+
for (const element of candidates) {
|
|
74
|
+
let nested = false;
|
|
75
|
+
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
|
|
76
|
+
if (candidates.has(parent)) { nested = true; break; }
|
|
77
|
+
}
|
|
78
|
+
if (nested || reportedParents.has(element.parentElement)) continue;
|
|
79
|
+
reportedParents.add(element.parentElement);
|
|
80
|
+
outermost.add(element);
|
|
81
|
+
}
|
|
82
|
+
return elements.map((element) => {
|
|
83
|
+
if (!outermost.has(element)) return { status: 'pass' };
|
|
84
|
+
const value = getComputedStyle(element).touchAction;
|
|
85
|
+
const tokens = value.split(/\s+/);
|
|
86
|
+
const handled = tokens.includes('none') ? 'every touch gesture'
|
|
87
|
+
: [
|
|
88
|
+
!tokens.some((t) => /^pan-(x|left|right)$/.test(t)) && 'horizontal swipes',
|
|
89
|
+
!tokens.some((t) => /^pan-(y|up|down)$/.test(t)) && 'vertical swipes',
|
|
90
|
+
].filter(Boolean).join(' and ');
|
|
91
|
+
return {
|
|
92
|
+
status: 'incomplete',
|
|
93
|
+
message: `touch-action: ${value} tells the browser to hand ${handled} on this element to the page's own script, which is how custom swipe, pan and pinch interactions are built. 2.5.1 requires whatever those gestures do to also work with a single tap or click (previous and next buttons, zoom in and out, a choose-a-value control), unless the gesture is essential. Check a single-pointer way exists for everything the gesture does.`,
|
|
94
|
+
fix: 'Add plain buttons for the same actions (previous/next, zoom in/out), or make the element operable by simple clicks as well as gestures.',
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
};
|