@surea11y/core 1.4.1 → 1.5.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 (104) hide show
  1. package/CHANGELOG.md +47 -7
  2. package/README.md +19 -3
  3. package/docs/API_STABILITY.md +2 -2
  4. package/docs/ENGINE_OPTIONS.md +14 -10
  5. package/docs/I18N.md +176 -20
  6. package/docs/INTEGRATION.md +28 -6
  7. package/docs/LIMITATIONS.md +1 -1
  8. package/docs/OUTPUT_SCHEMA.md +13 -3
  9. package/docs/REPORT.md +2 -0
  10. package/docs/RULE_AUTHORING.md +53 -0
  11. package/docs/TROUBLESHOOTING.md +2 -2
  12. package/package.json +6 -1
  13. package/src/checks/automatic/aria-allowed-attr.js +27 -30
  14. package/src/checks/automatic/aria-allowed-role.js +14 -16
  15. package/src/checks/automatic/aria-braille-equivalent.js +17 -19
  16. package/src/checks/automatic/aria-conditional-attr.js +17 -19
  17. package/src/checks/automatic/aria-deprecated-role.js +62 -49
  18. package/src/checks/automatic/aria-hidden-body.js +2 -9
  19. package/src/checks/automatic/aria-hidden-focus.js +99 -18
  20. package/src/checks/automatic/aria-prohibited-attr.js +54 -55
  21. package/src/checks/automatic/aria-prohibited-children.js +26 -26
  22. package/src/checks/automatic/aria-required-attr.js +14 -17
  23. package/src/checks/automatic/aria-required-children.js +17 -20
  24. package/src/checks/automatic/aria-required-parent.js +17 -20
  25. package/src/checks/automatic/aria-roles-valid.js +37 -23
  26. package/src/checks/automatic/aria-valid-attr-value.js +18 -21
  27. package/src/checks/automatic/aria-valid-attr.js +14 -17
  28. package/src/checks/automatic/autocomplete-valid.js +15 -17
  29. package/src/checks/automatic/avoid-inline-spacing.js +14 -16
  30. package/src/checks/automatic/binary-control-name-present.js +19 -21
  31. package/src/checks/automatic/button-name-present.js +23 -28
  32. package/src/checks/automatic/combobox-name-present.js +15 -17
  33. package/src/checks/automatic/css-orientation-lock.js +22 -22
  34. package/src/checks/automatic/definition-list-children-valid.js +18 -21
  35. package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
  36. package/src/checks/automatic/dialog-name-present.js +16 -18
  37. package/src/checks/automatic/dlitem-parent-valid.js +15 -17
  38. package/src/checks/automatic/duplicate-id-aria.js +45 -37
  39. package/src/checks/automatic/form-control-single-label.js +38 -40
  40. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
  41. package/src/checks/automatic/iframe-focusable-content.js +29 -31
  42. package/src/checks/automatic/iframe-name-present.js +15 -17
  43. package/src/checks/automatic/iframe-title-unique.js +18 -23
  44. package/src/checks/automatic/label-in-name.js +31 -36
  45. package/src/checks/automatic/link-in-text-block.js +19 -21
  46. package/src/checks/automatic/link-name-present.js +25 -30
  47. package/src/checks/automatic/list-children-valid.js +15 -17
  48. package/src/checks/automatic/listbox-name-present.js +15 -17
  49. package/src/checks/automatic/listitem-parent-valid.js +14 -17
  50. package/src/checks/automatic/menuitem-name-present.js +16 -18
  51. package/src/checks/automatic/meta-refresh-no-exceptions.js +15 -18
  52. package/src/checks/automatic/meta-refresh-timing-absent.js +14 -17
  53. package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
  54. package/src/checks/automatic/meter-name-present.js +16 -18
  55. package/src/checks/automatic/nested-interactive-controls-absent.js +17 -19
  56. package/src/checks/automatic/option-name-present.js +16 -18
  57. package/src/checks/automatic/progressbar-name-present.js +19 -21
  58. package/src/checks/automatic/searchbox-name-present.js +19 -17
  59. package/src/checks/automatic/server-side-image-map-absent.js +15 -18
  60. package/src/checks/automatic/slider-name-present.js +15 -17
  61. package/src/checks/automatic/spinbutton-name-present.js +19 -17
  62. package/src/checks/automatic/summary-name-present.js +16 -18
  63. package/src/checks/automatic/tab-name-present.js +16 -18
  64. package/src/checks/automatic/table-headers-attr-valid.js +15 -17
  65. package/src/checks/automatic/table-th-has-data-cells.js +15 -19
  66. package/src/checks/automatic/target-size-minimum.js +104 -81
  67. package/src/checks/automatic/td-has-header.js +15 -20
  68. package/src/checks/automatic/textbox-name-present.js +15 -17
  69. package/src/checks/automatic/tooltip-name-present.js +16 -18
  70. package/src/checks/automatic/treeitem-name-present.js +16 -18
  71. package/src/checks/automatic/valid-lang.js +15 -17
  72. package/src/checks/manual/accesskeys-manual.js +18 -19
  73. package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
  74. package/src/checks/manual/bypass-blocks-present-manual.js +4 -12
  75. package/src/checks/manual/empty-heading-manual.js +15 -17
  76. package/src/checks/manual/empty-table-header-manual.js +27 -30
  77. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
  78. package/src/checks/manual/heading-order-manual.js +17 -22
  79. package/src/checks/manual/image-redundant-alt-manual.js +14 -17
  80. package/src/checks/manual/label-title-only-manual.js +15 -17
  81. package/src/checks/manual/landmark-banner-is-top-level-manual.js +14 -17
  82. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +14 -17
  83. package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
  84. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
  85. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
  86. package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
  87. package/src/checks/manual/landmark-one-main-manual.js +2 -9
  88. package/src/checks/manual/landmark-unique-manual.js +22 -27
  89. package/src/checks/manual/link-name-quality-manual.js +15 -17
  90. package/src/checks/manual/meta-viewport-large-manual.js +14 -17
  91. package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
  92. package/src/checks/manual/page-has-heading-one-manual.js +2 -9
  93. package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
  94. package/src/checks/manual/region-manual.js +13 -6
  95. package/src/checks/manual/scope-attr-valid-manual.js +14 -17
  96. package/src/checks/manual/skip-link-manual.js +39 -47
  97. package/src/checks/manual/tabindex-manual.js +14 -17
  98. package/src/checks/manual/table-duplicate-name-manual.js +14 -17
  99. package/src/core.js +4203 -3604
  100. package/src/report.js +14 -0
  101. package/surea11y.browser.js +1960 -3671
  102. package/surea11y.i18n.de.js +22 -0
  103. package/surea11y.i18n.es.js +22 -0
  104. package/surea11y.i18n.fr.js +22 -0
