@surea11y/core 1.3.0 → 1.4.1

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 (166) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/README.md +109 -35
  3. package/bin/surea11y-core.js +20 -0
  4. package/docs/API_STABILITY.md +26 -0
  5. package/docs/ARIA_DEPRECATION.md +95 -0
  6. package/docs/CI_INTEGRATIONS.md +7 -7
  7. package/docs/ENGINE_OPTIONS.md +1 -1
  8. package/docs/I18N.md +12 -9
  9. package/docs/INTEGRATION.md +1 -1
  10. package/docs/LIMITATIONS.md +1 -1
  11. package/docs/REPORT.md +1 -1
  12. package/docs/RULE_CATALOG.md +6 -6
  13. package/package.json +52 -16
  14. package/src/baseline.js +0 -0
  15. package/src/checks/automatic/area-alt-present.js +4 -6
  16. package/src/checks/automatic/aria-allowed-attr.js +663 -134
  17. package/src/checks/automatic/aria-allowed-role.js +2 -0
  18. package/src/checks/automatic/aria-braille-equivalent.js +2 -0
  19. package/src/checks/automatic/aria-conditional-attr.js +8 -7
  20. package/src/checks/automatic/aria-deprecated-role.js +107 -38
  21. package/src/checks/automatic/aria-hidden-body.js +6 -4
  22. package/src/checks/automatic/aria-hidden-focus.js +12 -13
  23. package/src/checks/automatic/aria-prohibited-attr.js +98 -105
  24. package/src/checks/automatic/aria-prohibited-children.js +56 -87
  25. package/src/checks/automatic/aria-required-attr.js +6 -7
  26. package/src/checks/automatic/aria-required-children.js +7 -10
  27. package/src/checks/automatic/aria-required-parent.js +20 -25
  28. package/src/checks/automatic/aria-role-name-present.js +2 -0
  29. package/src/checks/automatic/aria-roles-valid.js +33 -6
  30. package/src/checks/automatic/aria-valid-attr-value.js +18 -15
  31. package/src/checks/automatic/aria-valid-attr.js +2 -0
  32. package/src/checks/automatic/autocomplete-valid.js +39 -1
  33. package/src/checks/automatic/avoid-inline-spacing.js +181 -20
  34. package/src/checks/automatic/binary-control-name-present.js +13 -3
  35. package/src/checks/automatic/button-name-present.js +54 -21
  36. package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
  37. package/src/checks/automatic/combobox-name-present.js +10 -1
  38. package/src/checks/automatic/contrast-computable.js +2 -0
  39. package/src/checks/automatic/contrast-enhanced.js +2 -0
  40. package/src/checks/automatic/contrast-minimum.js +2 -0
  41. package/src/checks/automatic/css-orientation-lock.js +21 -27
  42. package/src/checks/automatic/definition-list-children-valid.js +6 -6
  43. package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
  44. package/src/checks/automatic/dialog-name-present.js +18 -11
  45. package/src/checks/automatic/dlitem-parent-valid.js +2 -0
  46. package/src/checks/automatic/duplicate-id-aria.js +4 -3
  47. package/src/checks/automatic/embed-text-alternative-present.js +2 -0
  48. package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
  49. package/src/checks/automatic/form-control-single-label.js +110 -43
  50. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
  51. package/src/checks/automatic/iframe-focusable-content.js +246 -18
  52. package/src/checks/automatic/iframe-name-present.js +2 -0
  53. package/src/checks/automatic/iframe-title-unique.js +3 -1
  54. package/src/checks/automatic/img-alt-present.js +23 -18
  55. package/src/checks/automatic/input-image-alt-present.js +101 -52
  56. package/src/checks/automatic/label-in-name.js +97 -27
  57. package/src/checks/automatic/language-page-present.js +7 -1
  58. package/src/checks/automatic/link-in-text-block.js +2 -0
  59. package/src/checks/automatic/link-name-present.js +52 -17
  60. package/src/checks/automatic/list-children-valid.js +14 -24
  61. package/src/checks/automatic/listbox-name-present.js +10 -1
  62. package/src/checks/automatic/listitem-parent-valid.js +30 -7
  63. package/src/checks/automatic/menuitem-name-present.js +10 -1
  64. package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
  65. package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
  66. package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
  67. package/src/checks/automatic/meter-name-present.js +12 -4
  68. package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
  69. package/src/checks/automatic/object-text-alternative-present.js +16 -7
  70. package/src/checks/automatic/option-name-present.js +10 -1
  71. package/src/checks/automatic/page-title-present.js +2 -0
  72. package/src/checks/automatic/progressbar-name-present.js +16 -11
  73. package/src/checks/automatic/role-img-alt-present.js +4 -4
  74. package/src/checks/automatic/searchbox-name-present.js +10 -1
  75. package/src/checks/automatic/server-side-image-map-absent.js +4 -3
  76. package/src/checks/automatic/slider-name-present.js +13 -2
  77. package/src/checks/automatic/spinbutton-name-present.js +10 -1
  78. package/src/checks/automatic/summary-name-present.js +10 -1
  79. package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
  80. package/src/checks/automatic/svg-text-alternative-present.js +17 -5
  81. package/src/checks/automatic/tab-name-present.js +10 -1
  82. package/src/checks/automatic/table-headers-attr-valid.js +3 -2
  83. package/src/checks/automatic/table-th-has-data-cells.js +69 -6
  84. package/src/checks/automatic/target-size-minimum.js +5 -0
  85. package/src/checks/automatic/td-has-header.js +24 -1
  86. package/src/checks/automatic/textbox-name-present.js +10 -1
  87. package/src/checks/automatic/tooltip-name-present.js +10 -1
  88. package/src/checks/automatic/treeitem-name-present.js +10 -1
  89. package/src/checks/automatic/valid-lang.js +18 -3
  90. package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
  91. package/src/checks/manual/accesskeys-manual.js +3 -1
  92. package/src/checks/manual/area-alt-decorative-manual.js +2 -0
  93. package/src/checks/manual/area-alt-quality-manual.js +2 -0
  94. package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
  95. package/src/checks/manual/aria-text-manual.js +6 -5
  96. package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
  97. package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
  98. package/src/checks/manual/css-hidden-focus.js +184 -9
  99. package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
  100. package/src/checks/manual/empty-heading-manual.js +17 -17
  101. package/src/checks/manual/empty-table-header-manual.js +52 -25
  102. package/src/checks/manual/focus-order-semantics-manual.js +16 -4
  103. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
  104. package/src/checks/manual/heading-order-manual.js +28 -1
  105. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
  106. package/src/checks/manual/image-redundant-alt-manual.js +21 -1
  107. package/src/checks/manual/img-alt-decorative-manual.js +2 -0
  108. package/src/checks/manual/img-alt-quality-manual.js +2 -0
  109. package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
  110. package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
  111. package/src/checks/manual/label-title-only-manual.js +29 -22
  112. package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
  113. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
  114. package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
  115. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
  116. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
  117. package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
  118. package/src/checks/manual/landmark-one-main-manual.js +12 -23
  119. package/src/checks/manual/landmark-unique-manual.js +37 -52
  120. package/src/checks/manual/link-name-quality-manual.js +2 -0
  121. package/src/checks/manual/media-transcript-present-manual.js +2 -0
  122. package/src/checks/manual/meta-viewport-large-manual.js +3 -1
  123. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
  124. package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
  125. package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
  126. package/src/checks/manual/p-as-heading-manual.js +2 -0
  127. package/src/checks/manual/page-has-heading-one-manual.js +12 -11
  128. package/src/checks/manual/page-title-patterns-manual.js +2 -0
  129. package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
  130. package/src/checks/manual/region-manual.js +27 -36
  131. package/src/checks/manual/scope-attr-valid-manual.js +3 -1
  132. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
  133. package/src/checks/manual/skip-link-manual.js +7 -6
  134. package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
  135. package/src/checks/manual/tabindex-manual.js +3 -1
  136. package/src/checks/manual/table-duplicate-name-manual.js +5 -4
  137. package/src/checks/manual/table-fake-caption-manual.js +24 -3
  138. package/src/checks/manual/video-caption-manual.js +2 -0
  139. package/src/checks/manual-review.js +2 -0
  140. package/src/core.js +11820 -3317
  141. package/src/index.js +2 -0
  142. package/src/report.js +51 -9
  143. package/src/sarif.js +20 -5
  144. package/surea11y.browser.js +4943 -1388
  145. package/bin/core.js +0 -473
  146. package/docs/CLI.md +0 -128
  147. package/src/catalogs/composites.wcag.js +0 -454
  148. package/src/checks/automatic/bypass-blocks-present.js +0 -215
  149. package/src/checks/rules-and-tags.full.csv +0 -19
  150. package/src/checks/rules-and-tags.full.json +0 -259
  151. package/src/core/aria-helpers.js +0 -1211
  152. package/src/core/contrast-helpers.js +0 -1302
  153. package/src/core/dom-helpers.js +0 -4493
  154. package/src/core/dom-runner.js +0 -787
  155. package/src/core/frame-messaging.js +0 -261
  156. package/src/core/frame-scan.js +0 -190
  157. package/src/core/rollup-composites.js +0 -127
  158. package/src/core/rule-meta.js +0 -176
  159. package/src/coverage/wcag-facets.js +0 -1079
  160. package/src/coverage/wcag-version-map.js +0 -84
  161. package/src/i18n/en.js +0 -1228
  162. package/src/i18n/fr.js +0 -1185
  163. package/src/policy/contracts.js +0 -18
  164. package/src/policy/resolvePolicy.js +0 -59
  165. package/src/policy/schemas/engine-options.schema.json +0 -103
  166. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,34 +1,40 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check avoid-inline-spacing
