@surea11y/core 1.4.1 → 1.6.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 (151) hide show
  1. package/CHANGELOG.md +212 -128
  2. package/README.md +46 -9
  3. package/docs/ACT_RULE_MAPPING.md +243 -0
  4. package/docs/API_STABILITY.md +4 -4
  5. package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
  6. package/docs/DESIGN_CHALLENGES.md +301 -0
  7. package/docs/ENGINE_OPTIONS.md +30 -14
  8. package/docs/I18N.md +176 -20
  9. package/docs/INTEGRATION.md +29 -7
  10. package/docs/LIMITATIONS.md +6 -4
  11. package/docs/OUTPUT_SCHEMA.md +13 -3
  12. package/docs/REPORT.md +3 -1
  13. package/docs/RULE_AUTHORING.md +104 -23
  14. package/docs/RULE_CATALOG.md +1878 -169
  15. package/docs/RULE_TAXONOMY.md +2 -2
  16. package/docs/TROUBLESHOOTING.md +4 -4
  17. package/docs/WCAG_CONFORMANCE.md +25 -9
  18. package/package.json +8 -7
  19. package/src/baseline.js +3 -3
  20. package/src/checks/automatic/area-alt-present.js +2 -2
  21. package/src/checks/automatic/aria-allowed-attr.js +95 -40
  22. package/src/checks/automatic/aria-allowed-role.js +16 -18
  23. package/src/checks/automatic/aria-braille-equivalent.js +19 -21
  24. package/src/checks/automatic/aria-conditional-attr.js +22 -24
  25. package/src/checks/automatic/aria-deprecated-role.js +63 -50
  26. package/src/checks/automatic/aria-hidden-body.js +4 -11
  27. package/src/checks/automatic/aria-hidden-focus.js +104 -23
  28. package/src/checks/automatic/aria-prohibited-attr.js +71 -72
  29. package/src/checks/automatic/aria-prohibited-children.js +154 -61
  30. package/src/checks/automatic/aria-required-attr.js +74 -29
  31. package/src/checks/automatic/aria-required-children.js +38 -34
  32. package/src/checks/automatic/aria-required-parent.js +78 -29
  33. package/src/checks/automatic/aria-role-name-present.js +36 -22
  34. package/src/checks/automatic/aria-roles-valid.js +37 -23
  35. package/src/checks/automatic/aria-valid-attr-value.js +33 -33
  36. package/src/checks/automatic/aria-valid-attr.js +15 -18
  37. package/src/checks/automatic/autocomplete-valid.js +17 -19
  38. package/src/checks/automatic/avoid-inline-spacing.js +14 -16
  39. package/src/checks/automatic/binary-control-name-present.js +46 -26
  40. package/src/checks/automatic/button-name-present.js +115 -34
  41. package/src/checks/automatic/combobox-name-present.js +40 -22
  42. package/src/checks/automatic/contrast-computable.js +32 -0
  43. package/src/checks/automatic/contrast-enhanced.js +21 -1
  44. package/src/checks/automatic/contrast-minimum.js +21 -1
  45. package/src/checks/automatic/css-orientation-lock.js +118 -41
  46. package/src/checks/automatic/definition-list-children-valid.js +25 -29
  47. package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
  48. package/src/checks/automatic/dialog-name-present.js +36 -20
  49. package/src/checks/automatic/dlitem-parent-valid.js +15 -17
  50. package/src/checks/automatic/duplicate-id-aria.js +50 -40
  51. package/src/checks/automatic/duplicate-id.js +198 -0
  52. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  53. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  54. package/src/checks/automatic/form-control-single-label.js +39 -41
  55. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
  56. package/src/checks/automatic/iframe-focusable-content.js +92 -38
  57. package/src/checks/automatic/iframe-name-present.js +53 -21
  58. package/src/checks/automatic/iframe-title-unique.js +19 -24
  59. package/src/checks/automatic/img-alt-present.js +12 -4
  60. package/src/checks/automatic/label-in-name.js +198 -49
  61. package/src/checks/automatic/link-in-text-block.js +29 -31
  62. package/src/checks/automatic/link-name-present.js +47 -31
  63. package/src/checks/automatic/list-children-valid.js +21 -23
  64. package/src/checks/automatic/listbox-name-present.js +42 -24
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -21
  66. package/src/checks/automatic/menuitem-name-present.js +36 -20
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
  70. package/src/checks/automatic/meter-name-present.js +38 -21
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
  72. package/src/checks/automatic/option-name-present.js +39 -22
  73. package/src/checks/automatic/page-title-present.js +21 -3
  74. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  75. package/src/checks/automatic/progressbar-name-present.js +41 -24
  76. package/src/checks/automatic/role-img-alt-present.js +64 -16
  77. package/src/checks/automatic/searchbox-name-present.js +46 -24
  78. package/src/checks/automatic/server-side-image-map-absent.js +16 -19
  79. package/src/checks/automatic/slider-name-present.js +42 -23
  80. package/src/checks/automatic/spinbutton-name-present.js +46 -24
  81. package/src/checks/automatic/summary-name-present.js +34 -20
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  83. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  84. package/src/checks/automatic/tab-name-present.js +37 -20
  85. package/src/checks/automatic/table-headers-attr-valid.js +57 -24
  86. package/src/checks/automatic/table-th-has-data-cells.js +76 -24
  87. package/src/checks/automatic/target-size-minimum.js +172 -131
  88. package/src/checks/automatic/td-has-header.js +20 -25
  89. package/src/checks/automatic/textbox-name-present.js +42 -24
  90. package/src/checks/automatic/tooltip-name-present.js +37 -20
  91. package/src/checks/automatic/treeitem-name-present.js +39 -22
  92. package/src/checks/automatic/valid-lang.js +107 -24
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  94. package/src/checks/manual/accesskeys-manual.js +21 -22
  95. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  96. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +23 -26
  98. package/src/checks/manual/aria-text-manual.js +4 -4
  99. package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  101. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  103. package/src/checks/manual/empty-heading-manual.js +73 -28
  104. package/src/checks/manual/empty-table-header-manual.js +33 -36
  105. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  106. package/src/checks/manual/form-control-label-quality-manual.js +453 -0
  107. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +4 -11
  108. package/src/checks/manual/heading-order-manual.js +20 -25
  109. package/src/checks/manual/heading-quality-manual.js +338 -0
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +18 -21
  112. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  113. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  115. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  116. package/src/checks/manual/label-title-only-manual.js +19 -21
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
  123. package/src/checks/manual/landmark-one-main-manual.js +8 -15
  124. package/src/checks/manual/landmark-unique-manual.js +31 -36
  125. package/src/checks/manual/link-name-quality-manual.js +162 -35
  126. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  127. package/src/checks/manual/meta-viewport-large-manual.js +16 -19
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
  129. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  131. package/src/checks/manual/p-as-heading-manual.js +4 -4
  132. package/src/checks/manual/page-has-heading-one-manual.js +8 -15
  133. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  134. package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
  135. package/src/checks/manual/region-manual.js +32 -25
  136. package/src/checks/manual/scope-attr-valid-manual.js +16 -19
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  138. package/src/checks/manual/skip-link-manual.js +44 -52
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  140. package/src/checks/manual/tabindex-manual.js +16 -19
  141. package/src/checks/manual/table-duplicate-name-manual.js +16 -19
  142. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  143. package/src/checks/manual/video-caption-manual.js +3 -3
  144. package/src/checks/manual-review.js +17 -1
  145. package/src/core.js +13086 -5169
  146. package/src/report.js +16 -2
  147. package/surea11y.browser.js +5665 -4219
  148. package/surea11y.i18n.de.js +22 -0
  149. package/surea11y.i18n.es.js +22 -0
  150. package/surea11y.i18n.fr.js +22 -0
  151. package/bin/surea11y-core.js +0 -20
