@surea11y/core 1.1.2 → 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 (160) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/LICENSE +373 -21
  3. package/README.md +70 -1
  4. package/bin/core.js +240 -11
  5. package/docs/API_STABILITY.md +61 -0
  6. package/docs/BASELINE.md +66 -0
  7. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  8. package/docs/CI_INTEGRATIONS.md +103 -0
  9. package/docs/CLI.md +80 -1
  10. package/docs/ENGINE_OPTIONS.md +4 -0
  11. package/docs/INTEGRATION.md +19 -1
  12. package/docs/OUTPUT_SCHEMA.md +2 -2
  13. package/docs/REPORT.md +33 -0
  14. package/docs/RULE_AUTHORING.md +31 -0
  15. package/docs/RULE_CATALOG.md +1 -1
  16. package/docs/SARIF.md +59 -0
  17. package/package.json +15 -4
  18. package/src/baseline.js +0 -0
  19. package/src/catalogs/composites.wcag.js +414 -450
  20. package/src/checks/automatic/area-alt-present.js +59 -25
  21. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  22. package/src/checks/automatic/aria-allowed-role.js +21 -7
  23. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  24. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  25. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  26. package/src/checks/automatic/aria-hidden-body.js +50 -19
  27. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  28. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  29. package/src/checks/automatic/aria-prohibited-children.js +125 -29
  30. package/src/checks/automatic/aria-required-attr.js +23 -8
  31. package/src/checks/automatic/aria-required-children.js +37 -14
  32. package/src/checks/automatic/aria-required-parent.js +48 -14
  33. package/src/checks/automatic/aria-role-name-present.js +47 -21
  34. package/src/checks/automatic/aria-roles-valid.js +22 -12
  35. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  36. package/src/checks/automatic/aria-valid-attr.js +17 -5
  37. package/src/checks/automatic/autocomplete-valid.js +74 -16
  38. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  39. package/src/checks/automatic/binary-control-name-present.js +60 -50
  40. package/src/checks/automatic/button-name-present.js +48 -18
  41. package/src/checks/automatic/bypass-blocks-present.js +50 -25
  42. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  43. package/src/checks/automatic/combobox-name-present.js +38 -45
  44. package/src/checks/automatic/contrast-computable.js +361 -341
  45. package/src/checks/automatic/contrast-enhanced.js +487 -466
  46. package/src/checks/automatic/contrast-minimum.js +486 -465
  47. package/src/checks/automatic/css-orientation-lock.js +39 -9
  48. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  49. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  50. package/src/checks/automatic/dialog-name-present.js +37 -75
  51. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  52. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  53. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  55. package/src/checks/automatic/form-control-single-label.js +47 -10
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
  57. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  58. package/src/checks/automatic/iframe-name-present.js +31 -9
  59. package/src/checks/automatic/iframe-title-unique.js +29 -8
  60. package/src/checks/automatic/img-alt-present.js +47 -43
  61. package/src/checks/automatic/input-image-alt-present.js +143 -112
  62. package/src/checks/automatic/label-in-name.js +50 -22
  63. package/src/checks/automatic/language-page-present.js +117 -109
  64. package/src/checks/automatic/link-in-text-block.js +59 -19
  65. package/src/checks/automatic/link-name-present.js +45 -14
  66. package/src/checks/automatic/list-children-valid.js +26 -9
  67. package/src/checks/automatic/listbox-name-present.js +39 -19
  68. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  69. package/src/checks/automatic/menuitem-name-present.js +39 -61
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
  73. package/src/checks/automatic/meter-name-present.js +36 -33
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  75. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  76. package/src/checks/automatic/option-name-present.js +38 -21
  77. package/src/checks/automatic/page-title-present.js +26 -7
  78. package/src/checks/automatic/progressbar-name-present.js +41 -34
  79. package/src/checks/automatic/role-img-alt-present.js +209 -157
  80. package/src/checks/automatic/searchbox-name-present.js +39 -19
  81. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  82. package/src/checks/automatic/slider-name-present.js +40 -47
  83. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  84. package/src/checks/automatic/summary-name-present.js +37 -17
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  86. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  87. package/src/checks/automatic/tab-name-present.js +37 -60
  88. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  89. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  90. package/src/checks/automatic/target-size-minimum.js +118 -48
  91. package/src/checks/automatic/td-has-header.js +29 -11
  92. package/src/checks/automatic/textbox-name-present.js +39 -19
  93. package/src/checks/automatic/tooltip-name-present.js +37 -18
  94. package/src/checks/automatic/treeitem-name-present.js +38 -21
  95. package/src/checks/automatic/valid-lang.js +20 -6
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  97. package/src/checks/manual/accesskeys-manual.js +14 -5
  98. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  99. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  101. package/src/checks/manual/aria-text-manual.js +14 -6
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  103. package/src/checks/manual/css-hidden-focus.js +196 -165
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  105. package/src/checks/manual/empty-heading-manual.js +24 -12
  106. package/src/checks/manual/empty-table-header-manual.js +21 -14
  107. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  109. package/src/checks/manual/heading-order-manual.js +22 -12
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  112. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  113. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  115. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  116. package/src/checks/manual/label-title-only-manual.js +14 -5
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  123. package/src/checks/manual/landmark-one-main-manual.js +35 -21
  124. package/src/checks/manual/landmark-unique-manual.js +73 -45
  125. package/src/checks/manual/link-name-quality-manual.js +43 -12
  126. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  127. package/src/checks/manual/meta-viewport-large-manual.js +25 -6
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  129. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  131. package/src/checks/manual/p-as-heading-manual.js +22 -7
  132. package/src/checks/manual/page-has-heading-one-manual.js +40 -23
  133. package/src/checks/manual/page-title-patterns-manual.js +87 -51
  134. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  135. package/src/checks/manual/region-manual.js +275 -62
  136. package/src/checks/manual/scope-attr-valid-manual.js +11 -9
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  138. package/src/checks/manual/skip-link-manual.js +35 -12
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  140. package/src/checks/manual/tabindex-manual.js +11 -9
  141. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  142. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  143. package/src/checks/manual/video-caption-manual.js +15 -4
  144. package/src/checks/manual-review.js +56 -12
  145. package/src/core/aria-helpers.js +1128 -823
  146. package/src/core/contrast-helpers.js +1217 -1062
  147. package/src/core/dom-helpers.js +4176 -3886
  148. package/src/core/dom-runner.js +720 -596
  149. package/src/core/frame-messaging.js +189 -138
  150. package/src/core/frame-scan.js +94 -82
  151. package/src/core/rollup-composites.js +94 -102
  152. package/src/core/rule-meta.js +71 -35
  153. package/src/core.js +37939 -29121
  154. package/src/i18n/en.js +1194 -887
  155. package/src/i18n/fr.js +1136 -793
  156. package/src/policy/contracts.js +13 -13
  157. package/src/policy/resolvePolicy.js +48 -44
  158. package/src/report.js +502 -0
  159. package/src/sarif.js +175 -0
  160. 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
  }