@@ -150,24 +150,22 @@ function runInPage(ctx) {
150
150
  }
151
151
  })()
152
152
  : null;
153
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
154
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
155
-
156
- occurrences.push({
157
- selector: stableSelector,
158
- html,
159
- summary: 'This menu item has no accessible name.',
160
- hint: 'Provide visible text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
161
- i18n: {
162
- summaryKey: 'menuitemNamePresent_summary_fail',
163
- hintKey: 'menuitemNamePresent_hint_fail',
164
- params: { role }
165
- },
166
- data: {
167
- details: { reasonCode: 'name_missing', controlType: role, methodTried: res.method },
168
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
169
- }
170
- });
153
+
154
+ occurrences.push(
155
+ helpers.reportOccurrence(el, {
156
+ summary: 'This menu item has no accessible name.',
157
+ hint: 'Provide visible text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
158
+ i18n: {
159
+ summaryKey: 'menuitemNamePresent_summary_fail',
160
+ hintKey: 'menuitemNamePresent_hint_fail',
161
+ params: { role }
162
+ },
163
+ data: {
164
+ details: { reasonCode: 'name_missing', controlType: role, methodTried: res.method },
165
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
166
+ }
167
+ })
168
+ );
171
169
  }
172
170
 
173
171
  if (applicableCount === 0) {
@@ -126,24 +126,21 @@ function runInPage(ctx) {
126
126
 
127
127
  applicableCount += 1;
128
128
 
129
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
130
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
131
-
132
- occurrences.push({
133
- selector: stableSelector,
134
- html,
135
- summary:
136
- 'This page uses a meta refresh, which is an automatic context change not initiated by the user.',
137
- hint: 'Remove the meta refresh; trigger the redirect/refresh only in response to a user action instead.',
138
- i18n: {
139
- summaryKey: 'metaRefreshNoExceptions_summary_fail',
140
- hintKey: 'metaRefreshNoExceptions_hint_fail',
141
- params: {}
142
- },
143
- data: {
144
- details: { reasonCode: 'META_REFRESH_PRESENT' }
145
- }
146
- });
129
+ occurrences.push(
130
+ helpers.reportOccurrence(el, {
131
+ summary:
132
+ 'This page uses a meta refresh, which is an automatic context change not initiated by the user.',
133
+ hint: 'Remove the meta refresh; trigger the redirect/refresh only in response to a user action instead.',
134
+ i18n: {
135
+ summaryKey: 'metaRefreshNoExceptions_summary_fail',
136
+ hintKey: 'metaRefreshNoExceptions_hint_fail',
137
+ params: {}
138
+ },
139
+ data: {
140
+ details: { reasonCode: 'META_REFRESH_PRESENT' }
141
+ }
142
+ })
143
+ );
147
144
  }
148
145
 
149
146
  if (applicableCount === 0) {
@@ -121,23 +121,20 @@ function runInPage(ctx) {
121
121
  if (!(delay > 0)) continue;
122
122
  if (delay > EXEMPT_DELAY_SECONDS) continue; // WCAG 2.2.1 Exception 3 (>20 hours)
123
123
 
124
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
125
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
126
-
127
- occurrences.push({
128
- selector: stableSelector,
129
- html,
130
- summary: 'This page refreshes itself automatically after a delay.',
131
- hint: 'Remove the timed meta refresh, or provide a way for users to turn it off, extend it, or pause it before it triggers.',
132
- i18n: {
133
- summaryKey: 'metaRefreshTimingAbsent_summary_fail',
134
- hintKey: 'metaRefreshTimingAbsent_hint_fail',
135
- params: { delay: String(delay) }
136
- },
137
- data: {
138
- details: { reasonCode: 'META_REFRESH_DELAYED', delay }
139
- }
140
- });
124
+ occurrences.push(
125
+ helpers.reportOccurrence(el, {
126
+ summary: 'This page refreshes itself automatically after a delay.',
127
+ hint: 'Remove the timed meta refresh, or provide a way for users to turn it off, extend it, or pause it before it triggers.',
128
+ i18n: {
129
+ summaryKey: 'metaRefreshTimingAbsent_summary_fail',
130
+ hintKey: 'metaRefreshTimingAbsent_hint_fail',
131
+ params: { delay: String(delay) }
132
+ },
133
+ data: {
134
+ details: { reasonCode: 'META_REFRESH_DELAYED', delay }
135
+ }
136
+ })
137
+ );
141
138
  }
142
139
 
143
140
  if (applicableCount === 0) {
@@ -131,23 +131,20 @@ function runInPage(ctx) {
131
131
 
132
132
  if (!reasons.length) continue;
133
133
 
134
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
135
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
136
-
137
- occurrences.push({
138
- selector: stableSelector,
139
- html,
140
- summary: 'This viewport meta tag restricts the user’s ability to zoom.',
141
- hint: 'Remove user-scalable=no and any maximum-scale below 2 from the viewport meta content.',
142
- i18n: {
143
- summaryKey: 'metaViewportZoomEnabled_summary_fail',
144
- hintKey: 'metaViewportZoomEnabled_hint_fail',
145
- params: { reasons: reasons.join(', ') }
146
- },
147
- data: {
148
- details: { reasonCode: 'VIEWPORT_ZOOM_RESTRICTED', reasons }
149
- }
150
- });
134
+ occurrences.push(
135
+ helpers.reportOccurrence(el, {
136
+ summary: 'This viewport meta tag restricts the user’s ability to zoom.',
137
+ hint: 'Remove user-scalable=no and any maximum-scale below 2 from the viewport meta content.',
138
+ i18n: {
139
+ summaryKey: 'metaViewportZoomEnabled_summary_fail',
140
+ hintKey: 'metaViewportZoomEnabled_hint_fail',
141
+ params: { reasons: reasons.join(', ') }
142
+ },
143
+ data: {
144
+ details: { reasonCode: 'VIEWPORT_ZOOM_RESTRICTED', reasons }
145
+ }
146
+ })
147
+ );
151
148
  }
152
149
 
153
150
  if (applicableCount === 0) {
@@ -133,24 +133,22 @@ function runInPage(ctx) {
133
133
  }
134
134
  })()
135
135
  : null;
136
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
137
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
138
-
139
- occurrences.push({
140
- selector: stableSelector,
141
- html,
142
- summary: 'This meter has no accessible name.',
143
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this meter's accessible name.",
144
- i18n: {
145
- summaryKey: 'meterNamePresent_summary_fail',
146
- hintKey: 'meterNamePresent_hint_fail',
147
- params: {}
148
- },
149
- data: {
150
- details: { reasonCode: 'name_missing', controlType: 'meter', methodTried: res.method },
151
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
152
- }
153
- });
136
+
137
+ occurrences.push(
138
+ helpers.reportOccurrence(el, {
139
+ summary: 'This meter has no accessible name.',
140
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this meter's accessible name.",
141
+ i18n: {
142
+ summaryKey: 'meterNamePresent_summary_fail',
143
+ hintKey: 'meterNamePresent_hint_fail',
144
+ params: {}
145
+ },
146
+ data: {
147
+ details: { reasonCode: 'name_missing', controlType: 'meter', methodTried: res.method },
148
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
149
+ }
150
+ })
151
+ );
154
152
  }
155
153
 
156
154
  if (applicableCount === 0) {
@@ -267,27 +267,25 @@ function runInPage(ctx) {
267
267
  const dedupedNestedTags = [...new Set(nestedTags)];
268
268
 
269
269
  const tag = el.tagName.toLowerCase();
270
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
271
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
272
270
 
273
- occurrences.push({
274
- selector: stableSelector,
275
- html,
276
- summary: 'This interactive control contains one or more other interactive controls.',
277
- hint: 'Move the nested interactive control(s) outside this element; nested interactive controls are not reliably operable via assistive technology.',
278
- i18n: {
279
- summaryKey: 'nestedInteractiveControlsAbsent_summary_fail',
280
- hintKey: 'nestedInteractiveControlsAbsent_hint_fail',
281
- params: { element: tag, nestedElements: dedupedNestedTags.join(', ') }
282
- },
283
- data: {
284
- details: {
285
- reasonCode: 'NESTED_INTERACTIVE_CONTROL',
286
- element: tag,
287
- nestedElements: nestedTags
271
+ occurrences.push(
272
+ helpers.reportOccurrence(el, {
273
+ summary: 'This interactive control contains one or more other interactive controls.',
274
+ hint: 'Move the nested interactive control(s) outside this element; nested interactive controls are not reliably operable via assistive technology.',
275
+ i18n: {
276
+ summaryKey: 'nestedInteractiveControlsAbsent_summary_fail',
277
+ hintKey: 'nestedInteractiveControlsAbsent_hint_fail',
278
+ params: { element: tag, nestedElements: dedupedNestedTags.join(', ') }
279
+ },
280
+ data: {
281
+ details: {
282
+ reasonCode: 'NESTED_INTERACTIVE_CONTROL',
283
+ element: tag,
284
+ nestedElements: nestedTags
285
+ }
288
286
  }
289
- }
290
- });
287
+ })
288
+ );
291
289
  }
292
290
 
293
291
  if (applicableCount === 0) {
@@ -145,24 +145,22 @@ function runInPage(ctx) {
145
145
  }
146
146
  })()
147
147
  : null;
148
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
149
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
150
-
151
- occurrences.push({
152
- selector: stableSelector,
153
- html,
154
- summary: 'This element has no accessible name.',
155
- hint: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
156
- i18n: {
157
- summaryKey: 'optionNamePresent_summary_fail',
158
- hintKey: 'optionNamePresent_hint_fail',
159
- params: { controlType: 'option' }
160
- },
161
- data: {
162
- details: { reasonCode: 'name_missing', controlType: 'option', methodTried: res.method },
163
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
164
- }
165
- });
148
+
149
+ occurrences.push(
150
+ helpers.reportOccurrence(el, {
151
+ summary: 'This element has no accessible name.',
152
+ hint: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
153
+ i18n: {
154
+ summaryKey: 'optionNamePresent_summary_fail',
155
+ hintKey: 'optionNamePresent_hint_fail',
156
+ params: { controlType: 'option' }
157
+ },
158
+ data: {
159
+ details: { reasonCode: 'name_missing', controlType: 'option', methodTried: res.method },
160
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
161
+ }
162
+ })
163
+ );
166
164
  }
167
165
 
168
166
  if (applicableCount === 0) {
@@ -135,28 +135,26 @@ function runInPage(ctx) {
135
135
  }
136
136
  })()
