@surea11y/core 1.3.0 → 1.4.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 (163) hide show
  1. package/CHANGELOG.md +46 -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/CI_INTEGRATIONS.md +7 -7
  6. package/docs/ENGINE_OPTIONS.md +1 -1
  7. package/docs/I18N.md +12 -9
  8. package/docs/INTEGRATION.md +1 -1
  9. package/docs/LIMITATIONS.md +1 -1
  10. package/docs/REPORT.md +1 -1
  11. package/package.json +50 -16
  12. package/src/baseline.js +0 -0
  13. package/src/checks/automatic/area-alt-present.js +4 -6
  14. package/src/checks/automatic/aria-allowed-attr.js +15 -51
  15. package/src/checks/automatic/aria-allowed-role.js +2 -0
  16. package/src/checks/automatic/aria-braille-equivalent.js +2 -0
  17. package/src/checks/automatic/aria-conditional-attr.js +8 -7
  18. package/src/checks/automatic/aria-deprecated-role.js +4 -3
  19. package/src/checks/automatic/aria-hidden-body.js +6 -4
  20. package/src/checks/automatic/aria-hidden-focus.js +12 -13
  21. package/src/checks/automatic/aria-prohibited-attr.js +98 -105
  22. package/src/checks/automatic/aria-prohibited-children.js +56 -87
  23. package/src/checks/automatic/aria-required-attr.js +6 -7
  24. package/src/checks/automatic/aria-required-children.js +7 -10
  25. package/src/checks/automatic/aria-required-parent.js +20 -25
  26. package/src/checks/automatic/aria-role-name-present.js +2 -0
  27. package/src/checks/automatic/aria-roles-valid.js +2 -0
  28. package/src/checks/automatic/aria-valid-attr-value.js +18 -15
  29. package/src/checks/automatic/aria-valid-attr.js +2 -0
  30. package/src/checks/automatic/autocomplete-valid.js +2 -0
  31. package/src/checks/automatic/avoid-inline-spacing.js +3 -2
  32. package/src/checks/automatic/binary-control-name-present.js +2 -0
  33. package/src/checks/automatic/button-name-present.js +7 -7
  34. package/src/checks/automatic/bypass-blocks-present.js +9 -7
  35. package/src/checks/automatic/canvas-text-alternative-present.js +2 -0
  36. package/src/checks/automatic/combobox-name-present.js +2 -0
  37. package/src/checks/automatic/contrast-computable.js +2 -0
  38. package/src/checks/automatic/contrast-enhanced.js +2 -0
  39. package/src/checks/automatic/contrast-minimum.js +2 -0
  40. package/src/checks/automatic/css-orientation-lock.js +21 -27
  41. package/src/checks/automatic/definition-list-children-valid.js +6 -6
  42. package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
  43. package/src/checks/automatic/dialog-name-present.js +10 -10
  44. package/src/checks/automatic/dlitem-parent-valid.js +2 -0
  45. package/src/checks/automatic/duplicate-id-aria.js +4 -3
  46. package/src/checks/automatic/embed-text-alternative-present.js +2 -0
  47. package/src/checks/automatic/form-control-programmatic-label-present.js +2 -0
  48. package/src/checks/automatic/form-control-single-label.js +6 -7
  49. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
  50. package/src/checks/automatic/iframe-focusable-content.js +246 -18
  51. package/src/checks/automatic/iframe-name-present.js +2 -0
  52. package/src/checks/automatic/iframe-title-unique.js +3 -1
  53. package/src/checks/automatic/img-alt-present.js +7 -9
  54. package/src/checks/automatic/input-image-alt-present.js +4 -6
  55. package/src/checks/automatic/label-in-name.js +15 -19
  56. package/src/checks/automatic/language-page-present.js +2 -0
  57. package/src/checks/automatic/link-in-text-block.js +2 -0
  58. package/src/checks/automatic/link-name-present.js +2 -0
  59. package/src/checks/automatic/list-children-valid.js +14 -24
  60. package/src/checks/automatic/listbox-name-present.js +2 -0
  61. package/src/checks/automatic/listitem-parent-valid.js +30 -7
  62. package/src/checks/automatic/menuitem-name-present.js +2 -0
  63. package/src/checks/automatic/meta-refresh-no-exceptions.js +4 -4
  64. package/src/checks/automatic/meta-refresh-timing-absent.js +2 -0
  65. package/src/checks/automatic/meta-viewport-zoom-enabled.js +2 -0
  66. package/src/checks/automatic/meter-name-present.js +4 -3
  67. package/src/checks/automatic/nested-interactive-controls-absent.js +27 -5
  68. package/src/checks/automatic/object-text-alternative-present.js +2 -0
  69. package/src/checks/automatic/option-name-present.js +2 -0
  70. package/src/checks/automatic/page-title-present.js +2 -0
  71. package/src/checks/automatic/progressbar-name-present.js +8 -10
  72. package/src/checks/automatic/role-img-alt-present.js +4 -4
  73. package/src/checks/automatic/searchbox-name-present.js +2 -0
  74. package/src/checks/automatic/server-side-image-map-absent.js +4 -3
  75. package/src/checks/automatic/slider-name-present.js +2 -0
  76. package/src/checks/automatic/spinbutton-name-present.js +2 -0
  77. package/src/checks/automatic/summary-name-present.js +2 -0
  78. package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
  79. package/src/checks/automatic/svg-text-alternative-present.js +17 -5
  80. package/src/checks/automatic/tab-name-present.js +2 -0
  81. package/src/checks/automatic/table-headers-attr-valid.js +3 -2
  82. package/src/checks/automatic/table-th-has-data-cells.js +2 -0
  83. package/src/checks/automatic/target-size-minimum.js +5 -0
  84. package/src/checks/automatic/td-has-header.js +24 -1
  85. package/src/checks/automatic/textbox-name-present.js +2 -0
  86. package/src/checks/automatic/tooltip-name-present.js +2 -0
  87. package/src/checks/automatic/treeitem-name-present.js +2 -0
  88. package/src/checks/automatic/valid-lang.js +2 -0
  89. package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
  90. package/src/checks/manual/accesskeys-manual.js +3 -1
  91. package/src/checks/manual/area-alt-decorative-manual.js +2 -0
  92. package/src/checks/manual/area-alt-quality-manual.js +2 -0
  93. package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
  94. package/src/checks/manual/aria-text-manual.js +6 -5
  95. package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
  96. package/src/checks/manual/css-hidden-focus.js +184 -9
  97. package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
  98. package/src/checks/manual/empty-heading-manual.js +17 -17
  99. package/src/checks/manual/empty-table-header-manual.js +52 -25
  100. package/src/checks/manual/focus-order-semantics-manual.js +16 -4
  101. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
  102. package/src/checks/manual/heading-order-manual.js +28 -1
  103. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
  104. package/src/checks/manual/image-redundant-alt-manual.js +21 -1
  105. package/src/checks/manual/img-alt-decorative-manual.js +2 -0
  106. package/src/checks/manual/img-alt-quality-manual.js +2 -0
  107. package/src/checks/manual/input-image-alt-decorative-manual.js +2 -0
  108. package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
  109. package/src/checks/manual/label-title-only-manual.js +29 -22
  110. package/src/checks/manual/landmark-banner-is-top-level-manual.js +54 -47
  111. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +42 -26
  112. package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
  113. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
  114. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
  115. package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
  116. package/src/checks/manual/landmark-one-main-manual.js +12 -23
  117. package/src/checks/manual/landmark-unique-manual.js +37 -52
  118. package/src/checks/manual/link-name-quality-manual.js +2 -0
  119. package/src/checks/manual/media-transcript-present-manual.js +2 -0
  120. package/src/checks/manual/meta-viewport-large-manual.js +3 -1
  121. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
  122. package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
  123. package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
  124. package/src/checks/manual/p-as-heading-manual.js +2 -0
  125. package/src/checks/manual/page-has-heading-one-manual.js +12 -11
  126. package/src/checks/manual/page-title-patterns-manual.js +2 -0
  127. package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
  128. package/src/checks/manual/region-manual.js +27 -36
  129. package/src/checks/manual/scope-attr-valid-manual.js +3 -1
  130. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
  131. package/src/checks/manual/skip-link-manual.js +7 -6
  132. package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
  133. package/src/checks/manual/tabindex-manual.js +3 -1
  134. package/src/checks/manual/table-duplicate-name-manual.js +5 -4
  135. package/src/checks/manual/table-fake-caption-manual.js +24 -3
  136. package/src/checks/manual/video-caption-manual.js +2 -0
  137. package/src/checks/manual-review.js +2 -0
  138. package/src/core.js +6772 -2158
  139. package/src/index.js +2 -0
  140. package/src/report.js +51 -9
  141. package/src/sarif.js +18 -3
  142. package/surea11y.browser.js +2731 -999
  143. package/bin/core.js +0 -473
  144. package/docs/CLI.md +0 -128
  145. package/src/catalogs/composites.wcag.js +0 -454
  146. package/src/checks/rules-and-tags.full.csv +0 -19
  147. package/src/checks/rules-and-tags.full.json +0 -259
  148. package/src/core/aria-helpers.js +0 -1211
  149. package/src/core/contrast-helpers.js +0 -1302
  150. package/src/core/dom-helpers.js +0 -4493
  151. package/src/core/dom-runner.js +0 -787
  152. package/src/core/frame-messaging.js +0 -261
  153. package/src/core/frame-scan.js +0 -190
  154. package/src/core/rollup-composites.js +0 -127
  155. package/src/core/rule-meta.js +0 -176
  156. package/src/coverage/wcag-facets.js +0 -1079
  157. package/src/coverage/wcag-version-map.js +0 -84
  158. package/src/i18n/en.js +0 -1228
  159. package/src/i18n/fr.js +0 -1185
  160. package/src/policy/contracts.js +0 -18
  161. package/src/policy/resolvePolicy.js +0 -59
  162. package/src/policy/schemas/engine-options.schema.json +0 -103
  163. package/src/policy/schemas/policy-contract.schema.json +0 -40
