@surea11y/core 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +212 -128
  2. package/README.md +46 -9
  3. package/docs/ACT_RULE_MAPPING.md +243 -0
  4. package/docs/API_STABILITY.md +4 -4
  5. package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
  6. package/docs/DESIGN_CHALLENGES.md +301 -0
  7. package/docs/ENGINE_OPTIONS.md +30 -14
  8. package/docs/I18N.md +176 -20
  9. package/docs/INTEGRATION.md +29 -7
  10. package/docs/LIMITATIONS.md +6 -4
  11. package/docs/OUTPUT_SCHEMA.md +13 -3
  12. package/docs/REPORT.md +3 -1
  13. package/docs/RULE_AUTHORING.md +104 -23
  14. package/docs/RULE_CATALOG.md +1878 -169
  15. package/docs/RULE_TAXONOMY.md +2 -2
  16. package/docs/TROUBLESHOOTING.md +4 -4
  17. package/docs/WCAG_CONFORMANCE.md +25 -9
  18. package/package.json +8 -7
  19. package/src/baseline.js +3 -3
  20. package/src/checks/automatic/area-alt-present.js +2 -2
  21. package/src/checks/automatic/aria-allowed-attr.js +95 -40
  22. package/src/checks/automatic/aria-allowed-role.js +16 -18
  23. package/src/checks/automatic/aria-braille-equivalent.js +19 -21
  24. package/src/checks/automatic/aria-conditional-attr.js +22 -24
  25. package/src/checks/automatic/aria-deprecated-role.js +63 -50
  26. package/src/checks/automatic/aria-hidden-body.js +4 -11
  27. package/src/checks/automatic/aria-hidden-focus.js +104 -23
  28. package/src/checks/automatic/aria-prohibited-attr.js +71 -72
  29. package/src/checks/automatic/aria-prohibited-children.js +154 -61
  30. package/src/checks/automatic/aria-required-attr.js +74 -29
  31. package/src/checks/automatic/aria-required-children.js +38 -34
  32. package/src/checks/automatic/aria-required-parent.js +78 -29
  33. package/src/checks/automatic/aria-role-name-present.js +36 -22
  34. package/src/checks/automatic/aria-roles-valid.js +37 -23
  35. package/src/checks/automatic/aria-valid-attr-value.js +33 -33
  36. package/src/checks/automatic/aria-valid-attr.js +15 -18
  37. package/src/checks/automatic/autocomplete-valid.js +17 -19
  38. package/src/checks/automatic/avoid-inline-spacing.js +14 -16
  39. package/src/checks/automatic/binary-control-name-present.js +46 -26
  40. package/src/checks/automatic/button-name-present.js +115 -34
  41. package/src/checks/automatic/combobox-name-present.js +40 -22
  42. package/src/checks/automatic/contrast-computable.js +32 -0
  43. package/src/checks/automatic/contrast-enhanced.js +21 -1
  44. package/src/checks/automatic/contrast-minimum.js +21 -1
  45. package/src/checks/automatic/css-orientation-lock.js +118 -41
  46. package/src/checks/automatic/definition-list-children-valid.js +25 -29
  47. package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
  48. package/src/checks/automatic/dialog-name-present.js +36 -20
  49. package/src/checks/automatic/dlitem-parent-valid.js +15 -17
  50. package/src/checks/automatic/duplicate-id-aria.js +50 -40
  51. package/src/checks/automatic/duplicate-id.js +198 -0
  52. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  53. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  54. package/src/checks/automatic/form-control-single-label.js +39 -41
  55. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
  56. package/src/checks/automatic/iframe-focusable-content.js +92 -38
  57. package/src/checks/automatic/iframe-name-present.js +53 -21
  58. package/src/checks/automatic/iframe-title-unique.js +19 -24
  59. package/src/checks/automatic/img-alt-present.js +12 -4
  60. package/src/checks/automatic/label-in-name.js +198 -49
  61. package/src/checks/automatic/link-in-text-block.js +29 -31
  62. package/src/checks/automatic/link-name-present.js +47 -31
  63. package/src/checks/automatic/list-children-valid.js +21 -23
  64. package/src/checks/automatic/listbox-name-present.js +42 -24
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -21
  66. package/src/checks/automatic/menuitem-name-present.js +36 -20
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
  70. package/src/checks/automatic/meter-name-present.js +38 -21
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
  72. package/src/checks/automatic/option-name-present.js +39 -22
  73. package/src/checks/automatic/page-title-present.js +21 -3
  74. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  75. package/src/checks/automatic/progressbar-name-present.js +41 -24
  76. package/src/checks/automatic/role-img-alt-present.js +64 -16
  77. package/src/checks/automatic/searchbox-name-present.js +46 -24
  78. package/src/checks/automatic/server-side-image-map-absent.js +16 -19
  79. package/src/checks/automatic/slider-name-present.js +42 -23
  80. package/src/checks/automatic/spinbutton-name-present.js +46 -24
  81. package/src/checks/automatic/summary-name-present.js +34 -20
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  83. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  84. package/src/checks/automatic/tab-name-present.js +37 -20
  85. package/src/checks/automatic/table-headers-attr-valid.js +57 -24
  86. package/src/checks/automatic/table-th-has-data-cells.js +76 -24
  87. package/src/checks/automatic/target-size-minimum.js +172 -131
  88. package/src/checks/automatic/td-has-header.js +20 -25
  89. package/src/checks/automatic/textbox-name-present.js +42 -24
  90. package/src/checks/automatic/tooltip-name-present.js +37 -20
  91. package/src/checks/automatic/treeitem-name-present.js +39 -22
  92. package/src/checks/automatic/valid-lang.js +107 -24
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  94. package/src/checks/manual/accesskeys-manual.js +21 -22
  95. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  96. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +23 -26
  98. package/src/checks/manual/aria-text-manual.js +4 -4
  99. package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  101. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  103. package/src/checks/manual/empty-heading-manual.js +73 -28
  104. package/src/checks/manual/empty-table-header-manual.js +33 -36
  105. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  106. package/src/checks/manual/form-control-label-quality-manual.js +453 -0
  107. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +4 -11
  108. package/src/checks/manual/heading-order-manual.js +20 -25
  109. package/src/checks/manual/heading-quality-manual.js +338 -0
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +18 -21
  112. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  113. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  115. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  116. package/src/checks/manual/label-title-only-manual.js +19 -21
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
  123. package/src/checks/manual/landmark-one-main-manual.js +8 -15
  124. package/src/checks/manual/landmark-unique-manual.js +31 -36
  125. package/src/checks/manual/link-name-quality-manual.js +162 -35
  126. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  127. package/src/checks/manual/meta-viewport-large-manual.js +16 -19
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
  129. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  131. package/src/checks/manual/p-as-heading-manual.js +4 -4
  132. package/src/checks/manual/page-has-heading-one-manual.js +8 -15
  133. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  134. package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
  135. package/src/checks/manual/region-manual.js +32 -25
  136. package/src/checks/manual/scope-attr-valid-manual.js +16 -19
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  138. package/src/checks/manual/skip-link-manual.js +44 -52
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  140. package/src/checks/manual/tabindex-manual.js +16 -19
  141. package/src/checks/manual/table-duplicate-name-manual.js +16 -19
  142. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  143. package/src/checks/manual/video-caption-manual.js +3 -3
  144. package/src/checks/manual-review.js +17 -1
  145. package/src/core.js +13086 -5169
  146. package/src/report.js +16 -2
  147. package/surea11y.browser.js +5665 -4219
  148. package/surea11y.i18n.de.js +22 -0
  149. package/surea11y.i18n.es.js +22 -0
  150. package/surea11y.i18n.fr.js +22 -0
  151. package/bin/surea11y-core.js +0 -20