137
137
  : null;
138
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
139
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
140
-
141
- occurrences.push({
142
- selector: stableSelector,
143
- html,
144
- summary: 'This progress bar has no accessible name.',
145
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this progress bar's accessible name.",
146
- i18n: {
147
- summaryKey: 'progressbarNamePresent_summary_fail',
148
- hintKey: 'progressbarNamePresent_hint_fail',
149
- params: {}
150
- },
151
- data: {
152
- details: {
153
- reasonCode: 'name_missing',
154
- controlType: 'progressbar',
155
- methodTried: res.method
138
+
139
+ occurrences.push(
140
+ helpers.reportOccurrence(el, {
141
+ summary: 'This progress bar has no accessible name.',
142
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this progress bar's accessible name.",
143
+ i18n: {
144
+ summaryKey: 'progressbarNamePresent_summary_fail',
145
+ hintKey: 'progressbarNamePresent_hint_fail',
146
+ params: {}
156
147
  },
157
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
158
- }
159
- });
148
+ data: {
149
+ details: {
150
+ reasonCode: 'name_missing',
151
+ controlType: 'progressbar',
152
+ methodTried: res.method
153
+ },
154
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
155
+ }
156
+ })
157
+ );
160
158
  }
161
159
 
162
160
  if (applicableCount === 0) {
@@ -227,24 +227,26 @@ function runInPage(ctx) {
227
227
  }
228
228
  })()