package/package.json CHANGED
@@ -1,20 +1,50 @@
1
1
  {
2
2
  "name": "@surea11y/core",
3
- "version": "1.3.0",
4
- "description": "Lightweight DOM rules accessibility core with modular rules.",
3
+ "version": "1.4.0",
4
+ "description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
5
+ "keywords": [
6
+ "accessibility",
7
+ "a11y",
8
+ "wcag",
9
+ "wcag21",
10
+ "wcag22",
11
+ "accessibility-testing",
12
+ "a11y-testing",
13
+ "audit",
14
+ "aria",
15
+ "contrast",
16
+ "sarif",
17
+ "zero-dependencies",
18
+ "jsdom",
19
+ "playwright",
20
+ "puppeteer",
21
+ "selenium",
22
+ "cypress",
23
+ "webdriverio",
24
+ "jest",
25
+ "vitest"
26
+ ],
5
27
  "main": "src/index.js",
6
28
  "bin": {
7
- "surea11y": "bin/core.js"
29
+ "surea11y-core": "bin/surea11y-core.js"
30
+ },
31
+ "exports": {
32
+ ".": "./src/index.js",
33
+ "./baseline": "./src/baseline.js",
34
+ "./report": "./src/report.js",
35
+ "./sarif": "./src/sarif.js",
36
+ "./browser": "./surea11y.browser.js",
37
+ "./package.json": "./package.json"
8
38
  },
9
39
  "author": "Jorge Rumoroso",
10
40
  "license": "MPL-2.0",
11
41
  "repository": {
12
42
  "type": "git",
13
- "url": "git+https://github.com/rumoroso/surea11y-core.git"
43
+ "url": "git+https://github.com/SureA11y/core.git"
14
44
  },
15
- "homepage": "https://github.com/rumoroso/surea11y-core#readme",
45
+ "homepage": "https://github.com/SureA11y/core#readme",
16
46
  "bugs": {
17
- "url": "https://github.com/rumoroso/surea11y-core/issues"
47
+ "url": "https://github.com/SureA11y/core/issues"
18
48
  },
19
49
  "engines": {
20
50
  "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
@@ -23,8 +53,13 @@
23
53
  "access": "public"
24
54
  },
25
55
  "files": [
26
- "src",
27
- "bin",
56
+ "src/index.js",
57
+ "src/core.js",
58
+ "src/baseline.js",
59
+ "src/report.js",
60
+ "src/sarif.js",
61
+ "src/checks/**/*.js",
62
+ "bin/surea11y-core.js",
28
63
  "surea11y.browser.js",
29
64
  "docs/**/*.md",
30
65
  "README.md",
@@ -33,8 +68,7 @@
33
68
  "!docs/RULE_TEMPLATE.md",
34
69
  "!docs/RULE_TEST_TEMPLATE.md",
35
70
  "!docs/RULE_TEST_AUTHORING.md",
36
- "!docs/TEST_OUTCOME_STABILITY.md",
37
- "!src/explain"
71
+ "!docs/TEST_OUTCOME_STABILITY.md"
38
72
  ],
39
73
  "scripts": {
40
74
  "lint": "eslint .",
@@ -43,8 +77,8 @@
43
77
  "format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
44
78
  "build": "node scripts/build-core.js && node scripts/build-browser.js",
45
79
  "pretest": "playwright install chromium",
46
- "test": "npm run build && node scripts/run-tests.js",
47
- "test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js --test-coverage-include=bin/**/*.js",
80
+ "test": "npm run format:check && npm run build && node scripts/run-tests.js",
81
+ "test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js",
48
82
  "test:contrast-helpers": "node tests/contrast-helpers.test.js",
49
83
  "helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
50
84
  "engine-perf-bench": "node --expose-gc scripts/engine-perf-bench.js --profileRules=true --iters=25 --top=15",
@@ -56,16 +90,16 @@
56
90
  "fixtures:index": "node scripts/generate-fixture-index.js",
57
91
  "docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
58
92
  "validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
59
- "validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual"
60
- },
61
- "dependencies": {
62
- "jsdom": "^29.1.1"
93
+ "validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
94
+ "i18n:new": "node scripts/i18n-scaffold.js",
95
+ "i18n:report": "node scripts/i18n-report.js"
63
96
  },
64
97
  "devDependencies": {
65
98
  "@eslint/js": "^10.0.1",
66
99
  "eslint": "^10.8.0",
67
100
  "eslint-config-prettier": "^10.1.8",
68
101
  "globals": "^17.8.0",
102
+ "jsdom": "^29.1.1",
69
103
  "playwright": "^1.62.1",
70
104
  "prettier": "^3.9.6"
71
105
  }
package/src/baseline.js CHANGED
Binary file
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -199,12 +201,8 @@ function runInPage(ctx) {
199
201
  }
200
202
 
201
203
  // A non-empty title attribute is HTML-AAM's own next fallback naming
202
- // source once alt is entirely absent -- also accepted by a widely-used
203
- // reference engine's equivalent area-alt rule (non-empty-title, same "any" list as
204
- // non-empty-alt/aria-label/aria-labelledby). See img-alt-present's
205
- // sibling fix (2026-07-23, AliExpress's title-only logo <img>) for
206
- // the real page this was found via -- same gap, same fix, different
207
- // element.
204
+ // source once alt is entirely absent. Same gap img-alt-present handles
205
+ // for <img title="..."> with no alt.
208
206
  const titleRaw = (() => {
209
207
  try {
210
208
  return el.getAttribute('title');
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -19,57 +21,19 @@
19
21
  * set already covers implicit-role elements without asserting anything
20
22
  * role-specific; scope kept deliberately narrow to avoid false positives
21
23
  * on implicit-role ARIA-in-HTML edge cases not modeled here.
22
- * - SUPPORTED_ATTRS_BY_ROLE widened 2026-07-21 (7 new roles), cross-checked
23
- * against a widely-used reference engine's own per-role `allowedAttrs`
24
- * table AND verified each addition is an unambiguous, well-established ARIA
25
- * fact rather than a blind import:
26
- * - `searchbox`: identical to the already-covered `textbox` (ARIA
27
- * explicitly defines searchbox as textbox's subclass, same supported
28
- * set).
29
- * - `meter`: valuenow/valuemax/valuemin/valuetext, matching the
30
- * already-covered slider/progressbar/scrollbar/spinbutton pattern
31
- * (valuenow is also separately *required* on meter — see
32
- * `aria-required-attr`'s own 2026-07-21 widening — but the two tables
33
- * aren't unioned automatically, so it needs listing in both places).
34
- * - `columnheader`/`rowheader`: same table-role family already covered
35
- * by `gridcell`/`row` — sort/col-row-index/span are standard WAI-ARIA
36
- * grid-model properties.
37
- * - `dialog`/`alertdialog`: `aria-modal` — the spec-defining property
38
- * that distinguishes a modal from a non-modal dialog; unambiguous.
39
- * - `listitem`: level/posinset/setsize — standard flat-representation-
40
- * of-hierarchy properties, same family as the already-covered
41
- * `treeitem`.
42
- * - `menu`/`menubar`/`toolbar`: activedescendant/orientation — standard
43
- * composite-widget properties, same family as the already-covered
44
- * `tablist`.
45
- * - SUPPORTED_ATTRS_BY_ROLE full-reconciliation pass 2026-07-28: the
46
- * 2026-07-21 pass deliberately deferred ~61 roles where a widely-used
47
- * reference engine allows `aria-expanded`, treating it as too thin/
48
- * near-universal to import blindly. Checked against `aria-query`
49
- * (tracks the published WAI-ARIA 1.2 Recommendation, 6 June 2023 — the
50
- * latest actually-published version, as opposed to the in-progress 1.3
51
- * Editor's Draft) instead of that reference engine directly, because its own
52
- * source comments (`// Spec difference: Aria-expanded removed in 1.2`)
53
- * show that most of its `aria-expanded` allowances are deliberate
54
- * ARIA-1.1 legacy/AT-compat carryovers it keeps on purpose, not
55
- * current-spec facts — importing them wholesale would have re-added
56
- * exactly the kind of unverified allowance the 07-21 pass was avoiding.
57
- * `aria-query`'s per-role tables (with superclass inheritance resolved,
58
- * e.g. `aria-activedescendant` via the abstract `composite` role) gave a
59
- * spec-grounded diff instead. Net changes from that diff:
60
- * - `aria-expanded` added to: checkbox, columnheader, gridcell, listbox,
61
- * menuitemcheckbox, menuitemradio, row, rowheader, switch, tab —
62
- * confirmed present in current spec for these roles specifically
63
- * (unlike e.g. `listitem`, `dialog`, `alertdialog`, `heading`, which
64
- * the spec does NOT list it for — those stay excluded).
65
- * - `aria-activedescendant` added to: combobox, grid, listbox,
66
- * radiogroup, row, spinbutton, tablist, treegrid (inherited from the
67
- * abstract `composite` role for any composite/managed-focus widget).
68
- * - `aria-readonly`/`aria-required` added to: switch, menuitemcheckbox,
69
- * menuitemradio. `aria-posinset`/`aria-setsize` added to: tab, radio,
70
- * menuitemcheckbox, menuitemradio. `aria-level` added to: tablist.
71
- * - `tree`'s `aria-readonly` removed: not in aria-query's resolved
72
- * props for `tree` (was an unverified carryover, not spec-backed).
24
+ * - SUPPORTED_ATTRS_BY_ROLE holds unambiguous, well-established ARIA facts:
25
+ * subclass relationships like `searchbox`==`textbox`; same-family widget
26
+ * properties like `columnheader`/`rowheader` sort/col-row-index/span
27
+ * matching `gridcell`/`row`; spec-defining properties like `dialog`/
28
+ * `alertdialog` `aria-modal`. `aria-expanded` entries are checked against
29
+ * `aria-query` (which tracks the published WAI-ARIA 1.2 Recommendation,
30
+ * with superclass inheritance resolved, e.g. `aria-activedescendant` via
31
+ * the abstract `composite` role) rather than imported wholesale, since
32
+ * much of the wider ARIA-1.1 `aria-expanded` allowance list is legacy/
33
+ * AT-compat carryover, not current-spec fact.
34
+ * `meter`'s valuenow/valuemax/valuemin/valuetext is also separately
35
+ * *required* (see `aria-required-attr`), but the two tables aren't
36
+ * unioned automatically, so it's listed in both places.
73
37
  * - Not rule-gated on isAccTreeEligible: this remains a static-markup
74
38
  * property, while engine-level hidden-subtree filtering still applies
75
39
  * 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
  /**
@@ -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
  /**
@@ -16,13 +18,12 @@
16
18
  * `"false"` silently drops the error message from the accessibility
17
19
  * tree — authors almost always intend it to be exposed.
18
20
  * @implementation-notes
19
- * - This is deliberately narrow: a widely-used reference engine's own
20
- * `aria-conditional-attr` rule covers a broader table of attribute/condition
21
- * pairs. This rule implements only the one pairing (`aria-errormessage` /
22
- * `aria-invalid`) that is unambiguous and explicitly stated in the
23
- * ARIA spec, to keep `fail` high-confidence — matches this repo's
24
- * established pattern (see `aria-required-attr`/`aria-prohibited-attr`
25
- * for the same "narrower than that reference engine, but zero false positives"
21
+ * - This is deliberately narrow: the broader space is a table of many
22
+ * attribute/condition pairs. This rule implements only the one pairing
23
+ * (`aria-errormessage` / `aria-invalid`) that is unambiguous and
24
+ * explicitly stated in the ARIA spec, to keep `fail` high-confidence —
25
+ * matches this repo's established pattern (see `aria-required-attr`/
26
+ * `aria-prohibited-attr` for the same "narrow but zero false positives"
26
27
  * trade-off).
27
28
  * - Does not check whether the `aria-errormessage` ID reference itself
28
29
  * resolves to an existing element — that is `aria-valid-attr-value`'s
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -12,9 +14,8 @@
12
14
  * declare — either because WAI-ARIA has deprecated it (e.g. "directory",
13
15
  * superseded by role="list") or because it's reserved for user-agent-
14
16
  * internal use only, not a spec deprecation but the same "valid token,
15
- * prohibited for authors" shape (role="generic" — verified 2026-07-20
16
- * against both the WAI-ARIA 1.2 spec text and MDN's explicit "It should
17
- * not be used by web authors" guidance).
17
+ * prohibited for authors" shape (role="generic" — per WAI-ARIA 1.2 and
18
+ * MDN's "It should not be used by web authors" guidance).
18
19
  * @expectation
19
20
  * The role in use is neither deprecated nor author-prohibited. This is a
20
21
  * distinct, atomic decision from aria-roles-valid (existence/
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -14,10 +16,10 @@
14
16
  * @expectation
15
17
  * <body> does not have aria-hidden="true". Hiding the document body
16
18
  * removes the entire page's content and structure from the
17
- * accessibility tree at once — a widely-used reference engine tags this under both 1.3.1
18
- * (Info and Relationships: the page's structure becomes entirely
19
- * non-determinable) and 4.1.2 (Name, Role, Value: nothing in the
20
- * document exposes a role/name/value any longer).
19
+ * accessibility tree at once — both 1.3.1 (Info and Relationships: the
20
+ * page's structure becomes entirely non-determinable) and 4.1.2 (Name,
21
+ * Role, Value: nothing in the document exposes a role/name/value any
22
+ * longer) apply.
21
23
  */
22
24
 
23
25
  const id = 'aria-hidden-body';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -12,7 +14,7 @@
12
14
  * No element with aria-hidden="true" may itself be focusable, and no focusable element
13
15
  * may exist within an aria-hidden="true" subtree.
14
16
  *
15
- * Notes on parity with a widely-used reference engine's aria-hidden-focus check:
17
+ * Notes:
16
18
  * - Focusability is computed via ctx.helpers.getFocusableInfo (native + tabindex + contenteditable).
17
19
  * - Elements that are not rendered (e.g., display:none, visibility:hidden, [hidden]) are excluded.
18
20
  * - Elements hidden via CSS in ways that still allow keyboard focus (e.g., opacity:0, off-screen, clip)
@@ -485,14 +487,11 @@ function runInPage(ctx) {
485
487
  // An explicit negative tabindex removes the element from the keyboard
486
488
  // tab sequence entirely, regardless of tag — the standard, WAI-
487
489
  // recommended technique for safely hiding focusable content behind
488
- // aria-hidden (verified against a widely-used reference engine's own aria-hidden-focus check,
489
- // which requires tabbability — not raw focusability — via its
490
- // `focusable-not-tabbable` sub-check; confirmed via a real page:
491
- // Wikipedia's sticky header uses <button tabindex="-1">/<a tabindex="-1">
492
- // inside aria-hidden divs, a correct pattern this rule was previously
493
- // flagging as a false positive). Such an element is still
494
- // programmatically focusable (script could call .focus()), but that's
495
- // not what "no focusable content behind aria-hidden" cares about.
490
+ // aria-hidden (e.g. <button tabindex="-1"> / <a tabindex="-1"> inside
491
+ // an aria-hidden container). This cares about tabbability, not raw
492
+ // focusability. Such an element is still programmatically focusable
493
+ // (script could call .focus()), but that's not what "no focusable
494
+ // content behind aria-hidden" cares about.
496
495
  const explicitTabindex = trim(el.getAttribute('tabindex'));
497
496
  if (
498
497
  explicitTabindex !== '' &&
@@ -640,10 +639,9 @@ function runInPage(ctx) {
640
639
  // cheaper first cannot change which elements end up in the bucket —
641
640
  // it only skips the expensive check for the (typically vast) majority
642
641
  // of focusable candidates that were never inside an aria-hidden root
643
- // in the first place. On a real page with a large focusable-candidate
644
- // count and a complex stylesheet (Daily Mail: ~1400 links, ~3600 CSS
645
- // rules), this cut this check's runtime from ~30s to well under 1s —
646
- // a pure ordering change, not a behavior change.
642
+ // in the first place. On pages with many focusable candidates and a
643
+ // large stylesheet this is a big speedup, and a pure ordering change,
644
+ // not a behavior change.
647
645
  const rootEl = closestAriaHiddenTrue(el);
648
646
  if (!rootEl) continue;
649
647
 
@@ -777,6 +775,7 @@ function runInPage(ctx) {
777
775
  hint: downgradedToCantTell
778
776
  ? 'Verify this is an intentional focus sentinel/focus-trap handoff and that keyboard users never remain on hidden focus targets.'
779
777
  : 'Remove focusability from descendants or remove aria-hidden; ensure focus and accessibility trees stay aligned.',
778
+ occurrenceOutcome: downgradedToCantTell ? 'cantTell' : 'fail',
780
779
  i18n: {
781
780
  summaryKey: downgradedToCantTell
782
781
  ? 'ariaHidden_focus_summary_cantTell_redirect'