@surea11y/core 1.2.0 → 1.3.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 (157) hide show
  1. package/CHANGELOG.md +37 -7
  2. package/LICENSE +373 -21
  3. package/README.md +67 -1
  4. package/bin/core.js +144 -19
  5. package/docs/API_STABILITY.md +1 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/CLI.md +54 -1
  9. package/docs/ENGINE_OPTIONS.md +2 -0
  10. package/docs/INTEGRATION.md +18 -0
  11. package/docs/OUTPUT_SCHEMA.md +1 -1
  12. package/docs/RULE_CATALOG.md +1 -1
  13. package/docs/SARIF.md +59 -0
  14. package/package.json +15 -4
  15. package/src/baseline.js +0 -0
  16. package/src/catalogs/composites.wcag.js +414 -450
  17. package/src/checks/automatic/area-alt-present.js +59 -25
  18. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  19. package/src/checks/automatic/aria-allowed-role.js +21 -7
  20. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  21. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  22. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  23. package/src/checks/automatic/aria-hidden-body.js +42 -19
  24. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  25. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  26. package/src/checks/automatic/aria-prohibited-children.js +55 -16
  27. package/src/checks/automatic/aria-required-attr.js +23 -8
  28. package/src/checks/automatic/aria-required-children.js +37 -14
  29. package/src/checks/automatic/aria-required-parent.js +48 -14
  30. package/src/checks/automatic/aria-role-name-present.js +47 -21
  31. package/src/checks/automatic/aria-roles-valid.js +22 -12
  32. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  33. package/src/checks/automatic/aria-valid-attr.js +17 -5
  34. package/src/checks/automatic/autocomplete-valid.js +74 -16
  35. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  36. package/src/checks/automatic/binary-control-name-present.js +60 -50
  37. package/src/checks/automatic/button-name-present.js +48 -18
  38. package/src/checks/automatic/bypass-blocks-present.js +42 -25
  39. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  40. package/src/checks/automatic/combobox-name-present.js +38 -45
  41. package/src/checks/automatic/contrast-computable.js +361 -341
  42. package/src/checks/automatic/contrast-enhanced.js +487 -466
  43. package/src/checks/automatic/contrast-minimum.js +486 -465
  44. package/src/checks/automatic/css-orientation-lock.js +30 -8
  45. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  46. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  47. package/src/checks/automatic/dialog-name-present.js +37 -75
  48. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  49. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  50. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  51. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  52. package/src/checks/automatic/form-control-single-label.js +47 -10
  53. package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
  54. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  55. package/src/checks/automatic/iframe-name-present.js +31 -9
  56. package/src/checks/automatic/iframe-title-unique.js +29 -8
  57. package/src/checks/automatic/img-alt-present.js +47 -43
  58. package/src/checks/automatic/input-image-alt-present.js +143 -112
  59. package/src/checks/automatic/label-in-name.js +50 -22
  60. package/src/checks/automatic/language-page-present.js +109 -109
  61. package/src/checks/automatic/link-in-text-block.js +59 -19
  62. package/src/checks/automatic/link-name-present.js +45 -14
  63. package/src/checks/automatic/list-children-valid.js +26 -9
  64. package/src/checks/automatic/listbox-name-present.js +39 -19
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  66. package/src/checks/automatic/menuitem-name-present.js +39 -61
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
  70. package/src/checks/automatic/meter-name-present.js +36 -33
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  72. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  73. package/src/checks/automatic/option-name-present.js +38 -21
  74. package/src/checks/automatic/page-title-present.js +17 -6
  75. package/src/checks/automatic/progressbar-name-present.js +41 -34
  76. package/src/checks/automatic/role-img-alt-present.js +209 -157
  77. package/src/checks/automatic/searchbox-name-present.js +39 -19
  78. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  79. package/src/checks/automatic/slider-name-present.js +40 -47
  80. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  81. package/src/checks/automatic/summary-name-present.js +37 -17
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  83. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  84. package/src/checks/automatic/tab-name-present.js +37 -60
  85. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  86. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  87. package/src/checks/automatic/target-size-minimum.js +118 -48
  88. package/src/checks/automatic/td-has-header.js +29 -11
  89. package/src/checks/automatic/textbox-name-present.js +39 -19
  90. package/src/checks/automatic/tooltip-name-present.js +37 -18
  91. package/src/checks/automatic/treeitem-name-present.js +38 -21
  92. package/src/checks/automatic/valid-lang.js +20 -6
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  94. package/src/checks/manual/accesskeys-manual.js +14 -5
  95. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  96. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  98. package/src/checks/manual/aria-text-manual.js +14 -6
  99. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  100. package/src/checks/manual/css-hidden-focus.js +196 -165
  101. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  102. package/src/checks/manual/empty-heading-manual.js +24 -7
  103. package/src/checks/manual/empty-table-header-manual.js +17 -6
  104. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  105. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  106. package/src/checks/manual/heading-order-manual.js +22 -7
  107. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  108. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  109. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  110. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  111. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  112. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  113. package/src/checks/manual/label-title-only-manual.js +14 -5
  114. package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
  115. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
  116. package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
  117. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
  118. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
  119. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  120. package/src/checks/manual/landmark-one-main-manual.js +26 -20
  121. package/src/checks/manual/landmark-unique-manual.js +41 -15
  122. package/src/checks/manual/link-name-quality-manual.js +43 -12
  123. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  124. package/src/checks/manual/meta-viewport-large-manual.js +16 -5
  125. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  126. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  127. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  128. package/src/checks/manual/p-as-heading-manual.js +22 -7
  129. package/src/checks/manual/page-has-heading-one-manual.js +31 -22
  130. package/src/checks/manual/page-title-patterns-manual.js +78 -50
  131. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  132. package/src/checks/manual/region-manual.js +241 -48
  133. package/src/checks/manual/scope-attr-valid-manual.js +10 -3
  134. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  135. package/src/checks/manual/skip-link-manual.js +35 -12
  136. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  137. package/src/checks/manual/tabindex-manual.js +10 -3
  138. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  139. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  140. package/src/checks/manual/video-caption-manual.js +15 -4
  141. package/src/checks/manual-review.js +56 -12
  142. package/src/core/aria-helpers.js +1127 -886
  143. package/src/core/contrast-helpers.js +1217 -1062
  144. package/src/core/dom-helpers.js +4175 -3917
  145. package/src/core/dom-runner.js +720 -604
  146. package/src/core/frame-messaging.js +189 -138
  147. package/src/core/frame-scan.js +94 -82
  148. package/src/core/rollup-composites.js +94 -102
  149. package/src/core/rule-meta.js +58 -41
  150. package/src/core.js +36552 -29111
  151. package/src/i18n/en.js +1194 -889
  152. package/src/i18n/fr.js +1136 -795
  153. package/src/policy/contracts.js +13 -13
  154. package/src/policy/resolvePolicy.js +48 -44
  155. package/src/report.js +63 -43
  156. package/src/sarif.js +175 -0
  157. package/surea11y.browser.js +36042 -0