229
229
  : null;
230
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
231
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
232
230
 
233
- occurrences.push({
234
- selector: stableSelector,
235
- html,
236
- summary: 'This element has no accessible name.',
237
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this searchbox's accessible name.",
238
- i18n: {
239
- summaryKey: 'searchboxNamePresent_summary_fail',
240
- hintKey: 'searchboxNamePresent_hint_fail',
241
- params: { controlType: 'searchbox' }
242
- },
243
- data: {
244
- details: { reasonCode: 'name_missing', controlType: 'searchbox', methodTried: res.method },
245
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
246
- }
247
- });
231
+ occurrences.push(
232
+ helpers.reportOccurrence(el, {
233
+ summary: 'This element has no accessible name.',
234
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this searchbox's accessible name.",
235
+ i18n: {
236
+ summaryKey: 'searchboxNamePresent_summary_fail',
237
+ hintKey: 'searchboxNamePresent_hint_fail',
238
+ params: { controlType: 'searchbox' }
239
+ },
240
+ data: {
241
+ details: {
242
+ reasonCode: 'name_missing',
243
+ controlType: 'searchbox',
244
+ methodTried: res.method
245
+ },
246
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
247
+ }
248
+ })
249
+ );
248
250
  }
249
251
 
250
252
  if (applicableCount === 0) {
@@ -66,24 +66,21 @@ function runInPage(ctx) {
66
66
 
67
67
  applicableCount += 1;
68
68
 
69
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
70
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
71
-
72
- occurrences.push({
73
- selector: stableSelector,
74
- html,
75
- summary:
76
- 'This image uses a server-side image map, which has no keyboard-operable equivalent.',
77
- hint: 'Replace the server-side image map (ismap) with a client-side image map (<map>/<area>) or separate accessible links/buttons.',
78
- i18n: {
79
- summaryKey: 'serverSideImageMapAbsent_summary_fail',
80
- hintKey: 'serverSideImageMapAbsent_hint_fail',
81
- params: {}
82
- },
83
- data: {
84
- details: { reasonCode: 'SERVER_SIDE_IMAGE_MAP' }
85
- }
86
- });
69
+ occurrences.push(
70
+ helpers.reportOccurrence(el, {
71
+ summary:
72
+ 'This image uses a server-side image map, which has no keyboard-operable equivalent.',
73
+ hint: 'Replace the server-side image map (ismap) with a client-side image map (<map>/<area>) or separate accessible links/buttons.',
74
+ i18n: {
75
+ summaryKey: 'serverSideImageMapAbsent_summary_fail',
76
+ hintKey: 'serverSideImageMapAbsent_hint_fail',
77
+ params: {}
78
+ },
79
+ data: {
80
+ details: { reasonCode: 'SERVER_SIDE_IMAGE_MAP' }
81
+ }
82
+ })
83
+ );
87
84
  }
88
85
 
89
86
  if (applicableCount === 0) {
@@ -242,24 +242,22 @@ function runInPage(ctx) {
242
242
  }
243
243
  })()
