@surea11y/core 1.2.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 (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. 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
  /**
@@ -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 the reference
47
- * engine (axe-core) 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 axe-core directly, because axe-core's 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 axe-core 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.
@@ -79,7 +43,8 @@ const id = 'aria-allowed-attr';
79
43
 
80
44
  const meta = {
81
45
  title: 'aria-* attributes must be permitted for the element’s role',
82
- description: 'Checks that every recognized aria-* attribute present on an element with an explicit role is either globally supported or supported by that role.',
46
+ description:
47
+ 'Checks that every recognized aria-* attribute present on an element with an explicit role is either globally supported or supported by that role.',
83
48
  i18n: {
84
49
  titleKey: 'ariaAllowedAttr_title',
85
50
  descriptionKey: 'ariaAllowedAttr_description'
@@ -88,7 +53,13 @@ const meta = {
88
53
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
89
54
  wcagSc: ['4.1.2'],
90
55
  normativeMappings: [
91
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
56
+ {
57
+ standard: 'WCAG',
58
+ version: '2.2',
59
+ requirement: '4.1.2',
60
+ title: 'Name, Role, Value',
61
+ conformanceLevel: 'A'
62
+ }
92
63
  ],
93
64
  defaultSeverity: 'moderate',
94
65
  category: 'robust',
@@ -98,7 +69,7 @@ const meta = {
98
69
  };
99
70
 
100
71
  function runInPage(ctx) {
101
- const { document, helpers, rule } = ctx;
72
+ const { helpers, rule } = ctx;
102
73
 
103
74
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
104
75
  if (!ariaHelpers) {
@@ -111,12 +82,30 @@ function runInPage(ctx) {
111
82
  // build inlines only this function's own source text — see
112
83
  // scripts/build-core.js header ("runInPage MUST be self-contained").
113
84
  const GLOBAL_ATTRS = [
114
- 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy',
115
- 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description',
116
- 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage',
117
- 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid',
118
- 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live', 'aria-owns',
119
- 'aria-relevant', 'aria-roledescription'
85
+ 'aria-atomic',
86
+ 'aria-braillelabel',
87
+ 'aria-brailleroledescription',
88
+ 'aria-busy',
89
+ 'aria-controls',
90
+ 'aria-current',
91
+ 'aria-describedby',
92
+ 'aria-description',
93
+ 'aria-details',
94
+ 'aria-disabled',
95
+ 'aria-dropeffect',
96
+ 'aria-errormessage',
97
+ 'aria-flowto',
98
+ 'aria-grabbed',
99
+ 'aria-haspopup',
100
+ 'aria-hidden',
101
+ 'aria-invalid',
102
+ 'aria-keyshortcuts',
103
+ 'aria-label',
104
+ 'aria-labelledby',
105
+ 'aria-live',
106
+ 'aria-owns',
107
+ 'aria-relevant',
108
+ 'aria-roledescription'
120
109
  ];
121
110
 
122
111
  // Per-role supported (non-global) states/properties. Deliberately
@@ -126,43 +115,173 @@ function runInPage(ctx) {
126
115
  const SUPPORTED_ATTRS_BY_ROLE = {
127
116
  alertdialog: ['aria-modal'],
128
117
  checkbox: ['aria-checked', 'aria-readonly', 'aria-required', 'aria-expanded'],
129
- columnheader: ['aria-sort', 'aria-colindex', 'aria-colspan', 'aria-readonly', 'aria-required', 'aria-rowindex', 'aria-rowspan', 'aria-selected', 'aria-expanded'],
130
- combobox: ['aria-expanded', 'aria-autocomplete', 'aria-readonly', 'aria-required', 'aria-activedescendant'],
118
+ columnheader: [
119
+ 'aria-sort',
120
+ 'aria-colindex',
121
+ 'aria-colspan',
122
+ 'aria-readonly',
123
+ 'aria-required',
124
+ 'aria-rowindex',
125
+ 'aria-rowspan',
126
+ 'aria-selected',
127
+ 'aria-expanded'
128
+ ],
129
+ combobox: [
130
+ 'aria-expanded',
131
+ 'aria-autocomplete',
132
+ 'aria-readonly',
133
+ 'aria-required',
134
+ 'aria-activedescendant'
135
+ ],
131
136
  dialog: ['aria-modal'],
132
- grid: ['aria-multiselectable', 'aria-readonly', 'aria-colcount', 'aria-rowcount', 'aria-activedescendant'],
133
- gridcell: ['aria-selected', 'aria-readonly', 'aria-required', 'aria-colindex', 'aria-colspan', 'aria-rowindex', 'aria-rowspan', 'aria-expanded'],
137
+ grid: [
138
+ 'aria-multiselectable',
139
+ 'aria-readonly',
140
+ 'aria-colcount',
141
+ 'aria-rowcount',
142
+ 'aria-activedescendant'
143
+ ],
144
+ gridcell: [
145
+ 'aria-selected',
146
+ 'aria-readonly',
147
+ 'aria-required',
148
+ 'aria-colindex',
149
+ 'aria-colspan',
150
+ 'aria-rowindex',
151
+ 'aria-rowspan',
152
+ 'aria-expanded'
153
+ ],
134
154
  heading: ['aria-level'],
135
- listbox: ['aria-multiselectable', 'aria-readonly', 'aria-required', 'aria-orientation', 'aria-expanded', 'aria-activedescendant'],
155
+ listbox: [
156
+ 'aria-multiselectable',
157
+ 'aria-readonly',
158
+ 'aria-required',
159
+ 'aria-orientation',
160
+ 'aria-expanded',
161
+ 'aria-activedescendant'
162
+ ],
136
163
  listitem: ['aria-level', 'aria-posinset', 'aria-setsize'],
137
164
  menu: ['aria-activedescendant', 'aria-orientation'],
138
165
  menubar: ['aria-activedescendant', 'aria-orientation'],
139
- menuitemcheckbox: ['aria-checked', 'aria-expanded', 'aria-readonly', 'aria-required', 'aria-posinset', 'aria-setsize'],
140
- menuitemradio: ['aria-checked', 'aria-expanded', 'aria-readonly', 'aria-required', 'aria-posinset', 'aria-setsize'],
166
+ menuitemcheckbox: [
167
+ 'aria-checked',
168
+ 'aria-expanded',
169
+ 'aria-readonly',
170
+ 'aria-required',
171
+ 'aria-posinset',
172
+ 'aria-setsize'
173
+ ],
174
+ menuitemradio: [
175
+ 'aria-checked',
176
+ 'aria-expanded',
177
+ 'aria-readonly',
178
+ 'aria-required',
179
+ 'aria-posinset',
180
+ 'aria-setsize'
181
+ ],
141
182
  meter: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext'],
142
183
  option: ['aria-selected', 'aria-checked', 'aria-posinset', 'aria-setsize'],
143
184
  progressbar: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext'],
144
185
  radio: ['aria-checked', 'aria-posinset', 'aria-setsize'],
145
186
  radiogroup: ['aria-readonly', 'aria-required', 'aria-orientation', 'aria-activedescendant'],
146
- row: ['aria-selected', 'aria-level', 'aria-posinset', 'aria-setsize', 'aria-colindex', 'aria-rowindex', 'aria-expanded', 'aria-activedescendant'],
147
- rowheader: ['aria-sort', 'aria-colindex', 'aria-colspan', 'aria-readonly', 'aria-required', 'aria-rowindex', 'aria-rowspan', 'aria-selected', 'aria-expanded'],
148
- scrollbar: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-orientation', 'aria-controls'],
149
- searchbox: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-placeholder', 'aria-readonly', 'aria-required'],
150
- separator: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-orientation'],
151
- slider: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-orientation', 'aria-readonly'],
152
- spinbutton: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext', 'aria-readonly', 'aria-required', 'aria-activedescendant'],
187
+ row: [
188
+ 'aria-selected',
189
+ 'aria-level',
190
+ 'aria-posinset',
191
+ 'aria-setsize',
192
+ 'aria-colindex',
193
+ 'aria-rowindex',
194
+ 'aria-expanded',
195
+ 'aria-activedescendant'
196
+ ],
197
+ rowheader: [
198
+ 'aria-sort',
199
+ 'aria-colindex',
200
+ 'aria-colspan',
201
+ 'aria-readonly',
202
+ 'aria-required',
203
+ 'aria-rowindex',
204
+ 'aria-rowspan',
205
+ 'aria-selected',
206
+ 'aria-expanded'
207
+ ],
208
+ scrollbar: [
209
+ 'aria-valuenow',
210
+ 'aria-valuemin',
211
+ 'aria-valuemax',
212
+ 'aria-valuetext',
213
+ 'aria-orientation',
214
+ 'aria-controls'
215
+ ],
216
+ searchbox: [
217
+ 'aria-activedescendant',
218
+ 'aria-autocomplete',
219
+ 'aria-multiline',
220
+ 'aria-placeholder',
221
+ 'aria-readonly',
222
+ 'aria-required'
223
+ ],
224
+ separator: [
225
+ 'aria-valuenow',
226
+ 'aria-valuemin',
227
+ 'aria-valuemax',
228
+ 'aria-valuetext',
229
+ 'aria-orientation'
230
+ ],
231
+ slider: [
232
+ 'aria-valuenow',
233
+ 'aria-valuemin',
234
+ 'aria-valuemax',
235
+ 'aria-valuetext',
236
+ 'aria-orientation',
237
+ 'aria-readonly'
238
+ ],
239
+ spinbutton: [
240
+ 'aria-valuenow',
241
+ 'aria-valuemin',
242
+ 'aria-valuemax',
243
+ 'aria-valuetext',
244
+ 'aria-readonly',
245
+ 'aria-required',
246
+ 'aria-activedescendant'
247
+ ],
153
248
  switch: ['aria-checked', 'aria-expanded', 'aria-readonly', 'aria-required'],
154
249
  tab: ['aria-selected', 'aria-expanded', 'aria-posinset', 'aria-setsize'],
155
250
  table: ['aria-colcount', 'aria-rowcount'],
156
251
  tablist: ['aria-multiselectable', 'aria-orientation', 'aria-level', 'aria-activedescendant'],
157
- textbox: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-placeholder', 'aria-readonly', 'aria-required'],
252
+ textbox: [
253
+ 'aria-activedescendant',
254
+ 'aria-autocomplete',
255
+ 'aria-multiline',
256
+ 'aria-placeholder',
257
+ 'aria-readonly',
258
+ 'aria-required'
259
+ ],
158
260
  toolbar: ['aria-activedescendant', 'aria-orientation'],
159
261
  tree: ['aria-multiselectable', 'aria-required', 'aria-orientation', 'aria-activedescendant'],
160
- treegrid: ['aria-multiselectable', 'aria-readonly', 'aria-required', 'aria-orientation', 'aria-colcount', 'aria-rowcount', 'aria-activedescendant'],
161
- treeitem: ['aria-checked', 'aria-selected', 'aria-expanded', 'aria-level', 'aria-posinset', 'aria-setsize']
262
+ treegrid: [
263
+ 'aria-multiselectable',
264
+ 'aria-readonly',
265
+ 'aria-required',
266
+ 'aria-orientation',
267
+ 'aria-colcount',
268
+ 'aria-rowcount',
269
+ 'aria-activedescendant'
270
+ ],
271
+ treeitem: [
272
+ 'aria-checked',
273
+ 'aria-selected',
274
+ 'aria-expanded',
275
+ 'aria-level',
276
+ 'aria-posinset',
277
+ 'aria-setsize'
278
+ ]
162
279
  };
163
280
 
164
281
  const globalSet = new Set(GLOBAL_ATTRS);
165
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
282
+ const nodes = helpers.queryAllSmart
283
+ ? helpers.queryAllSmart('[role]')
284
+ : helpers.queryAll('[role]');
166
285
 
167
286
  const occurrences = [];
168
287
  let applicableCount = 0;
@@ -198,7 +317,7 @@ function runInPage(ctx) {
198
317
  if (!disallowed || !disallowed.length) continue;
199
318
 
200
319
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
201
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
320
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
202
321
 
203
322
  for (const name of disallowed) {
204
323
  occurrences.push({
@@ -222,7 +341,12 @@ function runInPage(ctx) {
222
341
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
223
342
  }
224
343
  if (occurrences.length) {
225
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
344
+ return {
345
+ ruleId: rule.ruleId,
346
+ outcome: 'fail',
347
+ severity: rule.defaultSeverity || 'moderate',
348
+ occurrences
349
+ };
226
350
  }
227
351
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
228
352
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -27,7 +29,8 @@ const id = 'aria-allowed-role';
27
29
 
28
30
  const meta = {
29
31
  title: 'Explicit role must be permitted for its host element',
30
- description: 'Checks that an explicit role="" attribute is one of the roles the ARIA-in-HTML specification permits for the host element (e.g. role="tab" is not permitted on <nav>).',
32
+ description:
33
+ 'Checks that an explicit role="" attribute is one of the roles the ARIA-in-HTML specification permits for the host element (e.g. role="tab" is not permitted on <nav>).',
31
34
  i18n: {
32
35
  titleKey: 'ariaAllowedRole_title',
33
36
  descriptionKey: 'ariaAllowedRole_description'
@@ -36,7 +39,13 @@ const meta = {
36
39
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
37
40
  wcagSc: ['4.1.2'],
38
41
  normativeMappings: [
39
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
42
+ {
43
+ standard: 'WCAG',
44
+ version: '2.2',
45
+ requirement: '4.1.2',
46
+ title: 'Name, Role, Value',
47
+ conformanceLevel: 'A'
48
+ }
40
49
  ],
41
50
  defaultSeverity: 'moderate',
42
51
  category: 'robust',
@@ -46,14 +55,16 @@ const meta = {
46
55
  };
47
56
 
48
57
  function runInPage(ctx) {
49
- const { document, helpers, rule } = ctx;
58
+ const { helpers, rule } = ctx;
50
59
 
51
60
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
52
61
  if (!ariaHelpers) {
53
62
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
54
63
  }
55
64
 
56
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
65
+ const nodes = helpers.queryAllSmart
66
+ ? helpers.queryAllSmart('[role]')
67
+ : helpers.queryAll('[role]');
57
68
 
58
69
  const occurrences = [];
59
70
  let applicableCount = 0;
@@ -72,7 +83,7 @@ function runInPage(ctx) {
72
83
  if (info.allowed) continue;
73
84
 
74
85
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
75
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
86
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
76
87
  const tag = (el.tagName || '').toLowerCase();
77
88
 
78
89
  occurrences.push({
@@ -95,9 +106,14 @@ function runInPage(ctx) {
95
106
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
96
107
  }
97
108
  if (occurrences.length) {
98
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
109
+ return {
110
+ ruleId: rule.ruleId,
111
+ outcome: 'fail',
112
+ severity: rule.defaultSeverity || 'moderate',
113
+ occurrences
114
+ };
99
115
  }
100
116
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
101
117
  }
102
118
 
103
- module.exports = { id, meta, runInPage };
119
+ module.exports = { id, meta, runInPage };
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -42,7 +44,13 @@ const meta = {
42
44
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
43
45
  wcagSc: ['4.1.2'],
44
46
  normativeMappings: [
45
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
47
+ {
48
+ standard: 'WCAG',
49
+ version: '2.2',
50
+ requirement: '4.1.2',
51
+ title: 'Name, Role, Value',
52
+ conformanceLevel: 'A'
53
+ }
46
54
  ],
47
55
  defaultSeverity: 'serious',
48
56
  category: 'robust',
@@ -52,9 +60,11 @@ const meta = {
52
60
  };
53
61
 
54
62
  function runInPage(ctx) {
55
- const { document, helpers, rule } = ctx;
63
+ const { helpers, rule } = ctx;
56
64
 
57
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
65
+ function trim(v) {
66
+ return (v == null ? '' : String(v)).trim();
67
+ }
58
68
 
59
69
  function getConservativeSubtreeText(container) {
60
70
  // "Name from content" — recurses into descendants and uses each one's
@@ -68,7 +78,7 @@ function runInPage(ctx) {
68
78
  const info = helpers.getContentNameInfo(container, ctx);
69
79
  return info && info.present ? info.value : '';
70
80
  }
71
- const t = (container && container.textContent) ? String(container.textContent) : '';
81
+ const t = container && container.textContent ? String(container.textContent) : '';
72
82
  return t.replace(/\s+/g, ' ').trim();
73
83
  }
74
84
 
@@ -91,21 +101,26 @@ function runInPage(ctx) {
91
101
  const missing = [];
92
102
 
93
103
  if (brailleLabel) {
94
- const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
95
- const programmaticName = trim(nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '');
104
+ const nameInfo = helpers.getAccessibleNameInfo
105
+ ? helpers.getAccessibleNameInfo(el, ctx)
106
+ : null;
107
+ const programmaticName = trim(
108
+ nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : ''
109
+ );
96
110
  const name = programmaticName || getConservativeSubtreeText(el);
97
111
  if (!name) missing.push({ attr: 'aria-braillelabel', requires: 'an accessible name' });
98
112
  }
99
113
 
100
114
  if (brailleRoleDesc) {
101
115
  const roleDesc = trim(el.getAttribute('aria-roledescription'));
102
- if (!roleDesc) missing.push({ attr: 'aria-brailleroledescription', requires: 'aria-roledescription' });
116
+ if (!roleDesc)
117
+ missing.push({ attr: 'aria-brailleroledescription', requires: 'aria-roledescription' });
103
118
  }
104
119
 
105
120
  if (!missing.length) continue;
106
121
 
107
122
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
108
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
123
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
109
124
  const tag = (el.tagName || '').toLowerCase();
110
125
 
111
126
  for (const m of missing) {
@@ -120,7 +135,11 @@ function runInPage(ctx) {
120
135
  params: { element: tag, attr: m.attr, requires: m.requires }
121
136
  },
122
137
  data: {
123
- details: { reasonCode: 'BRAILLE_ATTR_WITHOUT_EQUIVALENT', attr: m.attr, requires: m.requires }
138
+ details: {
139
+ reasonCode: 'BRAILLE_ATTR_WITHOUT_EQUIVALENT',
140
+ attr: m.attr,
141
+ requires: m.requires
142
+ }
124
143
  }
125
144
  });
126
145
  }
@@ -130,7 +149,12 @@ function runInPage(ctx) {
130
149
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
131
150
  }
132
151
  if (occurrences.length) {
133
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
152
+ return {
153
+ ruleId: rule.ruleId,
154
+ outcome: 'fail',
155
+ severity: rule.defaultSeverity || 'serious',
156
+ occurrences
157
+ };
134
158
  }
135
159
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
136
160
  }
@@ -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
@@ -43,7 +44,13 @@ const meta = {
43
44
  tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
44
45
  wcagSc: ['4.1.2'],
45
46
  normativeMappings: [
46
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
47
+ {
48
+ standard: 'WCAG',
49
+ version: '2.2',
50
+ requirement: '4.1.2',
51
+ title: 'Name, Role, Value',
52
+ conformanceLevel: 'A'
53
+ }
47
54
  ],
48
55
  defaultSeverity: 'serious',
49
56
  category: 'robust',
@@ -53,9 +60,11 @@ const meta = {
53
60
  };
54
61
 
55
62
  function runInPage(ctx) {
56
- const { document, helpers, rule } = ctx;
63
+ const { helpers, rule } = ctx;
57
64
 
58
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
65
+ function trim(v) {
66
+ return (v == null ? '' : String(v)).trim();
67
+ }
59
68
 
60
69
  const TRUTHY_INVALID_VALUES = new Set(['true', 'grammar', 'spelling']);
61
70
 
@@ -78,13 +87,14 @@ function runInPage(ctx) {
78
87
  if (TRUTHY_INVALID_VALUES.has(invalidValue)) continue;
79
88
 
80
89
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
81
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
90
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
82
91
  const tag = (el.tagName || '').toLowerCase();
83
92
 
84
93
  occurrences.push({
85
94
  selector: stableSelector,
86
95
  html,
87
- summary: 'This element has aria-errormessage but aria-invalid is missing or "false", so the error message is not exposed.',
96
+ summary:
97
+ 'This element has aria-errormessage but aria-invalid is missing or "false", so the error message is not exposed.',
88
98
  hint: 'Set aria-invalid to "true" (or "grammar"/"spelling") whenever aria-errormessage should be exposed to assistive technology.',
89
99
  i18n: {
90
100
  summaryKey: 'ariaConditionalAttr_summary_fail',
@@ -92,7 +102,10 @@ function runInPage(ctx) {
92
102
  params: { element: tag, ariaInvalid: invalidValue || '(absent)' }
93
103
  },
94
104
  data: {
95
- details: { reasonCode: 'ARIA_ERRORMESSAGE_WITHOUT_TRUTHY_INVALID', ariaInvalid: invalidValue }
105
+ details: {
106
+ reasonCode: 'ARIA_ERRORMESSAGE_WITHOUT_TRUTHY_INVALID',
107
+ ariaInvalid: invalidValue
108
+ }
96
109
  }
97
110
  });
98
111
  }
@@ -101,7 +114,12 @@ function runInPage(ctx) {
101
114
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
102
115
  }
103
116
  if (occurrences.length) {
104
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
117
+ return {
118
+ ruleId: rule.ruleId,
119
+ outcome: 'fail',
120
+ severity: rule.defaultSeverity || 'serious',
121
+ occurrences
122
+ };
105
123
  }
106
124
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
107
125
  }