pour-engine 1.29.0 → 1.31.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 +15 -0
- package/engine/rules/best-practice/landmark-one-main.js +11 -2
- package/engine/rules/best-practice/landmark-top-level.js +20 -5
- 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 +51 -7
- 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 +17 -3
- 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 +19 -3
- 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
|
@@ -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,11 +42,16 @@ 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
|
-
help: 'aria-label on a plain container is prohibited by ARIA, so its announcement is not guaranteed',
|
|
48
|
+
help: 'aria-label or aria-labelledby on a plain container is prohibited by ARIA, so its announcement is not guaranteed',
|
|
48
49
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
|
|
49
|
-
|
|
50
|
+
// §5.2.8.6 prohibits BOTH naming attributes on generic — aria-labelledby
|
|
51
|
+
// pointing at perfectly real text is the same authoring error as an inline
|
|
52
|
+
// aria-label, and the more tempting one (a card labelled by its own title).
|
|
53
|
+
selector: 'div[aria-label]:not([role]):not([tabindex]), span[aria-label]:not([role]):not([tabindex]), p[aria-label]:not([role]):not([tabindex]),'
|
|
54
|
+
+ ' div[aria-labelledby]:not([role]):not([tabindex]), span[aria-labelledby]:not([role]):not([tabindex]), p[aria-labelledby]:not([role]):not([tabindex])',
|
|
50
55
|
evaluate(element) {
|
|
51
56
|
// Inside a name-from-content ancestor the label is NOT ignored: accname
|
|
52
57
|
// applies the whole algorithm to each descendant while building the
|
|
@@ -54,9 +59,20 @@ export default {
|
|
|
54
59
|
// Chromium reports exactly that. Claiming it does nothing is false, and
|
|
55
60
|
// the advised fix would delete the control's only name.
|
|
56
61
|
if (element.closest(NAME_FROM_CONTENT)) return { status: 'pass' };
|
|
62
|
+
const label = element.getAttribute('aria-label');
|
|
63
|
+
const attempt = label !== null
|
|
64
|
+
? `aria-label="${label}"`
|
|
65
|
+
: (() => {
|
|
66
|
+
const text = (element.getAttribute('aria-labelledby') ?? '').trim().split(/\s+/)
|
|
67
|
+
.map((id) => element.ownerDocument.getElementById(id)?.textContent?.trim())
|
|
68
|
+
.filter(Boolean).join(' ');
|
|
69
|
+
return text
|
|
70
|
+
? `aria-labelledby (naming it "${text.slice(0, 80)}")`
|
|
71
|
+
: 'aria-labelledby (whose references resolve to no text)';
|
|
72
|
+
})();
|
|
57
73
|
return {
|
|
58
74
|
status: 'incomplete',
|
|
59
|
-
message:
|
|
75
|
+
message: `${attempt} on a plain <${element.tagName.toLowerCase()}> is prohibited by ARIA: this element's role does not support naming, so although many browser and screen reader pairings announce the label today, that support is not guaranteed anywhere and can differ between assistive technologies. Where it is announced it replaces the element's visible text; where it is not, users get the visible text alone. Check both presentations read correctly, and that nothing here relies on the label being heard.`,
|
|
60
76
|
fix: 'If this element must reliably announce something of its own, give it the role it is playing (a landmark, group, or widget role) so the name is legal and exposure is guaranteed — or move the spoken text into visible or visually-hidden real text. If the label is stray, remove it. Do not add a role solely to legalise the label.',
|
|
61
77
|
};
|
|
62
78
|
},
|
|
@@ -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