@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,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -9,7 +11,12 @@
9
11
  * @applicability
10
12
  * Applies to inline <svg> elements that are exposed to assistive technologies AND appear intended to be conveyed.
11
13
  * "Intended to be conveyed" is approximated deterministically by at least one of:
12
- * - role="img"
14
+ * - role="img", role="graphics-symbol", or role="graphics-document"
15
+ * on the SVG root element itself (the WAI-ARIA Graphics Module
16
+ * roles, alongside img). Deliberately does NOT extend to arbitrary
17
+ * role="graphics-symbol" descendants nested inside an <svg> — this
18
+ * check's scope is the <svg> root only; a broader feature, not
19
+ * attempted here.
13
20
  * - aria-label / aria-labelledby present
14
21
  * - <title> or <desc> present (desc alone is an applicability signal only — see @expectation)
15
22
  * - focusable/tabbable (e.g., tabindex, native focusability)
@@ -23,10 +30,9 @@
23
30
  * - an ARIA name (aria-label / aria-labelledby).
24
31
  * A <desc> element alone does NOT satisfy this — per the SVG Accessibility
25
32
  * API Mappings spec §7.1, <desc> only ever contributes to the accessible
26
- * DESCRIPTION, never the accessible NAME (verified 2026-07-20 directly
27
- * against the SVG-AAM spec text, not assumed). An <svg> with only a
33
+ * DESCRIPTION, never the accessible NAME. An <svg> with only a
28
34
  * <desc> and no <title>/ARIA name is still "applicable" (desc signals
29
- * authorial intent) but fails, matching a widely-used reference engine's svg-img-alt.
35
+ * authorial intent) but fails.
30
36
  */
31
37
 
32
38
  const id = 'svg-text-alternative-present';