@@ -11,20 +11,27 @@
11
11
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
12
  */
13
13
 
14
- const id = "input-image-alt-decorative";
14
+ const id = 'input-image-alt-decorative';
15
15
 
16
16
  const meta = {
17
- title: "<input type=\"image\"> with alt=\"\" must be appropriate (manual review)",
18
- description: "Flags <input type=\"image\"> elements with empty alt for human review (usually not appropriate for functional controls).",
17
+ title: '<input type="image"> with alt="" must be appropriate (manual review)',
18
+ description:
19
+ 'Flags <input type="image"> elements with empty alt for human review (usually not appropriate for functional controls).',
19
20
  i18n: {
20
- titleKey: "inputImage_altDecorative_title",
21
- descriptionKey: "inputImage_altDecorative_description"
21
+ titleKey: 'inputImage_altDecorative_title',
22
+ descriptionKey: 'inputImage_altDecorative_description'
22
23
  },
23
24
  helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "forms", "images", "manual", "atomic"],
25
+ tags: ['wcag2a', 'wcag111', 'forms', 'images', 'manual', 'atomic'],
25
26
  wcagSc: ['1.1.1'],
26
27
  normativeMappings: [
27
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
28
+ {
29
+ standard: 'WCAG',
30
+ version: '2.2',
31
+ requirement: '1.1.1',
32
+ title: 'Non-text Content',
33
+ conformanceLevel: 'A'
34
+ }
28
35
  ],
