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.
Files changed (88) hide show
  1. package/README.md +1 -6
  2. package/config/project.config.js +1 -1
  3. package/engine/index.js +15 -16
  4. package/engine/rules/best-practice/accesskeys.js +1 -0
  5. package/engine/rules/best-practice/button-type.js +1 -0
  6. package/engine/rules/best-practice/empty-heading.js +1 -0
  7. package/engine/rules/best-practice/fieldset-legend.js +1 -0
  8. package/engine/rules/best-practice/heading-order.js +15 -0
  9. package/engine/rules/best-practice/landmark-one-main.js +11 -2
  10. package/engine/rules/best-practice/landmark-top-level.js +20 -5
  11. package/engine/rules/best-practice/landmark-unique.js +1 -0
  12. package/engine/rules/best-practice/multiple-labels.js +1 -0
  13. package/engine/rules/best-practice/new-window-link.js +1 -0
  14. package/engine/rules/best-practice/no-autofocus.js +1 -0
  15. package/engine/rules/best-practice/page-heading-one.js +1 -0
  16. package/engine/rules/best-practice/positive-tabindex.js +1 -0
  17. package/engine/rules/best-practice/redundant-alt-phrase.js +1 -0
  18. package/engine/rules/best-practice/redundant-aria-label.js +1 -0
  19. package/engine/rules/best-practice/redundant-aria.js +1 -0
  20. package/engine/rules/best-practice/redundant-image-alt.js +51 -7
  21. package/engine/rules/best-practice/redundant-role.js +1 -0
  22. package/engine/rules/best-practice/redundant-tabindex.js +1 -0
  23. package/engine/rules/best-practice/region.js +17 -3
  24. package/engine/rules/wcag/1.1.1-area-alt.js +1 -0
  25. package/engine/rules/wcag/1.1.1-canvas-alt.js +1 -0
  26. package/engine/rules/wcag/1.1.1-embed-alt.js +1 -0
  27. package/engine/rules/wcag/1.1.1-image-alt.js +1 -0
  28. package/engine/rules/wcag/1.1.1-input-image-alt.js +1 -0
  29. package/engine/rules/wcag/1.1.1-object-alt.js +1 -0
  30. package/engine/rules/wcag/1.1.1-svg-img-alt.js +1 -0
  31. package/engine/rules/wcag/1.2.2-media-captions.js +12 -5
  32. package/engine/rules/wcag/1.3.1-aria-required-children.js +1 -0
  33. package/engine/rules/wcag/1.3.1-aria-required-parent.js +1 -0
  34. package/engine/rules/wcag/1.3.1-definition-list.js +1 -0
  35. package/engine/rules/wcag/1.3.1-dlitem-parent.js +1 -0
  36. package/engine/rules/wcag/1.3.1-form-label.js +1 -0
  37. package/engine/rules/wcag/1.3.1-list-structure.js +1 -0
  38. package/engine/rules/wcag/1.3.1-listitem-parent.js +1 -0
  39. package/engine/rules/wcag/1.3.1-p-as-heading.js +1 -0
  40. package/engine/rules/wcag/1.3.1-table-headers.js +1 -0
  41. package/engine/rules/wcag/1.3.4-orientation.js +1 -0
  42. package/engine/rules/wcag/1.3.5-autocomplete-valid.js +1 -0
  43. package/engine/rules/wcag/1.4.1-link-in-text-block.js +1 -0
  44. package/engine/rules/wcag/1.4.10-reflow.js +1 -0
  45. package/engine/rules/wcag/1.4.11-non-text-contrast.js +1 -0
  46. package/engine/rules/wcag/1.4.12-text-spacing.js +1 -0
  47. package/engine/rules/wcag/1.4.2-audio-control.js +1 -0
  48. package/engine/rules/wcag/1.4.3-color-contrast.js +1 -0
  49. package/engine/rules/wcag/1.4.3-control-contrast.js +1 -0
  50. package/engine/rules/wcag/1.4.4-meta-viewport.js +1 -0
  51. package/engine/rules/wcag/1.4.6-contrast-enhanced.js +1 -0
  52. package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +1 -0
  53. package/engine/rules/wcag/2.2.1-meta-refresh.js +1 -0
  54. package/engine/rules/wcag/2.2.2-pause-stop-hide.js +1 -0
  55. package/engine/rules/wcag/2.2.2-video-loop-motion.js +1 -0
  56. package/engine/rules/wcag/2.4.1-bypass-blocks.js +1 -0
  57. package/engine/rules/wcag/2.4.11-focus-not-obscured.js +1 -0
  58. package/engine/rules/wcag/2.4.2-document-title.js +1 -0
  59. package/engine/rules/wcag/2.4.3-visual-order-divergence.js +1 -0
  60. package/engine/rules/wcag/2.4.4-link-name.js +1 -0
  61. package/engine/rules/wcag/2.4.4-link-text-generic.js +1 -0
  62. package/engine/rules/wcag/2.4.7-focus-visible.js +1 -0
  63. package/engine/rules/wcag/2.4.9-link-text-generic-only.js +1 -0
  64. package/engine/rules/wcag/2.5.3-label-in-name.js +1 -0
  65. package/engine/rules/wcag/2.5.5-target-size-enhanced.js +1 -0
  66. package/engine/rules/wcag/2.5.8-target-size.js +1 -0
  67. package/engine/rules/wcag/3.1.1-html-lang.js +1 -0
  68. package/engine/rules/wcag/3.1.2-valid-lang-parts.js +1 -0
  69. package/engine/rules/wcag/3.2.2-on-input-navigation.js +1 -0
  70. package/engine/rules/wcag/3.3.1-error-message-linkage.js +1 -0
  71. package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +1 -0
  72. package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +1 -0
  73. package/engine/rules/wcag/4.1.2-aria-attr-valid.js +1 -0
  74. package/engine/rules/wcag/4.1.2-aria-field-name.js +1 -0
  75. package/engine/rules/wcag/4.1.2-aria-hidden-focus.js +1 -0
  76. package/engine/rules/wcag/4.1.2-aria-label-misuse.js +19 -3
  77. package/engine/rules/wcag/4.1.2-aria-valid-refs.js +1 -0
  78. package/engine/rules/wcag/4.1.2-button-name.js +1 -0
  79. package/engine/rules/wcag/4.1.2-composite-widget-name.js +1 -0
  80. package/engine/rules/wcag/4.1.2-dialog-name.js +1 -0
  81. package/engine/rules/wcag/4.1.2-frame-title.js +1 -0
  82. package/engine/rules/wcag/4.1.2-label-for-valid.js +1 -0
  83. package/engine/rules/wcag/4.1.2-nested-interactive.js +1 -0
  84. package/engine/rules/wcag/4.1.2-role-required-aria.js +1 -0
  85. package/engine/rules/wcag/4.1.2-summary-name.js +1 -0
  86. package/engine/rules/wcag/4.1.2-valid-role.js +1 -0
  87. package/package.json +1 -1
  88. package/engine/wcag30-draft.js +0 -149
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.3.1 Info and Relationships (Level A)
2
2
  export default {
3
3
  id: 'p-as-heading',
4
+ name: 'Styled text as heading',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag131'],
6
7
  help: 'Bold paragraphs should not stand in for headings',
@@ -9,6 +9,7 @@
9
9
  // better than false positives on real pages.
10
10
  export default {
11
11
  id: 'table-headers',
12
+ name: 'Table header associations',
12
13
  impact: 'serious',
13
14
  tags: ['wcag2a', 'wcag131'],
14
15
  help: 'Data tables must have properly associated header cells',
@@ -46,6 +46,7 @@ function scanRules(rules, insideOrientation, findings) {
46
46
 
47
47
  export default {
48
48
  id: 'orientation-lock',
49
+ name: 'Orientation lock',
49
50
  impact: 'serious',
50
51
  tags: ['wcag21aa', 'wcag134'],
51
52
  help: 'Content must work in both portrait and landscape',
@@ -16,6 +16,7 @@ const TOKENS = new Set([
16
16
 
17
17
  export default {
18
18
  id: 'autocomplete-valid',
19
+ name: 'Autocomplete tokens',
19
20
  impact: 'serious',
20
21
  tags: ['wcag21aa', 'wcag135'],
21
22
  help: 'autocomplete attributes must use valid tokens',
@@ -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',
@@ -6,6 +6,7 @@
6
6
  // and images are exempt content the geometry can't distinguish.
7
7
  export default {
8
8
  id: 'reflow',
9
+ name: 'Content reflow',
9
10
  impact: 'moderate',
10
11
  tags: ['wcag21aa', 'wcag1410'],
11
12
  help: 'Content should reflow to one-dimensional scrolling',
@@ -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',
@@ -16,6 +16,7 @@ const clipped = (element) =>
16
16
 
17
17
  export default {
18
18
  id: 'text-spacing',
19
+ name: 'Text spacing overrides',
19
20
  impact: 'serious',
20
21
  tags: ['wcag21aa', 'wcag1412'],
21
22
  help: 'Text must survive user spacing overrides without being cut off',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.4.2 Audio Control (Level A)
2
2
  export default {
3
3
  id: 'audio-control',
4
+ name: 'Auto-playing audio control',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag142'],
6
7
  help: 'Auto-playing audio must be stoppable or muted',
@@ -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',
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  export default {
11
11
  id: 'control-contrast',
12
+ name: 'Form control contrast',
12
13
  impact: 'serious',
13
14
  tags: ['wcag2aa', 'wcag143'],
14
15
  help: 'Text inside form controls must have sufficient contrast',
@@ -3,6 +3,7 @@ import { isEmbeddedDocument } from '../../lib/dom.js';
3
3
 
4
4
  export default {
5
5
  id: 'meta-viewport',
6
+ name: 'Viewport zoom lock',
6
7
  impact: 'critical',
7
8
  tags: ['wcag2aa', 'wcag144'],
8
9
  help: 'The viewport must let users zoom to at least 200%',
@@ -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',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 2.2.1 Timing Adjustable (Level A)
2
2
  export default {
3
3
  id: 'meta-refresh',
4
+ name: 'Timed page refresh',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag221'],
6
7
  help: 'The page must not use a timed refresh',
@@ -79,6 +79,7 @@ function endlesslyMoving(doc) {
79
79
 
80
80
  export default {
81
81
  id: 'pause-stop-hide',
82
+ name: 'Pausable moving content',
82
83
  impact: 'serious',
83
84
  tags: ['wcag2a', 'wcag222'],
84
85
  help: 'Moving content must be pausable',
@@ -15,6 +15,7 @@
15
15
  // abstaining, not clearing.
16
16
  export default {
17
17
  id: 'video-loop-motion',
18
+ name: 'Looping video control',
18
19
  impact: 'serious',
19
20
  tags: ['wcag2a', 'wcag222'],
20
21
  help: 'Looping autoplay video must be pausable',
@@ -3,6 +3,7 @@ import { collectRoots, isEmbeddedDocument } from '../../lib/dom.js';
3
3
 
4
4
  export default {
5
5
  id: 'bypass-blocks',
6
+ name: 'Skip to content',
6
7
  impact: 'serious',
7
8
  tags: ['wcag2a', 'wcag241'],
8
9
  help: 'The page must offer a way to skip repeated blocks',
@@ -31,6 +31,7 @@ function edgeReserved(doc, edge, needed) {
31
31
 
32
32
  export default {
33
33
  id: 'focus-not-obscured',
34
+ name: 'Unobscured focus',
34
35
  impact: 'serious',
35
36
  tags: ['wcag22aa', 'wcag2411'],
36
37
  help: 'Focused elements must not be fully hidden behind overlays',
@@ -32,6 +32,7 @@ const UNDESCRIPTIVE = new Set([
32
32
 
33
33
  export default {
34
34
  id: 'document-title',
35
+ name: 'Page title',
35
36
  impact: 'serious',
36
37
  tags: ['wcag2a', 'wcag242'],
37
38
  help: 'The page must have a title that describes its topic or purpose',
@@ -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',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 2.4.4 Link Purpose (In Context) (Level A) · SC 4.1.2 Name, Role, Value (Level A)
2
2
  export default {
3
3
  id: 'link-name',
4
+ name: 'Link names',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag244', 'wcag412'],
6
7
  help: 'Links must have an accessible name',
@@ -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',
@@ -52,6 +52,7 @@ function focusSuppressors(doc) {
52
52
 
53
53
  export default {
54
54
  id: 'focus-visible',
55
+ name: 'Visible focus',
55
56
  impact: 'serious',
56
57
  tags: ['wcag2aa', 'wcag247'],
57
58
  help: 'Keyboard focus must remain visible',
@@ -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)',
@@ -71,6 +71,7 @@ function visibleText(nodes) {
71
71
 
72
72
  export default {
73
73
  id: 'label-in-name',
74
+ name: 'Label in accessible name',
74
75
  impact: 'serious',
75
76
  tags: ['wcag21a', 'wcag253'],
76
77
  help: 'The visible label must be part of the accessible name',
@@ -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',
@@ -13,6 +13,7 @@
13
13
  // case worth asserting.
14
14
  export default {
15
15
  id: 'auth-field-obstruction',
16
+ name: 'Accessible login fields',
16
17
  impact: 'serious',
17
18
  tags: ['wcag22aa', 'wcag338'],
18
19
  help: 'Login fields must not block paste or password managers',
@@ -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',
@@ -24,6 +24,7 @@ const ENUMS = {
24
24
 
25
25
  export default {
26
26
  id: 'aria-attr-valid',
27
+ name: 'Valid ARIA values',
27
28
  impact: 'critical',
28
29
  tags: ['wcag2a', 'wcag412'],
29
30
  help: 'aria attributes must exist and have valid values',
@@ -23,6 +23,7 @@ function authorName(element) {
23
23
 
24
24
  export default {
25
25
  id: 'aria-field-name',
26
+ name: 'ARIA field names',
26
27
  impact: 'serious',
27
28
  tags: ['wcag2a', 'wcag412'],
28
29
  help: 'ARIA fields and value widgets must have an accessible name',
@@ -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
- selector: 'div[aria-label]:not([role]):not([tabindex]), span[aria-label]:not([role]):not([tabindex]), p[aria-label]:not([role]):not([tabindex])',
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: `aria-label="${element.getAttribute('aria-label')}" 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.`,
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.
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 4.1.2 Name, Role, Value (Level A)
2
2
  export default {
3
3
  id: 'button-name',
4
+ name: 'Button names',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag412'],
6
7
  help: 'Buttons must have an accessible name',
@@ -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',
@@ -8,6 +8,7 @@ import { labelledByName } from '../../lib/accessible-name.js';
8
8
 
9
9
  export default {
10
10
  id: 'dialog-name',
11
+ name: 'Dialog names',
11
12
  impact: 'serious',
12
13
  tags: ['wcag2a', 'wcag412'],
13
14
  help: 'Dialogs must have an accessible name',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 4.1.2 Name, Role, Value (Level A)
2
2
  export default {
3
3
  id: 'frame-title',
4
+ name: 'Frame titles',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag412'],
6
7
  help: 'Frames must have a title describing their content',
@@ -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',
@@ -8,6 +8,7 @@ const INTERACTIVE =
8
8
 
9
9
  export default {
10
10
  id: 'nested-interactive',
11
+ name: 'Nested controls',
11
12
  impact: 'serious',
12
13
  tags: ['wcag2a', 'wcag412'],
13
14
  help: 'A control must not contain another control',
@@ -11,6 +11,7 @@ const REQUIRED = {
11
11
 
12
12
  export default {
13
13
  id: 'role-required-aria',
14
+ name: 'Required role states',
14
15
  impact: 'serious',
15
16
  tags: ['wcag2a', 'wcag412'],
16
17
  help: 'Some roles require a state attribute to work',
@@ -11,6 +11,7 @@
11
11
  // label ("Details").
12
12
  export default {
13
13
  id: 'summary-name',
14
+ name: 'Disclosure names',
14
15
  impact: 'serious',
15
16
  tags: ['wcag2a', 'wcag412'],
16
17
  help: 'Disclosure controls (<summary>) need an accessible name',
@@ -24,6 +24,7 @@ const VALID_ROLES = new Set([
24
24
 
25
25
  export default {
26
26
  id: 'valid-role',
27
+ name: 'Valid roles',
27
28
  impact: 'critical',
28
29
  tags: ['wcag2a', 'wcag412'],
29
30
  help: 'role attributes must use valid ARIA roles',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",