5
7
  * @atomic true
6
- * @summary Inline style must not force text-spacing properties with !important
8
+ * @summary Inline style must not force text spacing below the WCAG metric
7
9
  * @standard WCAG 2.2
8
10
  * @sc 1.4.12
9
11
  * @applicability
10
- * Applies to elements with an inline style attribute that sets
11
- * line-height, letter-spacing, or word-spacing.
12
+ * Applies to a rendered element with visible text of its own whose style
13
+ * attribute declares line-height, letter-spacing or word-spacing as
14
+ * `!important` with a real value. A CSS-wide keyword (inherit, initial,
15
+ * unset, revert) specifies no spacing of its own and is out of scope.
12
16
  * @expectation
13
- * None of those three properties are declared with `!important` in the
14
- * inline style. WCAG 1.4.12 (Text Spacing) requires that users be able
15
- * to override these properties via a user stylesheet without losing
16
- * content or functionality; `!important` on an inline declaration
17
- * defeats that override (inline `!important` wins over nearly every
18
- * other origin, including user stylesheets applied the normal way).
17
+ * Each such declaration already meets WCAG 1.4.12's own metric for that
18
+ * property, as a multiple of the font size: line-height at least 1.5,
19
+ * letter-spacing at least 0.12, word-spacing at least 0.16. A forced value
20
+ * that already satisfies the criterion leaves the user nothing to override.
19
21
  * @implementation-notes