29
36
  defaultSeverity: 'minor',
30
37
  category: 'perceivable',
@@ -38,107 +45,133 @@ const meta = {
38
45
  };
39
46
 
40
47
  function runInPage(ctx) {
41
-
42
- const { document, root, helpers, rule } = ctx;
43
- const safeRoot = root || document;
44
-
45
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
46
- const queryAll = helpers && typeof helpers.queryAll === 'function'
47
- ? helpers.queryAll
48
- : (sel) => {
49
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
50
- catch { return []; }
48
+ const { document, root, helpers, rule } = ctx;
49
+ const safeRoot = root || document;
50
+
51
+ const queryAllSmart =
52
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
+ const queryAll =
54
+ helpers && typeof helpers.queryAll === 'function'
55
+ ? helpers.queryAll
56
+ : (sel) => {
57
+ try {
58
+ return safeRoot && safeRoot.querySelectorAll
59
+ ? Array.from(safeRoot.querySelectorAll(sel))
60
+ : [];
61
+ } catch {
62
+ return [];
63
+ }
51
64
  };
52
65
 
53
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
54
- ? helpers.getEligibilityInfo
55
- : null;
56
-
57
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
58
- ? helpers.isAccTreeEligible
59
- : null;
60
-
61
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
62
- ? helpers.getFocusableInfo
63
- : null;
64
-
65
- function isRolePresentationExcluded(el) {
66
- const role = (() => {
67
- try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
68
- catch { return ''; }
69
- })();
70
- if (role !== 'presentation' && role !== 'none') return false;
71
-
72
- // Exclude only when NOT focusable (mirrors img-alt-present policy)
73
- let focusable = false;
74
- if (getFocusableInfo) {
75
- const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
76
- focusable = !!(fi && fi.focusable);
77
- } else {
78
- const tabindex = el.getAttribute('tabindex');
79
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
80
- }
81
- return !focusable;
82
- }
83
-
84
-
85
- const els = (() => {
86
- try { return Array.from((queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) || []); }
87
- catch { return queryAll('input[type="image"]'); }
88
- })();
89
-
90
- if (!els.length) {
91
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
92
- }
66
+ const getEligibilityInfo =
67
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
93
68
 
94
- const occurrences = [];
95
- let applicableCount = 0;
69
+ const isAccTreeEligible =
70
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
96
71
 
97
- for (const el of els) {
98
- if (!el || !el.getAttribute) continue;
72
+ const getFocusableInfo =
73
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
99
74
 
100
- if (isAccTreeEligible) {
101
- const elig = (() => {
102
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
103
- })();
104
- if (elig && elig.eligible === false) continue;
75
+ function isRolePresentationExcluded(el) {
76
+ const role = (() => {
77
+ try {
78
+ return String(el.getAttribute('role') || '')
79
+ .trim()
80
+ .toLowerCase();
81
+ } catch {
82
+ return '';
83
+ }
84
+ })();
85
+ if (role !== 'presentation' && role !== 'none') return false;
86
+
87
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
88
+ let focusable;
89
+ if (getFocusableInfo) {
90
+ const fi = (() => {
91
+ try {
92
+ return getFocusableInfo(el, ctx);
93
+ } catch {
94
+ return null;
105
95
  }
96
+ })();
97
+ focusable = !!(fi && fi.focusable);
98
+ } else {
99
+ const tabindex = el.getAttribute('tabindex');
100
+ focusable =
101
+ tabindex != null &&
102
+ String(tabindex).trim() !== '' &&
103
+ !Number.isNaN(Number(String(tabindex).trim()));
104
+ }
105
+ return !focusable;
106
+ }
106
107
 
107
- if (isRolePresentationExcluded(el)) continue;
108
-
109
- // Rule-specific applicability (only elements that already have a text alternative mechanism)
110
- if (!((el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() === ''))) continue;
108
+ const els = (() => {
109
+ try {
110
+ return Array.from(
111
+ (queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) ||
112
+ []
113
+ );
114
+ } catch {
115
+ return queryAll('input[type="image"]');
116
+ }
117
+ })();
111
118
 
112
- applicableCount += 1;
119
+ if (!els.length) {
120
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
121
+ }
113
122
 
114
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
123
+ const occurrences = [];
124
+ let applicableCount = 0;
115
125
 
116
- const baseOccurrence = {
117
- summary: 'Review <input type="image"> with alt="".',
118
- hint: 'This control is typically functional. Confirm it has an equivalent accessible name elsewhere, or provide meaningful alt text.',
119
- i18n: {
120
- summaryKey: 'inputImage_altDecorative_summary_cantTell',
121
- hintKey: 'inputImage_altDecorative_hint_cantTell',
122
- params: { element: 'input[type=image]' }
123
- },
124
- data: {
125
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
126
- details: null
127
- }
128
- };
126
+ for (const el of els) {
127
+ if (!el || !el.getAttribute) continue;
129
128
 
130
- if (helpers && typeof helpers.reportOccurrence === 'function') {
131
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
132
- } else {
133
- occurrences.push({ selector: '', html: '', ...baseOccurrence });
129
+ if (isAccTreeEligible) {
130
+ const elig = (() => {
131
+ try {
132
+ return isAccTreeEligible(el, ctx);
133
+ } catch {
134
+ return { eligible: true, reasons: [] };
134
135
  }
136
+ })();
137
+ if (elig && elig.eligible === false) continue;
135
138
  }
136
139
 
137
- if (applicableCount === 0) {
138
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
140
+ if (isRolePresentationExcluded(el)) continue;
141
+
142
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
143
+ if (!(el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() === '')) continue;
144
+
145
+ applicableCount += 1;
146
+
147
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
148
+
149
+ const baseOccurrence = {
150
+ summary: 'Review <input type="image"> with alt="".',
151
+ hint: 'This control is typically functional. Confirm it has an equivalent accessible name elsewhere, or provide meaningful alt text.',
152
+ i18n: {
153
+ summaryKey: 'inputImage_altDecorative_summary_cantTell',
154
+ hintKey: 'inputImage_altDecorative_hint_cantTell',
155
+ params: { element: 'input[type=image]' }
156
+ },
157
+ data: {
158
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
159
+ details: null
160
+ }
161
+ };
162
+
163
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
164
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
165
+ } else {
166
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
139
167
  }
168
+ }
169
+
170
+ if (applicableCount === 0) {
171
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
172
+ }
140
173
 
141
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
174
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
142
175
  }
143
176
 
144
177
  module.exports = { id, meta, runInPage };
@@ -11,20 +11,27 @@
11
11
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
12
  */
13
13
 
14
- const id = "input-image-alt-quality";
14
+ const id = 'input-image-alt-quality';
15
15
 
16
16
  const meta = {
17
- title: "<input type=\"image\"> alt text must be appropriate (manual review)",
18
- description: "Flags <input type=\"image\"> elements with non-empty alt text for human review of appropriateness.",
17
+ title: '<input type="image"> alt text must be appropriate (manual review)',
18
+ description:
19
+ 'Flags <input type="image"> elements with non-empty alt text for human review of appropriateness.',
19
20
  i18n: {
20
- titleKey: "inputImage_altQuality_title",
21
- descriptionKey: "inputImage_altQuality_description"
21
+ titleKey: 'inputImage_altQuality_title',
22
+ descriptionKey: 'inputImage_altQuality_description'
22
23
  },
23
24
  helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "forms", "images", "manual", "atomic"],
25
+ tags: ['wcag2a', 'wcag111', 'forms', 'images', 'manual', 'atomic'],
25
26
  wcagSc: ['1.1.1'],
26
27
  normativeMappings: [
27
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
28
+ {
29
+ standard: 'WCAG',
30
+ version: '2.2',
31
+ requirement: '1.1.1',
32
+ title: 'Non-text Content',
33
+ conformanceLevel: 'A'
34
+ }
28
35
  ],
29
36
  defaultSeverity: 'minor',
30
37
  category: 'perceivable',
@@ -38,107 +45,133 @@ const meta = {
38
45
  };
39
46
 
40
47
  function runInPage(ctx) {
41
-
42
- const { document, root, helpers, rule } = ctx;
43
- const safeRoot = root || document;
44
-
45
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
46
- const queryAll = helpers && typeof helpers.queryAll === 'function'
47
- ? helpers.queryAll
48
- : (sel) => {
49
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
50
- catch { return []; }
48
+ const { document, root, helpers, rule } = ctx;
49
+ const safeRoot = root || document;
50
+
51
+ const queryAllSmart =
52
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
+ const queryAll =
54
+ helpers && typeof helpers.queryAll === 'function'
55
+ ? helpers.queryAll
56
+ : (sel) => {
57
+ try {
58
+ return safeRoot && safeRoot.querySelectorAll
59
+ ? Array.from(safeRoot.querySelectorAll(sel))
60
+ : [];
61
+ } catch {
62
+ return [];
63
+ }
51
64
  };
52
65
 
53
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
54
- ? helpers.getEligibilityInfo
55
- : null;
56
-
57
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
58
- ? helpers.isAccTreeEligible
59
- : null;
60
-
61
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
62
- ? helpers.getFocusableInfo
63
- : null;
64
-
65
- function isRolePresentationExcluded(el) {
66
- const role = (() => {
67
- try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
68
- catch { return ''; }
69
- })();
70
- if (role !== 'presentation' && role !== 'none') return false;
71
-
72
- // Exclude only when NOT focusable (mirrors img-alt-present policy)
73
- let focusable = false;
74
- if (getFocusableInfo) {
75
- const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
76
- focusable = !!(fi && fi.focusable);
77
- } else {
78
- const tabindex = el.getAttribute('tabindex');
79
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
80
- }
81
- return !focusable;
82
- }
83
-
84
-
85
- const els = (() => {
86
- try { return Array.from((queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) || []); }
87
- catch { return queryAll('input[type="image"]'); }
88
- })();
89
-
90
- if (!els.length) {
91
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
92
- }
66
+ const getEligibilityInfo =
67
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
93
68
 
94
- const occurrences = [];
95
- let applicableCount = 0;
69
+ const isAccTreeEligible =
70
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
96
71
 
97
- for (const el of els) {
98
- if (!el || !el.getAttribute) continue;
72
+ const getFocusableInfo =
73
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
99
74
 
100
- if (isAccTreeEligible) {
101
- const elig = (() => {
102
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
103
- })();
104
- if (elig && elig.eligible === false) continue;
75
+ function isRolePresentationExcluded(el) {
76
+ const role = (() => {
77
+ try {
78
+ return String(el.getAttribute('role') || '')
79
+ .trim()
80
+ .toLowerCase();
81
+ } catch {
82
+ return '';
83
+ }
84
+ })();
85
+ if (role !== 'presentation' && role !== 'none') return false;
86
+
87
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
88
+ let focusable;
89
+ if (getFocusableInfo) {
90
+ const fi = (() => {
91
+ try {
92
+ return getFocusableInfo(el, ctx);
93
+ } catch {
94
+ return null;
105
95
  }
96
+ })();
97
+ focusable = !!(fi && fi.focusable);
98
+ } else {
99
+ const tabindex = el.getAttribute('tabindex');
100
+ focusable =
101
+ tabindex != null &&
102
+ String(tabindex).trim() !== '' &&
103
+ !Number.isNaN(Number(String(tabindex).trim()));
104
+ }
105
+ return !focusable;
106
+ }
106
107
 
107
- if (isRolePresentationExcluded(el)) continue;
108
-
109
- // Rule-specific applicability (only elements that already have a text alternative mechanism)
110
- if (!((el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() !== ''))) continue;
108
+ const els = (() => {
109
+ try {
110
+ return Array.from(
111
+ (queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) ||
112
+ []
113
+ );
114
+ } catch {
115
+ return queryAll('input[type="image"]');
116
+ }
117
+ })();
111
118
 
112
- applicableCount += 1;
119
+ if (!els.length) {
120
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
121
+ }
113
122
 
114
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
123
+ const occurrences = [];
124
+ let applicableCount = 0;
115
125
 
116
- const baseOccurrence = {
117
- summary: 'Review alt text on <input type="image"> for accuracy and appropriateness.',
118
- hint: 'Ensure the alt text describes the control’s action (e.g., “Search”, “Submit order”) in context.',
119
- i18n: {
120
- summaryKey: 'inputImage_altQuality_summary_cantTell',
121
- hintKey: 'inputImage_altQuality_hint_cantTell',
122
- params: { element: 'input[type=image]' }
123
- },
124
- data: {
125
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
126
- details: { alt: String(el.getAttribute('alt') || '') } // optional but useful
127
- }
128
- };
126
+ for (const el of els) {
127
+ if (!el || !el.getAttribute) continue;
129
128
 
130
- if (helpers && typeof helpers.reportOccurrence === 'function') {
131
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
132
- } else {
133
- occurrences.push({ selector: '', html: '', ...baseOccurrence });
129
+ if (isAccTreeEligible) {
130
+ const elig = (() => {
131
+ try {
132
+ return isAccTreeEligible(el, ctx);
133
+ } catch {
134
+ return { eligible: true, reasons: [] };
134
135
  }
136
+ })();
137
+ if (elig && elig.eligible === false) continue;
135
138
  }
136
139
 
137
- if (applicableCount === 0) {
138
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
140
+ if (isRolePresentationExcluded(el)) continue;
141
+
142
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
143
+ if (!(el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() !== '')) continue;
144
+
145
+ applicableCount += 1;
146
+
147
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
148
+
149
+ const baseOccurrence = {
150
+ summary: 'Review alt text on <input type="image"> for accuracy and appropriateness.',
151
+ hint: 'Ensure the alt text describes the control’s action (e.g., “Search”, “Submit order”) in context.',
152
+ i18n: {
153
+ summaryKey: 'inputImage_altQuality_summary_cantTell',
154
+ hintKey: 'inputImage_altQuality_hint_cantTell',
155
+ params: { element: 'input[type=image]' }
156
+ },
157
+ data: {
158
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
159
+ details: { alt: String(el.getAttribute('alt') || '') } // optional but useful
160
+ }
161
+ };
162
+
163
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
164
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
165
+ } else {
166
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
139
167
  }
168
+ }
169
+
170
+ if (applicableCount === 0) {
171
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
172
+ }
140
173
 
141
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
174
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
142
175
  }
143
176
 
144
177
  module.exports = { id, meta, runInPage };
@@ -28,7 +28,8 @@ const id = 'label-title-only';
28
28
 
29
29
  const meta = {
30
30
  title: 'Form controls should not use title as their only label',
31
- description: 'Checks that a form control with a title attribute also has a real label (label element, aria-label, or aria-labelledby).',
31
+ description:
32
+ 'Checks that a form control with a title attribute also has a real label (label element, aria-label, or aria-labelledby).',
32
33
  i18n: {
33
34
  titleKey: 'labelTitleOnly_title',
34
35
  descriptionKey: 'labelTitleOnly_description'
@@ -47,8 +48,11 @@ const meta = {
47
48
  function runInPage(ctx) {
48
49
  const { document, helpers, rule } = ctx;
49
50
 
50
- const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
51
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
51
+ const selector =
52
+ 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
53
+ const nodes = helpers.queryAllSmart
54
+ ? helpers.queryAllSmart(selector)
55
+ : helpers.queryAll(selector);
52
56
 
53
57
  const labelsByFor = new Map();
54
58
  const allLabels = document.getElementsByTagName ? document.getElementsByTagName('label') : [];
@@ -84,7 +88,7 @@ function runInPage(ctx) {
84
88
 
85
89
  const tag = el.tagName.toLowerCase();
86
90
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
87
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
91
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
88
92
 
89
93
  occurrences.push({
90
94
  selector: stableSelector,
@@ -106,7 +110,12 @@ function runInPage(ctx) {
106
110
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
107
111
  }
108
112
  if (occurrences.length) {
109
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
113
+ return {
114
+ ruleId: rule.ruleId,
115
+ outcome: 'cantTell',
116
+ severity: rule.defaultSeverity || 'minor',
117
+ occurrences
118
+ };
110
119
  }
111
120
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
112
121
  }