244
244
  : null;
245
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
246
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
247
245
 
248
- occurrences.push({
249
- selector: stableSelector,
250
- html,
251
- summary: 'This slider has no accessible name.',
252
- hint: 'Provide a label, aria-label, or aria-labelledby so assistive technologies can identify the slider.',
253
- i18n: {
254
- summaryKey: 'sliderNamePresent_summary_fail',
255
- hintKey: 'sliderNamePresent_hint_fail',
256
- params: { kind }
257
- },
258
- data: {
259
- details: { reasonCode: 'name_missing', controlType: kind, methodTried: res.method },
260
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
261
- }
262
- });
246
+ occurrences.push(
247
+ helpers.reportOccurrence(el, {
248
+ summary: 'This slider has no accessible name.',
249
+ hint: 'Provide a label, aria-label, or aria-labelledby so assistive technologies can identify the slider.',
250
+ i18n: {
251
+ summaryKey: 'sliderNamePresent_summary_fail',
252
+ hintKey: 'sliderNamePresent_hint_fail',
253
+ params: { kind }
254
+ },
255
+ data: {
256
+ details: { reasonCode: 'name_missing', controlType: kind, methodTried: res.method },
257
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
258
+ }
259
+ })
260
+ );
263
261
  }
264
262
 
265
263
  if (applicableCount === 0) {
@@ -227,24 +227,26 @@ function runInPage(ctx) {
227
227
  }
228
228
  })()