20
- * - Static text-based parsing of the style attribute string (regex for
21
- * the three property names followed by `!important`), not computed
22
- * style — matches the property list a widely-used reference engine's avoid-inline-spacing
23
- * rule checks.
22
+ * - Computed style resolves the cascade and the units; the declared value is
23
+ * only a fallback for environments that do not lay the document out. Spacing
24
+ * that resolves to neither is left unreported rather than failed.
25
+ * - Within one declaration block, importance outranks order, so the effective
26
+ * declaration is the last `!important` one for that property.
27
+ * - ACT 78fd32/24afc2/9e45ec additionally require the text to contain a soft
28
+ * wrap break, which cannot be determined without layout. Not implemented, so
29
+ * a forced value on text that never wraps is still reported.
24
30
  */
25
31
 
26
32
  const id = 'avoid-inline-spacing';
27
33
 
28
34
  const meta = {
29
- title: 'Inline style must not force text spacing with !important',
35
+ title: 'Inline style must not force text spacing below the WCAG metric',
30
36
  description:
31
- 'Checks that inline style does not set line-height, letter-spacing, or word-spacing with !important, which blocks user text-spacing overrides.',
37
+ 'Checks that where inline style forces line-height, letter-spacing or word-spacing with !important, the value already meets WCAG 1.4.12, so the user has nothing left to override.',
32
38
  i18n: {
33
39
  titleKey: 'avoidInlineSpacing_title',
34
40
  descriptionKey: 'avoidInlineSpacing_description'
@@ -56,6 +62,20 @@ function runInPage(ctx) {
56
62
  const { helpers, rule } = ctx;
57
63
 
58
64
  const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
65
+ // WCAG 1.4.12's own metrics, as multiples of the font size.
66
+ const MIN_RATIO = { 'line-height': 1.5, 'letter-spacing': 0.12, 'word-spacing': 0.16 };
67
+ // Only these two take the value from the parent, leaving this declaration
68
+ // specifying no spacing of its own. `initial` and `revert` resolve to a
69
+ // concrete value (`normal` for all three properties), so they stay in scope.
70
+ const INHERITED_KEYWORDS = ['inherit', 'unset'];
71
+ // No user agent's `normal` line height reaches 1.5, so a forced `normal`
72
+ // always falls short of the metric.
73
+ const NORMAL_LINE_HEIGHT_RATIO = 1.2;
74
+ const CAMEL = {
75
+ 'line-height': 'lineHeight',
76
+ 'letter-spacing': 'letterSpacing',
77
+ 'word-spacing': 'wordSpacing'
78
+ };
59
79
 
60
80
  const nodes = helpers.queryAllSmart
61
81
  ? helpers.queryAllSmart('[style]')
@@ -64,6 +84,136 @@ function runInPage(ctx) {
64
84
  const occurrences = [];
65
85
  let applicableCount = 0;
66
86
 
87
+ // Within one declaration block, importance wins over order, so the last
88
+ // important declaration is the one that takes effect. Passed Example 5 of ACT
89
+ // 78fd32 turns on this and Passed Example 6 on the non-important half.
90
+ function effectiveDeclaration(raw, prop) {
91
+ const re = new RegExp('(?:^|;)\\s*' + prop + '\\s*:\\s*([^;]*)', 'gi');
92
+ let chosen = null;
93
+ let m;
94
+ while ((m = re.exec(raw))) {
95
+ const value = String(m[1] || '').trim();
96
+ const important = /!\s*important\s*$/i.test(value);
97
+ const clean = value.replace(/!\s*important\s*$/i, '').trim();
98
+ if (!clean) continue;
99
+ if (!chosen || important || !chosen.important) {
100
+ if (chosen && chosen.important && !important) continue;
101
+ chosen = { value: clean, important };
102
+ }
103
+ }
104
+ return chosen;
105
+ }
106
+
107
+ function hasVisibleTextChild(el) {
108
+ let kids;
109
+ try {
110
+ kids = el.childNodes ? Array.from(el.childNodes) : [];
111
+ } catch {
112
+ return false;
113
+ }
114
+ return kids.some((n) => n && n.nodeType === 3 && String(n.nodeValue || '').trim() !== '');
115
+ }
116
+
117
+ function isRendered(el) {
118
+ if (helpers.isDomVisibleEligible) {
119
+ try {
120
+ return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
121
+ } catch {
122
+ return true;
123
+ }
124
+ }
125
+ return true;
126
+ }
127
+
128
+ const px = (v) => {
129
+ const n = parseFloat(v);
130
+ return Number.isFinite(n) && /px\s*$/.test(String(v)) ? n : null;
131
+ };
132
+
133
+ function computedStyleOf(el) {
134
+ if (helpers && typeof helpers.computedStyle === 'function') {
135
+ try {
136
+ const cs = helpers.computedStyle(el);
137
+ if (cs) return cs;
138
+ } catch {
139
+ // fall through to the realm's own view
140
+ }
141
+ }
142
+ try {
143
+ const view = el.ownerDocument && el.ownerDocument.defaultView;
144
+ if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
145
+ } catch {
146
+ // no computed style available
147
+ }
148
+ return null;
149
+ }
150
+
151
+ // ACT scopes these rules to text visible on screen, and text pushed far off
152
+ // canvas is the one hidden shape the shared eligibility check keeps eligible.
153
+ function isOffScreen(el) {
154
+ if (!helpers.getVisibilityHintsInfo) return false;
155
+ try {
156
+ const info = helpers.getVisibilityHintsInfo(el, ctx, {});
157
+ return !!(info && Array.isArray(info.hints) && info.hints.indexOf('offscreen') !== -1);
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ // A realm that does not lay the document out reports font-size as the CSS
164
+ // absolute-size keyword rather than a length. Resolving those keeps px-valued
165
+ // spacing checkable there, and cannot manufacture a failure: a font size
166
+ // larger than assumed only makes a px ratio smaller.
167
+ const ABSOLUTE_FONT_SIZES = {
168
+ 'xx-small': 9,
169
+ 'x-small': 10,
170
+ small: 13,
171
+ medium: 16,
172
+ large: 18,
173
+ 'x-large': 24,
174
+ 'xx-large': 32
175
+ };
176
+
177
+ function fontSizeOf(cs) {
178
+ if (!cs) return null;
179
+ const direct = px(cs.fontSize);
180
+ if (direct !== null) return direct;
181
+ const keyword = String(cs.fontSize || '')
182
+ .trim()
183
+ .toLowerCase();
184
+ return ABSOLUTE_FONT_SIZES[keyword] || null;
185
+ }
186
+
187
+ /**
188
+ * The spacing as a multiple of the font size, or null when it cannot be
189
+ * resolved. Computed style is preferred because it already applies the
190
+ * cascade and unit resolution; the declared value is only a fallback for
191
+ * environments that do not lay the document out.
192
+ */
193
+ function spacingRatio(el, prop, declared) {
194
+ const cs = computedStyleOf(el);
195
+ const fontSize = fontSizeOf(cs);
196
+ if (cs && fontSize) {
197
+ const used = px(cs[CAMEL[prop]]);
198
+ if (used !== null) return used / fontSize;
199
+ }
200
+
201
+ const v = String(declared || '')
202
+ .trim()
203
+ .toLowerCase();
204
+ // `normal`, and the keywords that resolve to it, add nothing for the two
205
+ // spacing properties and stay under the metric for line height.
206
+ if (v === 'normal' || v === 'initial' || v === 'revert' || v === 'revert-layer') {
207
+ return prop === 'line-height' ? NORMAL_LINE_HEIGHT_RATIO : 0;
208
+ }
209
+ if (/^[0-9.]+$/.test(v)) return parseFloat(v);
210
+ if (/em$/.test(v)) return parseFloat(v);
211
+ if (/%$/.test(v)) return parseFloat(v) / 100;
212
+ const asPx = px(v);
213
+ if (asPx !== null && fontSize) return asPx / fontSize;
214
+ return null;
215
+ }
216
+
67
217
  for (const el of nodes) {
68
218
  if (!el || !el.getAttribute) continue;
69
219
  const raw = String(el.getAttribute('style') || '');
@@ -73,14 +223,25 @@ function runInPage(ctx) {
73
223
  const hasAnySpacingProp = SPACING_PROPS.some((p) => lower.includes(p));
74
224
  if (!hasAnySpacingProp) continue;
75
225
 
76
- applicableCount += 1;
226
+ // The rule is about text the user needs to re-space, so an element with no
227
+ // text of its own, or none that renders, is not a target.
228
+ if (!hasVisibleTextChild(el) || !isRendered(el) || isOffScreen(el)) continue;
77
229
 
78
230
  const flagged = [];
231
+ let inScope = false;
79
232
  for (const prop of SPACING_PROPS) {
80
- const re = new RegExp(prop.replace('-', '\\-') + '\\s*:[^;]*!important', 'i');
81
- if (re.test(raw)) flagged.push(prop);
233
+ const decl = effectiveDeclaration(raw, prop);
234
+ if (!decl || !decl.important) continue;
235
+ if (INHERITED_KEYWORDS.indexOf(decl.value.toLowerCase()) !== -1) continue;
236
+ inScope = true;
237
+ const ratio = spacingRatio(el, prop, decl.value);
238
+ // Unresolvable spacing is left alone: this engine reserves fail for
239
+ // high-confidence violations.
240
+ if (ratio === null) continue;
241
+ if (ratio < MIN_RATIO[prop]) flagged.push(prop);
82
242
  }
83
243
 
244
+ if (inScope) applicableCount += 1;
84
245
  if (!flagged.length) continue;
85
246
 
86
247
  const tag = el.tagName.toLowerCase();
@@ -90,8 +251,8 @@ function runInPage(ctx) {
90
251
  occurrences.push({
91
252
  selector: stableSelector,
92
253
  html,
93
- summary: `This element's inline style forces ${flagged.join(', ')} with !important, blocking user text-spacing overrides.`,
94
- hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles so users can override text spacing.',
254
+ summary: `This element's inline style forces ${flagged.join(', ')} with !important below the WCAG text-spacing metric, so the user cannot raise it.`,
255
+ hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles, or set a value that already meets the metric (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em).',
95
256
  i18n: {
96
257
  summaryKey: 'avoidInlineSpacing_summary_fail',
97
258
  hintKey: 'avoidInlineSpacing_hint_fail',
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'binary-control-name-present';
@@ -135,9 +137,16 @@ function runInPage(ctx) {
135
137
  return '';
136
138
  }
137
139
 
140
+ // Naming rules apply only to elements included in the accessibility tree
141
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
142
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
138
143
  function isEligibleAcc(helpers, el, ctx) {
139
144
  const fn =
140
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
145
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
146
+ ? helpers.isIncludedInAccessibilityTree
147
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
148
+ ? helpers.isAccTreeEligible
149
+ : null;
141
150
  if (!fn) return true;
142
151
  try {
143
152
  const r = fn(el, ctx);
@@ -151,8 +160,9 @@ function runInPage(ctx) {
151
160
  const occurrences = [];
152
161
  let applicableCount = 0;
153
162
 
154
- const selector =
155
- 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
163
+ // Native checkbox/radio without an explicit role belongs to
164
+ // form-control-programmatic-label-present.
165
+ const selector = '[role="checkbox"], [role="radio"], [role="switch"]';
156
166
  const nodes = helpers.queryAllSmart
157
167
  ? helpers.queryAllSmart(selector)
158
168
  : helpers.queryAll(selector);
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  // NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
@@ -47,9 +49,8 @@ function runInPage(ctx) {
47
49
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
48
50
  // it has one, not just literal text nodes. See getContentNameInfo's
49
51
  // header comment in src/core/dom-helpers.js for the full rationale
50
- // (this replaced a text-node-only TreeWalker that missed the common
51
- // "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
52
- // icon-button pattern).
52
+ // (covers the common "<a><img alt='...'></a>" logo-link /
53
+ // "<button><img alt='...'></button>" icon-button pattern).
53
54
  if (helpers.getContentNameInfo) {
54
55
  const info = helpers.getContentNameInfo(container, ctx);
55
56
  return info && info.present ? info.value : '';
@@ -77,7 +78,14 @@ function runInPage(ctx) {
77
78
 
78
79
  for (const el of nodes) {
79
80
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
80
- const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
81
+ // Naming rules apply only to elements included in the accessibility tree
82
+ // (ACT c487ae), which excludes focusable aria-hidden content;
83
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
84
+ const eligResult = helpers.isIncludedInAccessibilityTree
85
+ ? helpers.isIncludedInAccessibilityTree(el, ctx)
86
+ : helpers.isAccTreeEligible
87
+ ? helpers.isAccTreeEligible(el, ctx)
88
+ : true;
81
89
  const eligible =
82
90
  typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
83
91
  if (!eligible) continue;
@@ -103,25 +111,50 @@ function runInPage(ctx) {
103
111
  inputValueName = getInputButtonValueName(el);
104
112
  }
105
113
 
106
- // A native <button> (or [role="button"]) whose role has been overridden to
107
- // one of these roles is no longer semantically a button — per the WAI-ARIA
108
- // Accessible Name and Description Computation spec these roles are
109
- // name-from-author-only, and their rendered content represents a VALUE,
110
- // not a NAME. Found on a real page (Spotify's "sort by" control): a
111
- // <button role="combobox">List</button> where "List" is the combobox's
112
- // currently selected value, not a label for what the combobox is —
113
- // crediting it as the accessible name masked a real missing-name bug.
114
- const VALUE_ROLES = [
115
- 'textbox',
116
- 'progressbar',
117
- 'scrollbar',
118
- 'slider',
119
- 'spinbutton',
120
- 'combobox',
121
- 'listbox'
114
+ // ARIA 1.2 "Name From: author, contents". Every other known role is
115
+ // name-from-author-only: <button role="combobox">List</button> exposes a
116
+ // value, not a label. An unknown role falls back to the implicit role.
117
+ // <generated:aria-name-from-content>
118
+ const NAME_FROM_CONTENT_ROLES = [
119
+ 'button',
120
+ 'cell',
121
+ 'checkbox',
122
+ 'columnheader',
123
+ 'doc-backlink',
124
+ 'doc-biblioref',
125
+ 'doc-glossref',
126
+ 'doc-noteref',
127
+ 'graphics-object',
128
+ 'gridcell',
129
+ 'heading',
130
+ 'link',
131
+ 'menuitem',
132
+ 'menuitemcheckbox',
133
+ 'menuitemradio',
134
+ 'option',
135
+ 'radio',
136
+ 'row',
137
+ 'rowgroup',
138
+ 'rowheader',
139
+ 'switch',
140
+ 'tab',
141
+ 'tooltip',
142
+ 'treeitem'
122
143
  ];
144
+ // </generated:aria-name-from-content>
145
+ const isKnownRoleToken =
146
+ helpers && helpers.aria && typeof helpers.aria.isKnownRole === 'function'
147
+ ? (() => {
148
+ try {
149
+ return !!helpers.aria.isKnownRole(roleNorm);
150
+ } catch {
151
+ return false;
152
+ }
153
+ })()
154
+ : false;
123
155
  const isContentNameCandidate =
124
- (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
156
+ (tag === 'button' || role === 'button') &&
157
+ (!roleNorm || !isKnownRoleToken || NAME_FROM_CONTENT_ROLES.includes(roleNorm));
125
158
  const contentName =
126
159
  !trustedProgrammaticName && !inputValueName && isContentNameCandidate
127
160
  ? getConservativeSubtreeText(el)
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -7,9 +9,8 @@
7
9
  * @standard WCAG 2.2
8
10
  * @sc 1.1.1
9
11
  * @applicability
10
- * Applies to <canvas> elements that are exposed to assistive technologies.
11
- * Elements otherwise hidden from the accessibility tree remain applicable
12
- * if they are focusable (tabbable) or referenced by IDREF relationships (per engine eligibility checks).
12
+ * Applies to <canvas> elements included in the accessibility tree.
13
+ * Hidden elements are excluded whether or not they are focusable.
13
14
  * @expectation
14
15
  * Each applicable <canvas> provides a text alternative via fallback content or an accessible name.
15
16
  */
@@ -69,8 +70,15 @@ function runInPage(ctx) {
69
70
  const getEligibilityInfo =
70
71
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
71
72
 
72
- const isAccTreeEligible =
73
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
73
+ // aria-hidden removes the canvas from the accessibility tree, so a text
74
+ // alternative on it cannot reach anyone; aria-hidden-focus reports a
75
+ // focusable one. Matches the other non-text-content rules.
76
+ const isEligibleHelper =
77
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
78
+ ? helpers.isIncludedInAccessibilityTree
79
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
80
+ ? helpers.isAccTreeEligible
81
+ : null;
74
82
 
75
83
  const getTextAlternativeInfo =
76
84
  helpers && typeof helpers.getTextAlternativeInfo === 'function'
@@ -95,15 +103,16 @@ function runInPage(ctx) {
95
103
  for (const el of canvases) {
96
104
  if (!el) continue;
97
105
 
98
- // Applicability: eligible in the accessibility tree (with focusable/IDREF exceptions handled by helper).
99
- if (isAccTreeEligible) {
106
+ // Applicability: included in the accessibility tree.
107
+ if (isEligibleHelper) {
100
108
  const elig = (() => {
101
109
  try {
102
- return isAccTreeEligible(el, ctx);
110
+ return isEligibleHelper(el, ctx);
103
111
  } catch {
104
112
  return { eligible: true, reasons: [] };
105
113
  }
106
114
  })();
115
+ if (elig === false) continue;
107
116
  if (elig && elig.eligible === false) continue;
108
117
  }
109
118
 
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'combobox-name-present';
@@ -122,9 +124,16 @@ function runInPage(ctx) {
122
124
  return '';
123
125
  }
124
126
 
127
+ // Naming rules apply only to elements included in the accessibility tree
128
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
129
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
125
130
  function isEligibleAcc(helpers, el, ctx) {
126
131
  const fn =
127
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
132
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
133
+ ? helpers.isIncludedInAccessibilityTree
134
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
135
+ ? helpers.isAccTreeEligible
136
+ : null;
128
137
  if (!fn) return true;
129
138
  try {
130
139
  const r = fn(el, ctx);
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'contrast-computable';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'contrast-enhanced';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'contrast-minimum';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -20,25 +22,17 @@
20
22
  * display orientation unless that orientation is essential.
21
23
  * @implementation-notes
22
24
  * - The rotation DEGREE is what makes this the exploit signature, not
23
- * merely the presence of a `rotate()` function — verified against a
24
- * widely-used reference engine's own `css-orientation-lock` check
25
- * (`cssOrientationLockEvaluate`), which computes the actual rotation angle and only
26
- * flags ~90/~270 degrees, explicitly excluding ~0/~180 (a no-op or a
27
- * flip, neither of which changes portrait<->landscape). A prior
28
- * version of this rule flagged the bare presence of "rotate(" in an
29
- * orientation-media-query rule regardless of angle — a real, confirmed
30
- * false positive found via the live-DOM cross-engine corpus 2026-07-23
31
- * on a real page (Times of India) whose `@media (orientation:portrait)`
32
- * block only rotated a small decorative arrow icon 45 degrees, nothing
33
- * resembling a page-orientation lock.
25
+ * merely the presence of a `rotate()` function: a small decorative icon
26
+ * rotated 45 degrees inside an orientation media query is not a page
27
+ * lock. Compute the actual rotation angle and only flag ~90/~270 degrees,
28
+ * excluding ~0/~180 (a no-op or a flip, neither of which changes
29
+ * portrait<->landscape).
34
30
  * - Only `rotate`/`rotateZ` (transform functions) and the standalone CSS
35
- * `rotate` property are parsed for degrees — matches that reference engine's
36
- * own two sources (`style.transform`/`style.rotate`) but, like that engine,
37
- * does not attempt to decompose `matrix()`/`matrix3d()`/`rotate3d()` into an
38
- * equivalent angle (that engine does handle those; deliberately deferred here
39
- * as the same class of "genuinely more complex, lower real-world
40
- * value" work already deferred elsewhere in this engine, e.g.
41
- * `table-th-has-data-cells`'s narrower positional-header algorithm).
31
+ * `rotate` property are parsed for degrees, from the two sources
32
+ * `style.transform`/`style.rotate`; `matrix()`/`matrix3d()`/`rotate3d()`
33
+ * are not decomposed into an equivalent angle (deliberately deferred as
34
+ * the same class of higher-complexity/lower-value work deferred elsewhere,
35
+ * e.g. `table-th-has-data-cells`'s narrower positional-header algorithm).
42
36
  * - Cross-origin stylesheets throw on `.cssRules` access (browser
43
37
  * security model) and are skipped — same class of limitation as any
44
38
  * check that can only see same-origin/inspectable content (compare
@@ -98,9 +92,9 @@ function runInPage(ctx) {
98
92
  }
99
93
 
100
94
  // Converts a rotate()/rotateZ() angle argument (with unit) to degrees.
101
- // Matches a widely-used reference engine's own getAngleInDegrees: only deg/grad/rad/turn are
102
- // recognized; a unitless or unrecognized value contributes 0 (ignored,
103
- // not treated as a lock -- an unparseable angle isn't a confirmed exploit).
95
+ // Only deg/grad/rad/turn are recognized; a unitless or unrecognized value
96
+ // contributes 0 (ignored, not treated as a lock -- an unparseable angle
97
+ // isn't a confirmed exploit).
104
98
  function angleToDegrees(raw) {
105
99
  const m = /(-?[\d.]+)\s*(deg|grad|rad|turn)/i.exec(raw);
106
100
  if (!m) return 0;
@@ -127,17 +121,17 @@ function runInPage(ctx) {
127
121
  return total;
128
122
  }
129
123
 
130
- // Whole-page-orientation-lock detection, matching a widely-used reference engine's own degree
131
- // math exactly (see @implementation-notes): a rotation near 0 or 180
132
- // degrees (mod 180) is a no-op or a flip, neither of which changes
133
- // portrait<->landscape, so it's explicitly NOT a lock; only a rotation
134
- // near 90 or 270 degrees (mod 90, once the 0/180 case is excluded) is.
124
+ // Whole-page-orientation-lock detection (see @implementation-notes): a
125
+ // rotation near 0 or 180 degrees (mod 180) is a no-op or a flip, neither
126
+ // of which changes portrait<->landscape, so it's NOT a lock; only a
127
+ // rotation near 90 or 270 degrees (mod 90, once the 0/180 case is
128
+ // excluded) is.
135
129
  function isLockingRotation(styleDecl) {
136
130
  if (!styleDecl) return false;
137
131
  const transformVal =
138
132
  trim(styleDecl.transform) ||
139
133
  trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('-webkit-transform') : '');
140
- // The standalone CSS `rotate` property (distinct from `transform: rotate()`) -- that reference engine checks this too.
134
+ // The standalone CSS `rotate` property (distinct from `transform: rotate()`).
141
135
  const rotatePropVal = trim(
142
136
  styleDecl.getPropertyValue ? styleDecl.getPropertyValue('rotate') : ''
143
137
  );
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -18,12 +20,11 @@
18
20
  * violation (see implementation-notes). Any other direct or wrapped child
19
21
  * breaks the description-list semantics assistive technologies rely on.
20
22
  * @implementation-notes
21
- * - Only one level of <div> wrapping is flattened, matching a widely-used
22
- * reference engine's definition-list algorithm — a <div> nested inside
23
+ * - Only one level of <div> wrapping is flattened — a <div> nested inside
23
24
  * another wrapping <div> is not flattened further and its contents are
24
25
  * reported invalid.
25
- * - Verified 2026-07-20 against a widely-used reference engine: the dt/dd pairing is
26
- * only required "when not empty". A flattened set with NEITHER dt nor dd
26
+ * - The dt/dd pairing is only required "when not empty". A flattened set
27
+ * with NEITHER dt nor dd
27
28
  * — whether from an empty wrapping <div>, only <script>/<template>/
28
29
  * <style> content, or a genuinely childless <dl> — is not flagged; only
29
30
  * an unbalanced dt/dd pairing is a real structural problem.
@@ -108,8 +109,7 @@ function runInPage(ctx) {
108
109
  }
109
110
  const dedupedInvalidTags = [...new Set(invalidTags)];
110
111
 
111
- // Verified 2026-07-20 against a widely-used reference engine's definition-list check:
112
- // the dt/dd pairing is only required "when not empty" — a <dl> with
112
+ // The dt/dd pairing is only required "when not empty" — a <dl> with
113
113
  // NEITHER dt nor dd (whether genuinely childless after flattening, only
114
114
  // passthrough script/template/style content, or an empty wrapping div)
115
115
  // is vacuously fine, not a violation. Only an UNBALANCED pairing (dt
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -14,8 +16,8 @@
14
16
  * @expectation
15
17
  * Neither element is present. Since their movement can never be paused,
16
18
  * stopped, or hidden by the user, presence is itself the violation — this
17
- * rule has no partial-pass case, matching a widely-used reference engine's `blink`/`marquee`
18
- * rules (which report only when the element is found).
19
+ * rule has no partial-pass case (it reports only when the element is
20
+ * found).
19
21
  * @implementation-notes
20
22
  * - Not rule-gated on isAccTreeEligible: presence in markup is itself the
21
23
  * violation, independent of visibility (moving/blinking content inside a