@@ -2,6 +2,28 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check binary-control-name-present
7
+ * @atomic true
8
+ * @summary Checkboxes, radios and switches must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="checkbox", role="radio" or
13
+ * role="switch" (the attribute must name one of those roles alone, not a
14
+ * fallback list) that are included in the accessibility tree. A native
15
+ * <input type="checkbox">/<input type="radio"> is in scope only when it
16
+ * carries one of those roles explicitly; without a role attribute it
17
+ * belongs to form-control-programmatic-label-present.
18
+ * @expectation
19
+ * The control has a non-empty accessible name from aria-label, from an
20
+ * aria-labelledby that resolves to non-empty text, or from title. A native
21
+ * checkbox or radio additionally accepts an associated <label>, the
22
+ * labels API, a wrapping <label>, or label[for], with at most four labels
23
+ * read for determinism, and any other element accepts its own subtree
24
+ * text, those roles being name-from-content.
25
+ */
26
+
5
27
  const id = 'binary-control-name-present';
6
28
 
7
29
  const meta = {
@@ -79,7 +101,7 @@ function runInPage(ctx) {
79
101
  }
80
102
 
81
103
  function getConservativeSubtreeText(document, container) {
82
- // "Name from content" — recurses into descendants and uses each one's
104
+ // "Name from content", recurses into descendants and uses each one's
83
105
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
84
106
  // it has one, not just literal text nodes. See getContentNameInfo's
85
107
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -96,7 +118,7 @@ function runInPage(ctx) {
96
118
 
97
119
  // A <label> contributes a name via its own aria-label/aria-labelledby
98
120
  // (checked first, same ARIA-over-content precedence any element's
99
- // accessible name gives — e.g. <label aria-label="Search"><svg
121
+ // accessible name gives, e.g. <label aria-label="Search"><svg
100
122
  // aria-hidden="true">...</svg></label> names its control "Search" even
101
123
  // though the label's only child content is aria-hidden) or, failing
102
124
  // that, its rendered content (getConservativeSubtreeText).
@@ -111,10 +133,10 @@ function runInPage(ctx) {
111
133
  if (content) return content;
112
134
  // Final fallback per the general accname text-alternative algorithm,
113
135
  // which applies to any element being asked for its name regardless of
114
- // why (own aria-label, an aria-labelledby reference, or — here — native
136
+ // why (own aria-label, an aria-labelledby reference, or, here, native
115
137
  // <label for> association): title, when nothing else yields a name.
116
138
  // Purely additive (only fills in a name where there was none before),
117
- // so it carries no false-positive risk — see dialog-name-present.js's
139
+ // so it carries no false-positive risk, see dialog-name-present.js's
118
140
  // identical <iframe>-title-fallback fix for the concrete real-world
119
141
  // trigger this same accname step covers elsewhere.
120
142
  return getAttr(lab, 'title');
@@ -124,7 +146,7 @@ function runInPage(ctx) {
124
146
  const raw = getAttr(el, 'aria-labelledby');
125
147
  if (!raw) return '';
126
148
  // Delegates to the shared getTextFromIdRefs helper instead of computing
127
- // name-from-content of the referenced element — see dialog-name-
149
+ // name-from-content of the referenced element, see dialog-name-
128
150
  // present.js's identical fix for the full rationale (an <iframe>
129
151
  // aria-labelledby target's only name source is its title attribute,
130
152
  // which name-from-content alone can never see).
@@ -272,28 +294,26 @@ function runInPage(ctx) {
272
294
  }
273
295
  })()
274
296
  : null;
275
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
276
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
277
-
278
- occurrences.push({
279
- selector: stableSelector,
280
- html,
281
- summary: 'This control has no accessible name.',
282
- hint: 'Provide a label, aria-label, aria-labelledby, or other accessible-name mechanism so assistive technologies can identify the control.',
283
- i18n: {
284
- summaryKey: 'binaryControlNamePresent_summary_fail',
285
- hintKey: 'binaryControlNamePresent_hint_fail',
286
- params: { controlType }
287
- },
288
- data: {
289
- details: {
290
- reasonCode: 'name_missing',
291
- controlType,
292
- methodTried: res.method
297
+
298
+ occurrences.push(
299
+ helpers.reportOccurrence(el, {
300
+ summary: 'This control has no accessible name.',
301
+ hint: 'Provide a label, aria-label, aria-labelledby, or other accessible-name mechanism so assistive technologies can identify the control.',
302
+ i18n: {
303
+ summaryKey: 'binaryControlNamePresent_summary_fail',
304
+ hintKey: 'binaryControlNamePresent_hint_fail',
305
+ params: { controlType }
293
306
  },
294
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
295
- }
296
- });
307
+ data: {
308
+ details: {
309
+ reasonCode: 'name_missing',
310
+ controlType,
311
+ methodTried: res.method
312
+ },
313
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
314
+ }
315
+ })
316
+ );
297
317
  }
298
318
 
299
319
  if (applicableCount === 0) {
@@ -2,6 +2,29 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check button-name-present
7
+ * @atomic true
8
+ * @summary Buttons must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to <button>, <input type="button">, <input type="submit">,
13
+ * <input type="reset"> and elements with role="button", where the element
14
+ * is included in the accessibility tree. role="presentation"/"none" takes
15
+ * an element out of scope unless a global ARIA attribute or focusability
16
+ * restores its role, per presentational roles conflict resolution.
17
+ * @expectation
18
+ * The element has a non-empty accessible name. A programmatic name is
19
+ * taken first (aria-labelledby, aria-label, an associated <label>, title).
20
+ * Failing that, an <input> button falls back to its value attribute, and
21
+ * type="submit"/type="reset" fall back to the user agent's own
22
+ * "Submit"/"Reset" default, which is why those two are never nameless.
23
+ * Failing both, a button whose role is name-from-content falls back to its
24
+ * subtree text, counting each descendant's own name (an <img alt>,
25
+ * aria-label or title) rather than only text nodes.
26
+ */
27
+
5
28
  // NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
6
29
  // File name intentionally has no prefix, per request.
7
30
  const id = 'button-name-present';
@@ -45,7 +68,7 @@ function runInPage(ctx) {
45
68
  }
46
69
 
47
70
  function getConservativeSubtreeText(container) {
48
- // "Name from content" — recurses into descendants and uses each one's
71
+ // "Name from content", recurses into descendants and uses each one's
49
72
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
50
73
  // it has one, not just literal text nodes. See getContentNameInfo's
51
74
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -64,7 +87,16 @@ function runInPage(ctx) {
64
87
  const type = normalizeWs(el.getAttribute ? el.getAttribute('type') : '').toLowerCase();
65
88
  if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
66
89
  const vAttr = el.getAttribute ? el.getAttribute('value') : '';
67
- return normalizeWs(vAttr != null ? vAttr : typeof el.value === 'string' ? el.value : '');
90
+ const explicit = normalizeWs(
91
+ vAttr != null ? vAttr : typeof el.value === 'string' ? el.value : ''
92
+ );
93
+ if (explicit) return explicit;
94
+ // HTML spec: input[type=submit]/[type=reset] with no `value` fall back
95
+ // to a UA-supplied default label ("Submit"/"Reset"), so they are never
96
+ // actually nameless -- unlike type=button, whose value defaults to "".
97
+ if (type === 'submit') return 'Submit';
98
+ if (type === 'reset') return 'Reset';
99
+ return '';
68
100
  } catch {
69
101
  return '';
70
102
  }
@@ -90,17 +122,71 @@ function runInPage(ctx) {
90
122
  typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
91
123
  if (!eligible) continue;
92
124
 
93
- applicableCount += 1;
94
-
95
125
  const tag = (el.tagName || '').toLowerCase();
96
126
  const role = el.getAttribute ? el.getAttribute('role') : null;
97
127
  const roleNorm = normalizeWs(role).toLowerCase();
98
128
 
129
+ // role="none"/"presentation" removes this element from the accessibility
130
+ // tree as a button (WAI-ARIA Presentational Roles Conflict Resolution),
131
+ // UNLESS a conflicting global ARIA attribute or focusability restores
132
+ // its native/explicit role -- mirrors presentation-role-conflict-manual.js's
133
+ // detection logic. Kept local to this rule rather than routed through
134
+ // the shared eligibility helper: several other rules rely
135
+ // on that helper staying permissive for role="none" wrappers they walk
136
+ // through themselves (e.g. aria-prohibited-children's "transparent
137
+ // wrapper" traversal).
138
+ if (roleNorm === 'none' || roleNorm === 'presentation') {
139
+ const ariaHiddenTrue = el.getAttribute && el.getAttribute('aria-hidden') === 'true';
140
+ if (!ariaHiddenTrue) {
141
+ const GLOBAL_ARIA_ATTRS = [
142
+ 'aria-atomic',
143
+ 'aria-braillelabel',
144
+ 'aria-brailleroledescription',
145
+ 'aria-busy',
146
+ 'aria-controls',
147
+ 'aria-current',
148
+ 'aria-describedby',
149
+ 'aria-description',
150
+ 'aria-details',
151
+ 'aria-disabled',
152
+ 'aria-dropeffect',
153
+ 'aria-errormessage',
154
+ 'aria-flowto',
155
+ 'aria-grabbed',
156
+ 'aria-haspopup',
157
+ 'aria-hidden',
158
+ 'aria-invalid',
159
+ 'aria-keyshortcuts',
160
+ 'aria-label',
161
+ 'aria-labelledby',
162
+ 'aria-live',
163
+ 'aria-owns',
164
+ 'aria-relevant',
165
+ 'aria-roledescription'
166
+ ];
167
+ const hasConflict = GLOBAL_ARIA_ATTRS.some((a) =>
168
+ el.hasAttribute ? el.hasAttribute(a) : false
169
+ );
170
+ let isFocusable = false;
171
+ if (!hasConflict && helpers.getFocusableInfo) {
172
+ try {
173
+ const fi = helpers.getFocusableInfo(el, ctx);
174
+ isFocusable = !!(fi && fi.focusable);
175
+ } catch {
176
+ isFocusable = false;
177
+ }
178
+ }
179
+ if (!hasConflict && !isFocusable) continue;
180
+ }
181
+ }
182
+
183
+ applicableCount += 1;
184
+
99
185
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
100
186
 
101
187
  // getAccessibleNameInfo only resolves programmatic mechanisms (aria-labelledby,
102
- // aria-label, native <label> association, title) — it never falls back to
103
- // subtree content — so it's safe to trust directly whenever present.
188
+ // aria-label, native <label> association, title), it never falls back to
189
+ // subtree content, so it's safe to trust directly whenever present.
104
190
  const trustedProgrammaticName = normalizeWs(
105
191
  nameInfo && nameInfo.present && typeof nameInfo.value === 'string' ? nameInfo.value : ''
106
192
  );
@@ -170,35 +256,30 @@ function runInPage(ctx) {
170
256
  ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
171
257
  : null;
172
258
 
173
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
174
- const html = helpers.getOuterHtmlSnippet
175
- ? helpers.getOuterHtmlSnippet(el)
176
- : el.outerHTML || '';
177
-
178
- occurrences.push({
179
- selector: stableSelector,
180
- html,
181
- summary: 'This button has no accessible name.',
182
- hint: 'Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.',
183
- i18n: {
184
- summaryKey: 'buttonNamePresent_summary_fail',
185
- hintKey: 'buttonNamePresent_hint_fail',
186
- params: { element: tag }
187
- },
188
- data: {
189
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
190
- details: {
191
- reasonCode: 'name_missing',
192
- metrics: {
193
- trustedProgrammaticNameLength: trustedProgrammaticName.length,
194
- inputValueNameLength: inputValueName.length,
195
- contentNameLength: contentName.length,
196
- explicitProgrammatic: explicitProg ? 1 : 0
197
- },
198
- refs: { accessibleName: nameInfo || null }
259
+ occurrences.push(
260
+ helpers.reportOccurrence(el, {
261
+ summary: 'This button has no accessible name.',
262
+ hint: 'Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.',
263
+ i18n: {
264
+ summaryKey: 'buttonNamePresent_summary_fail',
265
+ hintKey: 'buttonNamePresent_hint_fail',
266
+ params: { element: tag }
267
+ },
268
+ data: {
269
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
270
+ details: {
271
+ reasonCode: 'name_missing',
272
+ metrics: {
273
+ trustedProgrammaticNameLength: trustedProgrammaticName.length,
274
+ inputValueNameLength: inputValueName.length,
275
+ contentNameLength: contentName.length,
276
+ explicitProgrammatic: explicitProg ? 1 : 0
277
+ },
278
+ refs: { accessibleName: nameInfo || null }
279
+ }
199
280
  }
200
- }
201
- });
281
+ })
282
+ );
202
283
  }
203
284
  }
204
285
 
@@ -2,6 +2,26 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check combobox-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="combobox" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="combobox" (the attribute must name
13
+ * that role alone, not a fallback list) that are included in the
14
+ * accessibility tree. An element with the matching implicit role but no
15
+ * role attribute is out of scope.
16
+ * @expectation
17
+ * The element has a non-empty accessible name from aria-label, from an
18
+ * aria-labelledby that resolves to non-empty text, or from title.
19
+ * role="combobox" is name-from-author-only, so subtree text is never
20
+ * accepted: text sitting inside a custom combobox widget is not reliably
21
+ * exposed as its name. On a labelable element (<input role="combobox">) an
22
+ * associated <label> counts as well.
23
+ */
24
+
5
25
  const id = 'combobox-name-present';
6
26
 
7
27
  const meta = {
@@ -66,7 +86,7 @@ function runInPage(ctx) {
66
86
  }
67
87
 
68
88
  function getConservativeSubtreeText(document, container) {
69
- // "Name from content" — recurses into descendants and uses each one's
89
+ // "Name from content", recurses into descendants and uses each one's
70
90
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
71
91
  // it has one, not just literal text nodes. See getContentNameInfo's
72
92
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -83,7 +103,7 @@ function runInPage(ctx) {
83
103
 
84
104
  // A <label> contributes a name via its own aria-label/aria-labelledby
85
105
  // (checked first, same ARIA-over-content precedence any element's
86
- // accessible name gives — e.g. <label aria-label="Search"><svg
106
+ // accessible name gives, e.g. <label aria-label="Search"><svg
87
107
  // aria-hidden="true">...</svg></label> names its control "Search" even
88
108
  // though the label's only child content is aria-hidden) or, failing
89
109
  // that, its rendered content (getConservativeSubtreeText).
@@ -98,10 +118,10 @@ function runInPage(ctx) {
98
118
  if (content) return content;
99
119
  // Final fallback per the general accname text-alternative algorithm,
100
120
  // which applies to any element being asked for its name regardless of
101
- // why (own aria-label, an aria-labelledby reference, or — here — native
121
+ // why (own aria-label, an aria-labelledby reference, or, here, native
102
122
  // <label for> association): title, when nothing else yields a name.
103
123
  // Purely additive (only fills in a name where there was none before),
104
- // so it carries no false-positive risk — see dialog-name-present.js's
124
+ // so it carries no false-positive risk, see dialog-name-present.js's
105
125
  // identical <iframe>-title-fallback fix for the concrete real-world
106
126
  // trigger this same accname step covers elsewhere.
107
127
  return getAttr(lab, 'title');
@@ -111,7 +131,7 @@ function runInPage(ctx) {
111
131
  const raw = getAttr(el, 'aria-labelledby');
112
132
  if (!raw) return '';
113
133
  // Delegates to the shared getTextFromIdRefs helper instead of computing
114
- // name-from-content of the referenced element — see dialog-name-
134
+ // name-from-content of the referenced element, see dialog-name-
115
135
  // present.js's identical fix for the full rationale (an <iframe>
116
136
  // aria-labelledby target's only name source is its title attribute,
117
137
  // which name-from-content alone can never see).
@@ -231,24 +251,22 @@ function runInPage(ctx) {
231
251
  }
232
252
  })()
233
253
  : null;
234
- const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
235
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
236
254
 
237
- occurrences.push({
238
- selector: stableSelector,
239
- html,
240
- summary: 'This combobox has no accessible name.',
241
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this combobox's accessible name.",
242
- i18n: {
243
- summaryKey: 'comboboxNamePresent_summary_fail',
244
- hintKey: 'comboboxNamePresent_hint_fail',
245
- params: {}
246
- },
247
- data: {
248
- details: { reasonCode: 'name_missing', controlType: 'combobox', methodTried: res.method },
249
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
250
- }
251
- });
255
+ occurrences.push(
256
+ helpers.reportOccurrence(el, {
257
+ summary: 'This combobox has no accessible name.',
258
+ hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this combobox's accessible name.",
259
+ i18n: {
260
+ summaryKey: 'comboboxNamePresent_summary_fail',
261
+ hintKey: 'comboboxNamePresent_hint_fail',
262
+ params: {}
263
+ },
264
+ data: {
265
+ details: { reasonCode: 'name_missing', controlType: 'combobox', methodTried: res.method },
266
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
267
+ }
268
+ })
269
+ );
252
270
  }
253
271
 
254
272
  if (applicableCount === 0) {
@@ -2,6 +2,37 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check contrast-computable
7
+ * @atomic true
8
+ * @summary Color contrast must be computable from CSS for rendered text
9
+ * @standard WCAG 2.2
10
+ * @sc 1.4.3
11
+ * @sc 1.4.6
12
+ * @applicability
13
+ * Applies to every visible text node in scope, plus the label of <input
14
+ * type="button">/[type="submit"]/[type="reset"], which is rendered from
15
+ * the value attribute and so is invisible to a text-node walk. Text counts
16
+ * only when its element is DOM-visible under the run's visibility mode, is
17
+ * not clipped out of sight by the sr-only technique (clip or clip-path),
18
+ * and belongs neither to a disabled control nor to the label of one,
19
+ * WCAG's inactive-user-interface-component exception. Subtrees excluded
20
+ * via engineOptions.excludeSelectors are skipped, and open shadow roots
21
+ * are walked as roots in their own right.
22
+ * @expectation
23
+ * Both sides of the contrast calculation can be established from CSS for
24
+ * every applicable text node: an effective background resolving to an
25
+ * opaque color, and a parsable foreground color. Where either cannot be,
26
+ * a background image or gradient, mix-blend-mode, a filter or
27
+ * backdrop-filter, a text-shadow (which may add contrast this engine has
28
+ * no glyph-rendering model to account for), ancestor opacity, a root
29
+ * background that never becomes opaque, or a color that does not parse,
30
+ * the result is cantTell naming the blocker. This rule is the one that
31
+ * reports that uncertainty, which is what lets contrast-minimum and
32
+ * contrast-enhanced stay silent on the same text instead of guessing at
33
+ * a ratio.
34
+ */
35
+
5
36
  const id = 'contrast-computable';
6
37
 
7
38
  const meta = {
@@ -118,6 +149,7 @@ function runInPage(ctx) {
118
149
  else summaryKey = 'contrastComputable_cantTell_filterOrBackdropFilter';
119
150
  } else if (rc === 'BACKGROUND_NOT_OPAQUE_AT_ROOT')
120
151
  summaryKey = 'contrastComputable_cantTell_rootNotOpaque';
152
+ else if (rc === 'TEXT_SHADOW') summaryKey = 'contrastComputable_cantTell_textShadow';
121
153
 
122
154
  const details = Object.assign(
123
155
  { reasonCode: rc },
@@ -2,10 +2,30 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check contrast-enhanced
7
+ * @atomic true
8
+ * @summary Text must meet the WCAG AAA contrast ratio for its size
9
+ * @standard WCAG 2.2
10
+ * @sc 1.4.6
11
+ * @applicability
12
+ * Applies to the visible text contrast-computable applies to, see that
13
+ * rule for the eligibility gates, narrowed to text whose background and
14
+ * foreground are actually computable. Eligible text that is not computable
15
+ * leaves this rule notApplicable rather than cantTell: reporting that
16
+ * uncertainty belongs to contrast-computable, so the two never report the
17
+ * same text twice.
18
+ * @expectation
19
+ * Every computable text node reaches the ratio SC 1.4.6 requires for its
20
+ * size: 4.5:1 for large text, 7:1 for everything else. Text is large at
21
+ * 24px or more, or at 14pt (about 18.667px) or more when the computed font
22
+ * weight is 700 or higher.
23
+ */
24
+
5
25
  const id = 'contrast-enhanced';
6
26
 
7
27
  const meta = {
8
- title: 'Text must meet the enhanced color contrast ratio',
28
+ title: 'Text meets enhanced color contrast (AAA)',
9
29
  description:
10
30
  'Checks that visible text has a contrast ratio of at least 7:1 (normal) or 4.5:1 (large), when contrast is computable from CSS.',
11
31
  i18n: {
@@ -2,10 +2,30 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check contrast-minimum
7
+ * @atomic true
8
+ * @summary Text must meet the WCAG AA contrast ratio for its size
9
+ * @standard WCAG 2.2
10
+ * @sc 1.4.3
11
+ * @applicability
12
+ * Applies to the visible text contrast-computable applies to, see that
13
+ * rule for the eligibility gates, narrowed to text whose background and
14
+ * foreground are actually computable. Eligible text that is not computable
15
+ * leaves this rule notApplicable rather than cantTell: reporting that
16
+ * uncertainty belongs to contrast-computable, so the two never report the
17
+ * same text twice.
18
+ * @expectation
19
+ * Every computable text node reaches the ratio SC 1.4.3 requires for its
20
+ * size: 3:1 for large text, 4.5:1 for everything else. Text is large at
21
+ * 24px or more, or at 14pt (about 18.667px) or more when the computed font
22
+ * weight is 700 or higher.
23
+ */
24
+
5
25
  const id = 'contrast-minimum';
6
26
 
7
27
  const meta = {
8
- title: 'Text must meet the minimum color contrast ratio',
28
+ title: 'Text meets minimum color contrast (AA)',
9
29
  description:
10
30
  'Checks that visible text has a contrast ratio of at least 4.5:1 (normal) or 3:1 (large), when contrast is computable from CSS.',
11
31
  i18n: {