229
229
  : null;
230
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
231
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
232
230
 
233
- occurrences.push({
234
- selector: stableSelector,
235
- html,
236
- summary: 'This element has no accessible name.',
237
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this spinbutton's accessible name.",
238
- i18n: {
239
- summaryKey: 'spinbuttonNamePresent_summary_fail',
240
- hintKey: 'spinbuttonNamePresent_hint_fail',
241
- params: { controlType: 'spinbutton' }
242
- },
243
- data: {
244
- details: { reasonCode: 'name_missing', controlType: 'spinbutton', methodTried: res.method },
245
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
246
- }
247
- });
231
+ occurrences.push(
232
+ helpers.reportOccurrence(el, {
233
+ summary: 'This element has no accessible name.',
234
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this spinbutton's accessible name.",
235
+ i18n: {
236
+ summaryKey: 'spinbuttonNamePresent_summary_fail',
237
+ hintKey: 'spinbuttonNamePresent_hint_fail',
238
+ params: { controlType: 'spinbutton' }
239
+ },
240
+ data: {
241
+ details: {
242
+ reasonCode: 'name_missing',
243
+ controlType: 'spinbutton',
244
+ methodTried: res.method
245
+ },
246
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
247
+ }
248
+ })
249
+ );
248
250
  }
249
251
 
250
252
  if (applicableCount === 0) {
@@ -144,24 +144,22 @@ function runInPage(ctx) {
144
144
  }
145
145
  })()
146
146
  : null;