@@ -6,7 +6,7 @@
6
6
  * @check empty-heading
7
7
  * @atomic true
8
8
  * @summary Heading elements must not be empty
9
- * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion)
10
10
  * @applicability
11
11
  * Applies to elements with a heading role: native <h1>-<h6>, or any
12
12
  * element with explicit role="heading" (unless overridden by another
@@ -18,20 +18,20 @@
18
18
  * heading is announced as "heading, level N" with nothing else, which
19
19
  * is confusing when navigating by heading.
20
20
  * @implementation-notes
21
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
21
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
22
22
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
23
23
  * header comment for the shared rationale/precedent.
24
24
  * - This is also the reconciliation point for the ACT-rules
25
- * "heading-name-present" requirement (see ROADMAP.md "Tier 5
26
- * candidates"): already covered by this pre-existing rule under a
27
- * different name, not a separate gap. `title` is accepted as a naming
25
+ * "heading-name-present" requirement: already covered by this
26
+ * pre-existing rule under a different name, not a separate gap.
27
+ * `title` is accepted as a naming
28
28
  * fallback, and hidden/aria-hidden/display:none headings are excluded
29
29
  * (gated on `isAccTreeEligible`), so an empty heading no AT user could
30
30
  * ever reach is not flagged.
31
31
  * - Descendant name resolution uses the shared, accname-aligned
32
- * `helpers.getContentNameInfo` (see dom-helpers.js) — the same "name
32
+ * `helpers.getContentNameInfo` (see dom-helpers.js), the same "name
33
33
  * from content" implementation the 19 `-name-present` rules already
34
- * use — rather than a narrower hand-rolled walker, so an `<img alt="...">`
34
+ * use, rather than a narrower hand-rolled walker, so an `<img alt="...">`
35
35
  * descendant's alt text (e.g. a
36
36
  * `<h1><a><div><img alt="..."></div></a></h1>` logo header) is correctly
37
37
  * picked up as the heading's name instead of producing a false "empty
@@ -74,11 +74,58 @@ function runInPage(ctx) {
74
74
  return raw.split(/\s+/)[0].toLowerCase();
75
75
  }
76
76
 
77
+ // Same Global States and Properties set used elsewhere in this engine
78
+ // (aria-required-parent.js, aria-prohibited-children.js) for the same
79
+ // presentational-roles-conflict-resolution concept: a native h1-h6
80
+ // marked role="none"/"presentation" still reverts to its native heading
81
+ // role when it carries a global ARIA attribute (even one with an empty
82
+ // value, like aria-label="" -- the attribute's presence is what
83
+ // triggers conflict resolution, not its value).
84
+ const GLOBAL_ARIA_ATTRS = [
85
+ 'aria-atomic',
86
+ 'aria-braillelabel',
87
+ 'aria-brailleroledescription',
88
+ 'aria-busy',
89
+ 'aria-controls',
90
+ 'aria-current',
91
+ 'aria-describedby',
92
+ 'aria-description',
93
+ 'aria-details',
94
+ 'aria-disabled',
95
+ 'aria-dropeffect',
96
+ 'aria-errormessage',
97
+ 'aria-flowto',
98
+ 'aria-grabbed',
99
+ 'aria-haspopup',
100
+ 'aria-hidden',
101
+ 'aria-invalid',
102
+ 'aria-keyshortcuts',
103
+ 'aria-label',
104
+ 'aria-labelledby',
105
+ 'aria-live',
106
+ 'aria-owns',
107
+ 'aria-relevant',
108
+ 'aria-roledescription'
109
+ ];
110
+
111
+ function hasGlobalAriaAttr(el) {
112
+ for (const attr of GLOBAL_ARIA_ATTRS) {
113
+ if (el.getAttribute && el.getAttribute(attr) != null) return true;
114
+ }
115
+ return false;
116
+ }
117
+
77
118
  function isHeading(el) {
78
- const explicit = getExplicitRoleToken(el);
79
- if (explicit) return explicit === 'heading';
80
119
  const tag = el.tagName ? el.tagName.toLowerCase() : '';
81
- return /^h[1-6]$/.test(tag);
120
+ const isNativeHeadingTag = /^h[1-6]$/.test(tag);
121
+
122
+ const explicit = getExplicitRoleToken(el);
123
+ if (!explicit) return isNativeHeadingTag;
124
+ if (explicit === 'heading') return true;
125
+ if ((explicit === 'none' || explicit === 'presentation') && isNativeHeadingTag) {
126
+ return hasGlobalAriaAttr(el);
127
+ }
128
+ return false;
82
129
  }
83
130
 
84
131
  function getAccessibleNameText(el) {
@@ -100,7 +147,7 @@ function runInPage(ctx) {
100
147
  if (joined) return joined;
101
148
  }
102
149
  // Shared, accname-aligned "name from content" implementation (see
103
- // dom-helpers.js's getContentNameInfo header comment) — resolves an
150
+ // dom-helpers.js's getContentNameInfo header comment). Resolves an
104
151
  // <img> descendant's own alt text, an aria-label/aria-labelledby'd
105
152
  // descendant's own name, etc., and already gates every descendant on
106
153
  // full accessibility-tree eligibility (aria-hidden, display:none,
@@ -148,8 +195,6 @@ function runInPage(ctx) {
148
195
  const name = getAccessibleNameText(el);
149
196
  if (name) continue;
150
197
 
151
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
152
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
153
198
  const eligInfo = helpers.getEligibilityInfo
154
199
  ? (() => {
155
200
  try {
@@ -160,21 +205,21 @@ function runInPage(ctx) {
160
205
  })()
161
206
  : null;
162
207
 
163
- occurrences.push({
164
- selector: stableSelector,
165
- html,
166
- summary: 'This heading has no accessible name.',
167
- hint: 'Add text content (or aria-label/aria-labelledby) to this heading, or remove it if it is not needed.',
168
- i18n: {
169
- summaryKey: 'emptyHeading_summary_cantTell',
170
- hintKey: 'emptyHeading_hint_cantTell',
171
- params: {}
172
- },
173
- data: {
174
- details: { reasonCode: 'HEADING_EMPTY' },
175
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
176
- }
177
- });
208
+ occurrences.push(
209
+ helpers.reportOccurrence(el, {
210
+ summary: 'This heading has no accessible name.',
211
+ hint: 'Add text content (or aria-label/aria-labelledby) to this heading, or remove it if it is not needed.',
212
+ i18n: {
213
+ summaryKey: 'emptyHeading_summary_cantTell',
214
+ hintKey: 'emptyHeading_hint_cantTell',
215
+ params: {}
216
+ },
217
+ data: {
218
+ details: { reasonCode: 'HEADING_EMPTY' },
219
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
220
+ }
221
+ })
222
+ );
178
223
  }
179
224
 
180
225
  if (applicableCount === 0) {
@@ -6,7 +6,7 @@
6
6
  * @check empty-table-header
7
7
  * @atomic true
8
8
  * @summary Table header cells must not be empty
9
- * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion)
10
10
  * @applicability
11
11
  * Applies to <th> elements that don't carry a conflicting explicit role,
12
12
  * plus any element (native <th> or not) with role="columnheader" or
@@ -19,19 +19,19 @@
19
19
  * @expectation
20
20
  * The header cell has visible text content. A <th> named only via
21
21
  * aria-label/aria-labelledby (no visible text) is ALSO flagged, not
22
- * treated as equivalent — aria-label support on <th> is genuinely
23
- * inconsistent in practice: NVDA+Firefox and iOS VoiceOver+Safari ignore
22
+ * treated as equivalent: aria-label support on <th> is inconsistent
23
+ * in practice. NVDA+Firefox and iOS VoiceOver+Safari ignore
24
24
  * it entirely (only visible text is announced), JAWS+Chrome/IE11 also only
25
25
  * announce visible text in the header cell itself. Visible text is the one
26
26
  * mechanism confirmed to work across every tested combination. See
27
27
  * https://html5accessibility.com/stuff/2024/05/22/not-so-short-note-on-aria-label-usage-big-table-edition/.
28
28
  * @implementation-notes
29
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
29
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
30
30
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
31
31
  * header comment for the shared rationale/precedent.
32
32
  * - Two distinct reasonCodes: TABLE_HEADER_EMPTY (no accessible name at
33
33
  * all) vs. TABLE_HEADER_NAME_NOT_VISIBLE_TEXT (has aria-label/
34
- * aria-labelledby but no visible text) — the latter gets a different
34
+ * aria-labelledby but no visible text), the latter gets a different
35
35
  * summary/hint explaining the AT-support gap, since the header isn't
36
36
  * literally nameless, just unreliably named in practice.
37
37
  */
@@ -41,7 +41,7 @@ const id = 'empty-table-header';
41
41
  const meta = {
42
42
  title: 'Table header cells must not be empty',
43
43
  description:
44
- 'Checks that table header cells (<th>, or any element with role="columnheader"/"rowheader") have visible text content — a header named only via aria-label/aria-labelledby is also flagged, since real screen-reader/browser support for that is inconsistent.',
44
+ 'Checks that table header cells (<th>, or any element with role="columnheader"/"rowheader") have visible text content. A header named only via aria-label/aria-labelledby is also flagged, since real screen-reader/browser support for that is inconsistent.',
45
45
  i18n: {
46
46
  titleKey: 'emptyTableHeader_title',
47
47
  descriptionKey: 'emptyTableHeader_description'
@@ -141,43 +141,40 @@ function runInPage(ctx) {
141
141
 
142
142
  if (getVisibleText(el)) continue;
143
143
 
144
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
145
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
146
-
147
144
  const ariaName = getAriaOnlyName(el);
148
145
  if (ariaName) {
149
- occurrences.push({
150
- selector: stableSelector,
151
- html,
152
- summary:
153
- 'This table header cell has no visible text — its only accessible name comes from aria-label/aria-labelledby, which real screen-reader/browser combinations (e.g. NVDA+Firefox, iOS VoiceOver+Safari) are known to ignore on <th> elements.',
154
- hint: 'Add visible text content to this header cell (in addition to, or instead of, aria-label/aria-labelledby) — visible text is the only naming mechanism confirmed to work across tested screen readers.',
146
+ occurrences.push(
147
+ helpers.reportOccurrence(el, {
148
+ summary:
149
+ 'This table header cell has no visible text. Its only accessible name comes from aria-label/aria-labelledby, which real screen-reader/browser combinations (e.g. NVDA+Firefox, iOS VoiceOver+Safari) are known to ignore on <th> elements.',
150
+ hint: 'Add visible text content to this header cell (in addition to, or instead of, aria-label/aria-labelledby); visible text is the only naming mechanism confirmed to work across tested screen readers.',
151
+ i18n: {
152
+ summaryKey: 'emptyTableHeader_summary_cantTell_ariaOnly',
153
+ hintKey: 'emptyTableHeader_hint_cantTell_ariaOnly',
154
+ params: {}
155
+ },
156
+ data: {
157
+ details: { reasonCode: 'TABLE_HEADER_NAME_NOT_VISIBLE_TEXT', ariaName }
158
+ }
159
+ })
160
+ );
161
+ continue;
162
+ }
163
+
164
+ occurrences.push(
165
+ helpers.reportOccurrence(el, {
166
+ summary: 'This table header cell has no accessible name.',
167
+ hint: 'Add text content (or aria-label/aria-labelledby) to this header cell, or remove it if it is not needed.',
155
168
  i18n: {
156
- summaryKey: 'emptyTableHeader_summary_cantTell_ariaOnly',
157
- hintKey: 'emptyTableHeader_hint_cantTell_ariaOnly',
169
+ summaryKey: 'emptyTableHeader_summary_cantTell',
170
+ hintKey: 'emptyTableHeader_hint_cantTell',
158
171
  params: {}
159
172
  },
160
173
  data: {
161
- details: { reasonCode: 'TABLE_HEADER_NAME_NOT_VISIBLE_TEXT', ariaName }
174
+ details: { reasonCode: 'TABLE_HEADER_EMPTY' }
162
175
  }
163
- });
164
- continue;
165
- }
166
-
167
- occurrences.push({
168
- selector: stableSelector,
169
- html,
170
- summary: 'This table header cell has no accessible name.',
171
- hint: 'Add text content (or aria-label/aria-labelledby) to this header cell, or remove it if it is not needed.',
172
- i18n: {
173
- summaryKey: 'emptyTableHeader_summary_cantTell',
174
- hintKey: 'emptyTableHeader_hint_cantTell',
175
- params: {}
176
- },
177
- data: {
178
- details: { reasonCode: 'TABLE_HEADER_EMPTY' }
179
- }
180
- });
176
+ })
177
+ );
181
178
  }
182
179
 
183
180
  if (applicableCount === 0) {
@@ -6,32 +6,32 @@
6
6
  * @check focus-order-semantics
7
7
  * @atomic true
8
8
  * @summary Elements added to the tab order should have interactive semantics
9
- * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion)
10
10
  * @applicability
11
11
  * Elements with an explicit `tabindex` of `0` or greater (in the tab
12
12
  * order) AND an explicit `role` attribute that is one of a curated set
13
13
  * of clearly non-interactive, structural/document roles.
14
14
  * @expectation
15
- * An element deliberately placed in the tab order should communicate
16
- * why it's focusable — a role like `heading`, `list`, `region`, or
15
+ * An element placed in the tab order on purpose should communicate
16
+ * why it's focusable: a role like `heading`, `list`, `region`, or
17
17
  * `presentation` gives assistive technology no interactive semantic to
18
18
  * announce, which is confusing for keyboard users who land on it and
19
19
  * get no indication of what activating it (if anything) would do.
20
20
  * @implementation-notes
21
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
21
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
22
22
  * `type: 'manual'` rule.
23
- * - The non-interactive role list is deliberately curated and
24
- * conservative (structural/document roles only) — legitimate custom
25
- * widget patterns using `tabindex` with a genuinely interactive role
26
- * (`option`, `tab`, `menuitem`, etc.) are never flagged. Elements with
27
- * `tabindex` and NO role at all are also not flagged: native semantics
28
- * or an intentionally generic custom-interactive pattern cannot be
23
+ * - The non-interactive role list is curated and conservative
24
+ * (structural/document roles only). Legitimate custom widget patterns
25
+ * using `tabindex` with an actually interactive role (`option`, `tab`,
26
+ * `menuitem`, etc.) are never flagged. Elements with `tabindex` and NO
27
+ * role at all are also not flagged: native semantics or an
28
+ * intentionally generic custom-interactive pattern cannot be
29
29
  * distinguished from markup alone with the same confidence.
30
- * - `region` is deliberately NOT in the non-interactive role list: a
31
- * tabbable `role="region"` is a real, WCAG 2.1.1/2.1.3-grounded pattern
32
- * this engine's own `scrollable-region-focusable` check exists to
33
- * RECOMMEND (a scrollable landmark with no other focusable content
34
- * needs `tabindex="0"` to be keyboard-reachable at all) — flagging it
30
+ * - `region` is intentionally excluded from the non-interactive role
31
+ * list: a tabbable `role="region"` is a real, WCAG 2.1.1/2.1.3-grounded
32
+ * pattern this engine's own `scrollable-region-focusable` check exists
33
+ * to RECOMMEND (a scrollable landmark with no other focusable content
34
+ * needs `tabindex="0"` to be keyboard-reachable at all), so flagging it
35
35
  * here would be internally inconsistent with that sibling check. A
36
36
  * `role="region"` is also commonly made tabbable on its own merits
37
37
  * (e.g. a cookie-consent banner or notification/toast region a keyboard