@@ -222,7 +228,13 @@ function runInPage(ctx) {
222
228
  const descText = titleText ? '' : nonEmptyDescText(el); // avoid second scan if title already passes
223
229
  const hasTitleOrDesc = !!(titleText || descText);
224
230
 
225
- const hasIntent = role === 'img' || hasAriaNamingAttr || hasTitleOrDesc || focusable;
231
+ const hasIntent =
232
+ role === 'img' ||
233
+ role === 'graphics-symbol' ||
234
+ role === 'graphics-document' ||
235
+ hasAriaNamingAttr ||
236
+ hasTitleOrDesc ||
237
+ focusable;
226
238
 
227
239
  if (!hasIntent) continue;
228
240
 
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'tab-name-present';
@@ -81,9 +83,16 @@ function runInPage(ctx) {
81
83
  return '';
82
84
  }
83
85
 
86
+ // Naming rules apply only to elements included in the accessibility tree
87
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
88
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
84
89
  function isEligibleAcc(helpers, el, ctx) {
85
90
  const fn =
86
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
91
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
92
+ ? helpers.isIncludedInAccessibilityTree
93
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
94
+ ? helpers.isAccTreeEligible
95
+ : null;
87
96
  if (!fn) return true;
88
97
  try {
89
98
  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
  /**
@@ -14,8 +16,7 @@
14
16
  * the referencing cell, and (d) is not the cell itself.
15
17
  * @implementation-notes
16
18
  * - One occurrence per offending cell (not per bad token), listing every
17
- * invalid reference — matches a widely-used reference engine's
18
- * td-headers-attr reporting granularity.
19
+ * invalid reference.
19
20
  * - Not rule-gated on isAccTreeEligible: this remains a static-markup
20
21
  * property, while engine-level hidden-subtree filtering still applies
21
22
  * unless engineOptions.includeHiddenElements is true.
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -7,7 +9,10 @@
7
9
  * @standard WCAG 2.2
8
10
  * @sc 1.3.1
9
11
  * @applicability
10
- * Applies to <table> elements that contain at least one <th>.
12
+ * Applies to <table> elements that keep their table semantics and are included
13
+ * in the accessibility tree, and that contain at least one <th> which is
14
+ * visible, included in the accessibility tree, and not overridden by an
15
+ * explicit role other than rowheader/columnheader.
11
16
  * @expectation
12
17
  * The table also contains at least one <td> somewhere in it.
13
18
  * @implementation-notes
@@ -28,9 +33,10 @@
28
33
  * if some particular <th> in it doesn't actually describe any cell
29
34
  * (false negative, not a false positive — acceptable under this
30
35
  * engine's philosophy).
31
- * - Not rule-gated on isAccTreeEligible: this remains a static-markup
32
- * property, while engine-level hidden-subtree filtering still applies
33
- * unless engineOptions.includeHiddenElements is true.
36
+ * - Applicability IS gated, because the zero-<td> case above turns any
37
+ * over-broad scope straight into a false fail: a layout table marked
38
+ * role="presentation" with a stray <th> and no <td> was being failed,
39
+ * as were tables whose only header was hidden or carried role="cell".
34
40
  */
35
41
 
36
42
  const id = 'table-th-has-data-cells';
@@ -70,16 +76,73 @@ function runInPage(ctx) {
70
76
  const occurrences = [];
71
77
  let applicableCount = 0;
72
78
 
79
+ function explicitRole(el) {
80
+ try {
81
+ return String((el && el.getAttribute && el.getAttribute('role')) || '')
82
+ .trim()
83
+ .toLowerCase()
84
+ .split(/\s+/)[0];
85
+ } catch {
86
+ return '';
87
+ }
88
+ }
89
+
90
+ function isIncludedInTree(el) {
91
+ const fn =
92
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
93
+ ? helpers.isIncludedInAccessibilityTree
94
+ : null;
95
+ if (!fn) return true;
96
+ try {
97
+ const r = fn(el, ctx);
98
+ return typeof r === 'boolean' ? r : !!(r && r.eligible);
99
+ } catch {
100
+ return true;
101
+ }
102
+ }
103
+
104
+ // role="presentation"/"none" removes the table role, and with it every
105
+ // header cell the role would have implied. A table is not focusable, so
106
+ // presentational role conflict resolution does not restore it.
107
+ function hasTableSemantics(table) {
108
+ const role = explicitRole(table);
109
+ if (role === 'presentation' || role === 'none') return false;
110
+ if (role && role !== 'table' && role !== 'grid' && role !== 'treegrid') return false;
111
+ return isIncludedInTree(table);
112
+ }
113
+
114
+ // A <th> only carries a rowheader/columnheader role while nothing overrides
115
+ // it, and a hidden header describes nothing to a screen reader either way.
116
+ function isHeaderCellInScope(th) {
117
+ if (!th) return false;
118
+ const role = explicitRole(th);
119
+ if (role && role !== 'rowheader' && role !== 'columnheader') return false;
120
+ if (helpers.isDomVisibleEligible) {
121
+ try {
122
+ if (!helpers.isDomVisibleEligible(th, ctx, { targetSet: 'dom' }).eligible) return false;
123
+ } catch {
124
+ // fall through to the tree check
125
+ }
126
+ }
127
+ return isIncludedInTree(th);
128
+ }
129
+
73
130
  for (const table of tables) {
74
131
  if (!table || !table.querySelectorAll) continue;
75
132
 
133
+ // A table stripped of its semantics has no header cells to describe
134
+ // anything, so nothing in it is in scope.
135
+ if (!hasTableSemantics(table)) continue;
136
+
76
137
  let ths;
77
138
  try {
78
139
  ths = table.querySelectorAll('th');
79
140
  } catch {
80
141
  ths = [];
81
142
  }
82
- if (!ths.length) continue;
143
+
144
+ const headers = Array.from(ths).filter(isHeaderCellInScope);
145
+ if (!headers.length) continue;
83
146
 
84
147
  applicableCount += 1;
85
148
 
@@ -91,7 +154,7 @@ function runInPage(ctx) {
91
154
  }
92
155
  if (hasDataCell) continue;
93
156
 
94
- for (const th of ths) {
157
+ for (const th of headers) {
95
158
  if (!th) continue;
96
159
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(th) : 'html';
97
160
  const html = helpers.getOuterHtmlSnippet
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -591,6 +593,7 @@ function runInPage(ctx) {
591
593
  cantTellOccurrences.push({
592
594
  selector: buildSelector(it.el),
593
595
  html: htmlSnippet(it.el),
596
+ occurrenceOutcome: 'cantTell',
594
597
  summary:
595
598
  'Target may be too small and too close to another target, but the overlap is near the detection threshold and could not be confidently measured.',
596
599
  hint: 'Manually verify the effective spacing between this target and its neighbor; increase target size or spacing if the overlap is real.',
@@ -619,6 +622,7 @@ function runInPage(ctx) {
619
622
  cantTellOccurrences.push({
620
623
  selector: buildSelector(it.el),
621
624
  html: htmlSnippet(it.el),
625
+ occurrenceOutcome: 'cantTell',
622
626
  summary:
623
627
  'Target is too small and too close to another target, but may be exempt as part of an essential graphic or image-map region.',
624
628
  hint: 'Verify whether this target’s size is genuinely essential to its function (e.g. part of an SVG/canvas/image map); if not, increase target size or spacing.',
@@ -642,6 +646,7 @@ function runInPage(ctx) {
642
646
  failOccurrences.push({
643
647
  selector: buildSelector(it.el),
644
648
  html: htmlSnippet(it.el),
649
+ occurrenceOutcome: 'fail',
645
650
  summary: 'Target is too small and too close to another target.',
646
651
  hint: 'Increase target size to at least 24 by 24 CSS pixels, or add sufficient spacing.',
647
652
  i18n: {
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -69,6 +71,19 @@ function runInPage(ctx) {
69
71
  return (v == null ? '' : String(v)).trim();
70
72
  }
71
73
 
74
+ const isAccTreeEligible =
75
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
76
+
77
+ function isEligible(node) {
78
+ if (!isAccTreeEligible) return true;
79
+ try {
80
+ const r = isAccTreeEligible(node, ctx);
81
+ return !!(r && r.eligible);
82
+ } catch {
83
+ return true;
84
+ }
85
+ }
86
+
72
87
  const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
73
88
 
74
89
  const occurrences = [];
@@ -95,8 +110,12 @@ function runInPage(ctx) {
95
110
 
96
111
  applicableCount += 1;
97
112
 
113
+ // An aria-hidden <th> is removed from the accessibility tree entirely
114
+ // -- a screen reader never announces it, so it can't actually serve as
115
+ // another cell's row/column header, even though it's still structurally
116
+ // a <th>.
98
117
  function isHeaderCell(cell) {
99
- return !!(cell && cell.tagName && cell.tagName.toLowerCase() === 'th');
118
+ return !!(cell && cell.tagName && cell.tagName.toLowerCase() === 'th' && isEligible(cell));
100
119
  }
101
120
 
102
121
  function hasColumnHeaderAbove(r, c) {
@@ -121,6 +140,10 @@ function runInPage(ctx) {
121
140
  const cell = cells[c];
122
141
  if (!cell || isHeaderCell(cell)) continue;
123
142
 
143
+ // An aria-hidden data cell isn't exposed to AT either, so it has
144
+ // no need for an accessible header association.
145
+ if (!isEligible(cell)) continue;
146
+
124
147
  const headersAttr = trim(cell.getAttribute('headers'));
125
148
  if (headersAttr) continue;
126
149
 
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'textbox-name-present';
@@ -107,9 +109,16 @@ function runInPage(ctx) {
107
109
  return '';
108
110
  }
109
111
 
112
+ // Naming rules apply only to elements included in the accessibility tree
113
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
114
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
110
115
  function isEligibleAcc(helpers, el, ctx) {
111
116
  const fn =
112
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
117
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
118
+ ? helpers.isIncludedInAccessibilityTree
119
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
120
+ ? helpers.isAccTreeEligible
121
+ : null;
113
122
  if (!fn) return true;
114
123
  try {
115
124
  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 = 'tooltip-name-present';
@@ -81,9 +83,16 @@ function runInPage(ctx) {
81
83
  return '';
82
84
  }
83
85
 
86
+ // Naming rules apply only to elements included in the accessibility tree
87
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
88
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
84
89
  function isEligibleAcc(helpers, el, ctx) {
85
90
  const fn =
86
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
91
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
92
+ ? helpers.isIncludedInAccessibilityTree
93
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
94
+ ? helpers.isAccTreeEligible
95
+ : null;
87
96
  if (!fn) return true;
88
97
  try {
89
98
  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 = 'treeitem-name-present';
@@ -81,9 +83,16 @@ function runInPage(ctx) {
81
83
  return '';
82
84
  }
83
85
 
86
+ // Naming rules apply only to elements included in the accessibility tree
87
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
88
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
84
89
  function isEligibleAcc(helpers, el, ctx) {
85
90
  const fn =
86
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
91
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
92
+ ? helpers.isIncludedInAccessibilityTree
93
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
94
+ ? helpers.isAccTreeEligible
95
+ : null;
87
96
  if (!fn) return true;
88
97
  try {
89
98
  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
  /**
@@ -56,7 +58,13 @@ const meta = {
56
58
  function runInPage(ctx) {
57
59
  const { helpers, rule } = ctx;
58
60
 
61
+ // Shape alone accepts unregistered tags such as "eng" and "em-US", so the
62
+ // primary subtag is checked against the IANA registry via the shared helper.
59
63
  const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
64
+ const isValidTag =
65
+ typeof helpers.isValidLanguageTag === 'function'
66
+ ? helpers.isValidLanguageTag
67
+ : (v) => BCP47_RE.test(String(v || ''));
60
68
 
61
69
  const nodes = helpers.queryAllSmart
62
70
  ? helpers.queryAllSmart('[lang]')
@@ -69,12 +77,19 @@ function runInPage(ctx) {
69
77
  if (!el || !el.getAttribute) continue;
70
78
  if (el.tagName && el.tagName.toLowerCase() === 'html') continue;
71
79
 
72
- const raw = String(el.getAttribute('lang') || '').trim();
73
- if (!raw) continue;
80
+ const rawAttr = el.getAttribute('lang');
81
+ if (rawAttr === null || rawAttr === '') continue; // ACT de46e4: empty is out of scope
82
+
83
+ // The rule applies only where text actually inherits the language, so an
84
+ // element with no text, or whose text reaches no one, has nothing to
85
+ // declare a language for.
86
+ if (!String(el.textContent || '').trim()) continue;
74
87
 
75
88
  applicableCount += 1;
76
89
 
77
- if (BCP47_RE.test(raw)) continue;
90
+ // A whitespace-only value is in scope and has no primary language tag.
91
+ const raw = String(rawAttr).trim();
92
+ if (isValidTag(raw)) continue;
78
93
 
79
94
  const tag = el.tagName.toLowerCase();
80
95
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check accesskeys
5
7
  * @atomic true
6
8
  * @summary accesskey values must be unique on the page
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Applies whenever two or more elements share the same non-empty
10
12
  * accesskey attribute value (case-insensitive).
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -14,34 +16,23 @@
14
16
  * checked state is already exposed natively), but when an author sets
15
17
  * it explicitly it should agree with the element's actual state —
16
18
  * otherwise assistive technology is told something different from what
17
- * a sighted user perceives. This is the same static/live-DOM check
18
- * a widely-used reference engine's own `aria-conditional-attr` rule
19
- * performs (verified directly against its `ariaConditionalCheckboxAttr`/
20
- * `ariaConditionalRadioAttr` source).
19
+ * a sighted user perceives.
21
20
  * @implementation-notes
22
21
  * - Deliberately authored as `type: 'manual'` (cantTell-capped, never
23
- * fail), unlike most ARIA-validity rules in this file family. That
24
- * reference engine runs as a script injected into the LIVE, already-hydrated DOM, so a
25
- * mismatch it reports is a genuine, current-state fact. This engine
26
- * analyzes STATIC markup only (no script execution) — `.checked`
22
+ * fail), unlike most ARIA-validity rules in this file family. This
23
+ * engine analyzes STATIC markup only (no script execution) — `.checked`
27
24
  * reliably reflects the static `checked` attribute for freshly-parsed
28
- * markup (verified directly against jsdom), but a very common, entirely
29
- * legitimate real-world pattern is a JS-hydrated widget whose
30
- * server-rendered HTML intentionally ships `aria-checked="true"` (or
31
- * `"mixed"`) BEFORE client JS sets the native `checked`/`indeterminate`
32
- * property to match on hydration. A hard `fail` here would misfire on
33
- * that pattern constantly — real risk given how common it is in exactly
34
- * the kind of JS-framework-heavy pages this engine's own real-world
35
- * corpus is full of. Flagging for human review (cantTell) captures the
36
- * same signal without the false-positive risk of asserting it as a
37
- * confirmed violation.
25
+ * markup, but a very common, entirely legitimate real-world pattern is a
26
+ * JS-hydrated widget whose server-rendered HTML intentionally ships
27
+ * `aria-checked="true"` (or `"mixed"`) BEFORE client JS sets the native
28
+ * `checked`/`indeterminate` property to match on hydration. A hard
29
+ * `fail` here would misfire on that pattern constantly. Flagging for
30
+ * human review (cantTell) captures the same signal without the
31
+ * false-positive risk of asserting it as a confirmed violation.
38
32
  * - `aria-checked="mixed"` is only meaningful for checkboxes (radio
39
- * buttons have no indeterminate state) — matches that reference engine's
40
- * own `normalizeAriaChecked` (radio) vs `normalizeAriaChecked2` (checkbox)
41
- * split, verified directly against its source.
33
+ * buttons have no indeterminate state).
42
34
  * - Any `aria-checked` value other than "true"/"false"/"mixed" (checkbox)
43
- * or "true"/"false" (radio) is treated as equivalent to "false" — also
44
- * matches that reference engine's own normalization exactly.
35
+ * or "true"/"false" (radio) is treated as equivalent to "false".
45
36
  * - Not rule-gated on isAccTreeEligible: whether the accessible state
46
37
  * matches is still a markup-correctness property, while engine-level
47
38
  * hidden-subtree filtering applies unless engineOptions.includeHiddenElements
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check aria-text
5
7
  * @atomic true
6
8
  * @summary role="text" elements should have no focusable descendants
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Elements with an explicit `role="text"`.
10
12
  * @expectation
@@ -15,10 +17,9 @@
15
17
  * focusable content — a focusable descendant inside a "this is just
16
18
  * text" region is unreachable or confusing for keyboard/AT users.
17
19
  * @implementation-notes
18
- * - Not WCAG-normative by a widely-used reference engine's own classification — authored as an
19
- * advisory, cantTell-capped `type: 'manual'` rule, matching the Tier 1b
20
- * precedent (see `landmark-unique`'s header comment for the shared
21
- * rationale).
20
+ * - Not WCAG-normative — authored as an advisory, cantTell-capped
21
+ * `type: 'manual'` rule, matching the Tier 1b precedent (see
22
+ * `landmark-unique`'s header comment for the shared rationale).
22
23
  * - "Focusable descendant" is a presence check (link/button/form
23
24
  * control/`[tabindex]`/`iframe`/`[contenteditable]`), the same
24
25
  * conservative selector `scrollable-region-focusable` uses, not a full