147
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
148
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
149
-
150
- occurrences.push({
151
- selector: stableSelector,
152
- html,
153
- summary: 'This summary has no accessible name.',
154
- hint: 'Provide summary text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
155
- i18n: {
156
- summaryKey: 'summaryNamePresent_summary_fail',
157
- hintKey: 'summaryNamePresent_hint_fail',
158
- params: {}
159
- },
160
- data: {
161
- details: { reasonCode: 'name_missing', controlType: 'summary', methodTried: res.method },
162
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
163
- }
164
- });
147
+
148
+ occurrences.push(
149
+ helpers.reportOccurrence(el, {
150
+ summary: 'This summary has no accessible name.',
151
+ hint: 'Provide summary text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
152
+ i18n: {
153
+ summaryKey: 'summaryNamePresent_summary_fail',
154
+ hintKey: 'summaryNamePresent_hint_fail',
155
+ params: {}
156
+ },
157
+ data: {
158
+ details: { reasonCode: 'name_missing', controlType: 'summary', methodTried: res.method },
159
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
160
+ }
161
+ })
162
+ );
165
163
  }
166
164
 
167
165
  if (applicableCount === 0) {
@@ -148,24 +148,22 @@ function runInPage(ctx) {
148
148
  }
149
149
  })()
150
150
  : null;
151
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
152
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
153
-
154
- occurrences.push({
155
- selector: stableSelector,
156
- html,
157
- summary: 'This tab has no accessible name.',
158
- hint: 'Provide tab text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
159
- i18n: {
160
- summaryKey: 'tabNamePresent_summary_fail',
161
- hintKey: 'tabNamePresent_hint_fail',
162
- params: {}
163
- },
164
- data: {
165
- details: { reasonCode: 'name_missing', controlType: 'tab', methodTried: res.method },
166
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
167
- }
168
- });
151
+
152
+ occurrences.push(
153
+ helpers.reportOccurrence(el, {
154
+ summary: 'This tab has no accessible name.',
155
+ hint: 'Provide tab text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
156
+ i18n: {
157
+ summaryKey: 'tabNamePresent_summary_fail',
158
+ hintKey: 'tabNamePresent_hint_fail',
159
+ params: {}
160
+ },
161
+ data: {
162
+ details: { reasonCode: 'name_missing', controlType: 'tab', methodTried: res.method },
163
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
164
+ }
165
+ })
166
+ );
169
167
  }
170
168
 
171
169
  if (applicableCount === 0) {
@@ -104,23 +104,21 @@ function runInPage(ctx) {
104
104
  const dedupedInvalidIds = [...new Set(invalid.map((i) => i.id))];
105
105
 
106
106
  const tag = el.tagName.toLowerCase();
107
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
108
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
109
-
110
- occurrences.push({
111
- selector: stableSelector,
112
- html,
113
- summary: 'This cell’s headers attribute references one or more invalid header cells.',
114
- hint: 'Update the headers attribute so every id refers to a <th> element within the same table.',
115
- i18n: {
116
- summaryKey: 'tableHeadersAttrValid_summary_fail',
117
- hintKey: 'tableHeadersAttrValid_hint_fail',
118
- params: { element: tag, invalidIds: dedupedInvalidIds.join(', ') }
119
- },
120
- data: {
121
- details: { reasonCode: 'TABLE_HEADERS_ATTR_INVALID', element: tag, invalid }
122
- }
123
- });
107
+
108
+ occurrences.push(
109
+ helpers.reportOccurrence(el, {
110
+ summary: 'This cell’s headers attribute references one or more invalid header cells.',
111
+ hint: 'Update the headers attribute so every id refers to a <th> element within the same table.',
112
+ i18n: {
113
+ summaryKey: 'tableHeadersAttrValid_summary_fail',
114
+ hintKey: 'tableHeadersAttrValid_hint_fail',
115
+ params: { element: tag, invalidIds: dedupedInvalidIds.join(', ') }
116
+ },
117
+ data: {
118
+ details: { reasonCode: 'TABLE_HEADERS_ATTR_INVALID', element: tag, invalid }
119
+ }
120
+ })
121
+ );
124
122
  }
125
123
 
126
124
  if (applicableCount === 0) {