@surea11y/core 1.5.0 → 1.7.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 (157) hide show
  1. package/CHANGELOG.md +240 -149
  2. package/README.md +51 -44
  3. package/docs/ACT_RULE_MAPPING.md +245 -0
  4. package/docs/API_STABILITY.md +53 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
  6. package/docs/DESIGN_CHALLENGES.md +367 -0
  7. package/docs/EARL.md +100 -0
  8. package/docs/ENGINE_OPTIONS.md +42 -4
  9. package/docs/I18N.md +4 -4
  10. package/docs/INTEGRATION.md +4 -2
  11. package/docs/LIMITATIONS.md +9 -5
  12. package/docs/OUTPUT_SCHEMA.md +44 -6
  13. package/docs/POLICY.md +1 -1
  14. package/docs/REPORT.md +1 -1
  15. package/docs/RULE_AUTHORING.md +63 -36
  16. package/docs/RULE_CATALOG.md +1928 -169
  17. package/docs/RULE_HELPERS.md +333 -0
  18. package/docs/RULE_TAXONOMY.md +27 -6
  19. package/docs/SARIF.md +21 -2
  20. package/docs/TROUBLESHOOTING.md +2 -2
  21. package/docs/WCAG_CONFORMANCE.md +34 -10
  22. package/package.json +11 -9
  23. package/src/baseline.js +3 -3
  24. package/src/checks/automatic/area-alt-present.js +2 -2
  25. package/src/checks/automatic/aria-allowed-attr.js +74 -10
  26. package/src/checks/automatic/aria-allowed-role.js +34 -25
  27. package/src/checks/automatic/aria-braille-equivalent.js +21 -13
  28. package/src/checks/automatic/aria-conditional-attr.js +22 -15
  29. package/src/checks/automatic/aria-deprecated-role.js +13 -1
  30. package/src/checks/automatic/aria-hidden-body.js +3 -3
  31. package/src/checks/automatic/aria-hidden-focus.js +5 -5
  32. package/src/checks/automatic/aria-prohibited-attr.js +23 -18
  33. package/src/checks/automatic/aria-prohibited-children.js +136 -43
  34. package/src/checks/automatic/aria-required-attr.js +119 -24
  35. package/src/checks/automatic/aria-required-children.js +54 -30
  36. package/src/checks/automatic/aria-required-parent.js +93 -15
  37. package/src/checks/automatic/aria-role-name-present.js +37 -23
  38. package/src/checks/automatic/aria-roles-valid.js +52 -21
  39. package/src/checks/automatic/aria-valid-attr-value.js +89 -33
  40. package/src/checks/automatic/aria-valid-attr.js +15 -10
  41. package/src/checks/automatic/autocomplete-valid.js +2 -2
  42. package/src/checks/automatic/avoid-inline-spacing.js +133 -6
  43. package/src/checks/automatic/binary-control-name-present.js +27 -5
  44. package/src/checks/automatic/button-name-present.js +92 -6
  45. package/src/checks/automatic/combobox-name-present.js +26 -6
  46. package/src/checks/automatic/contrast-computable.js +42 -0
  47. package/src/checks/automatic/contrast-enhanced.js +33 -1
  48. package/src/checks/automatic/contrast-minimum.js +33 -1
  49. package/src/checks/automatic/css-orientation-lock.js +138 -24
  50. package/src/checks/automatic/definition-list-children-valid.js +7 -8
  51. package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
  52. package/src/checks/automatic/dialog-name-present.js +20 -2
  53. package/src/checks/automatic/duplicate-id-aria.js +10 -3
  54. package/src/checks/automatic/duplicate-id.js +203 -0
  55. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  56. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  57. package/src/checks/automatic/form-control-single-label.js +10 -1
  58. package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
  59. package/src/checks/automatic/iframe-focusable-content.js +68 -7
  60. package/src/checks/automatic/iframe-name-present.js +37 -3
  61. package/src/checks/automatic/iframe-title-unique.js +1 -1
  62. package/src/checks/automatic/img-alt-present.js +12 -4
  63. package/src/checks/automatic/label-in-name.js +204 -68
  64. package/src/checks/automatic/link-in-text-block.js +285 -29
  65. package/src/checks/automatic/link-name-present.js +22 -1
  66. package/src/checks/automatic/list-children-valid.js +6 -6
  67. package/src/checks/automatic/listbox-name-present.js +28 -8
  68. package/src/checks/automatic/listitem-parent-valid.js +4 -4
  69. package/src/checks/automatic/menuitem-name-present.js +20 -2
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
  72. package/src/checks/automatic/meter-name-present.js +23 -4
  73. package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
  74. package/src/checks/automatic/option-name-present.js +23 -4
  75. package/src/checks/automatic/page-title-present.js +21 -3
  76. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  77. package/src/checks/automatic/progressbar-name-present.js +23 -4
  78. package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
  79. package/src/checks/automatic/searchbox-name-present.js +28 -8
  80. package/src/checks/automatic/server-side-image-map-absent.js +1 -1
  81. package/src/checks/automatic/slider-name-present.js +27 -6
  82. package/src/checks/automatic/spinbutton-name-present.js +28 -8
  83. package/src/checks/automatic/summary-name-present.js +18 -2
  84. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  85. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  86. package/src/checks/automatic/tab-name-present.js +21 -2
  87. package/src/checks/automatic/table-headers-attr-valid.js +43 -8
  88. package/src/checks/automatic/table-th-has-data-cells.js +61 -5
  89. package/src/checks/automatic/target-size-minimum.js +155 -58
  90. package/src/checks/automatic/td-has-header.js +24 -23
  91. package/src/checks/automatic/textbox-name-present.js +28 -8
  92. package/src/checks/automatic/tooltip-name-present.js +21 -2
  93. package/src/checks/automatic/treeitem-name-present.js +23 -4
  94. package/src/checks/automatic/valid-lang.js +92 -7
  95. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  96. package/src/checks/manual/accesskeys-manual.js +3 -3
  97. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  98. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  99. package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
  100. package/src/checks/manual/aria-text-manual.js +4 -4
  101. package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  103. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  105. package/src/checks/manual/empty-heading-manual.js +58 -11
  106. package/src/checks/manual/empty-table-header-manual.js +8 -8
  107. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  108. package/src/checks/manual/form-control-label-quality-manual.js +563 -0
  109. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
  110. package/src/checks/manual/heading-order-manual.js +3 -3
  111. package/src/checks/manual/heading-quality-manual.js +338 -0
  112. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  113. package/src/checks/manual/image-redundant-alt-manual.js +4 -4
  114. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  115. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  116. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  117. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  118. package/src/checks/manual/label-title-only-manual.js +4 -4
  119. package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
  120. package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
  121. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
  122. package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
  123. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
  124. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
  125. package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
  126. package/src/checks/manual/landmark-one-main-manual.js +6 -6
  127. package/src/checks/manual/landmark-unique-manual.js +9 -9
  128. package/src/checks/manual/link-name-quality-manual.js +161 -32
  129. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  130. package/src/checks/manual/meta-viewport-large-manual.js +2 -2
  131. package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
  132. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  133. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  134. package/src/checks/manual/p-as-heading-manual.js +4 -4
  135. package/src/checks/manual/page-has-heading-one-manual.js +6 -6
  136. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  137. package/src/checks/manual/password-paste-enabled-manual.js +255 -0
  138. package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
  139. package/src/checks/manual/region-manual.js +19 -19
  140. package/src/checks/manual/scope-attr-valid-manual.js +2 -2
  141. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  142. package/src/checks/manual/skip-link-manual.js +5 -5
  143. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  144. package/src/checks/manual/tabindex-manual.js +2 -2
  145. package/src/checks/manual/table-duplicate-name-manual.js +2 -2
  146. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  147. package/src/checks/manual/video-caption-manual.js +3 -3
  148. package/src/checks/manual-review.js +17 -1
  149. package/src/core.js +8880 -41883
  150. package/src/earl.js +144 -0
  151. package/src/report.js +2 -2
  152. package/src/sarif.js +22 -2
  153. package/surea11y.browser.js +10 -37882
  154. package/surea11y.i18n.de.js +2 -21
  155. package/surea11y.i18n.es.js +2 -21
  156. package/surea11y.i18n.fr.js +2 -21
  157. package/bin/surea11y-core.js +0 -20
@@ -2,6 +2,25 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check progressbar-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="progressbar" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 1.1.1
11
+ * @applicability
12
+ * Applies to elements carrying role="progressbar" (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="progressbar" is name-from-author-only, so subtree text is never
20
+ * accepted: text sitting inside a custom progressbar widget is not
21
+ * reliably exposed as its name.
22
+ */
23
+
5
24
  const id = 'progressbar-name-present';
6
25
 
7
26
  const meta = {
@@ -54,7 +73,7 @@ function runInPage(ctx) {
54
73
  const raw = getAttr(el, 'aria-labelledby');
55
74
  if (!raw) return '';
56
75
  // Delegates to the shared getTextFromIdRefs helper instead of computing
57
- // name-from-content of the referenced element — see dialog-name-
76
+ // name-from-content of the referenced element, see dialog-name-
58
77
  // present.js's identical fix for the full rationale (an <iframe>
59
78
  // aria-labelledby target's only name source is its title attribute,
60
79
  // which name-from-content alone can never see).
@@ -106,10 +125,10 @@ function runInPage(ctx) {
106
125
  if (title) return { ok: true, method: 'title' };
107
126
 
108
127
  // role="progressbar" is name-from-author-only per WAI-ARIA: aria-label,
109
- // aria-labelledby, or title — no content-based naming method at all. It
128
+ // aria-labelledby, or title, no content-based naming method at all. It
110
129
  // must NOT fall back to subtree content: e.g. a <ul role="progressbar">
111
130
  // loading skeleton with no name of its own, whose nested descendants
112
- // carry their own aria-label for a different purpose — falling back to
131
+ // carry their own aria-label for a different purpose, falling back to
113
132
  // subtree content would wrongly treat that as the progressbar's name.
114
133
  return { ok: false, method: 'none' };
115
134
  }
@@ -139,7 +158,7 @@ function runInPage(ctx) {
139
158
  occurrences.push(
140
159
  helpers.reportOccurrence(el, {
141
160
  summary: 'This progress bar has no accessible name.',
142
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this progress bar's accessible name.",
161
+ hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this progress bar's accessible name.",
143
162
  i18n: {
144
163
  summaryKey: 'progressbarNamePresent_summary_fail',
145
164
  hintKey: 'progressbarNamePresent_hint_fail',
@@ -5,26 +5,36 @@
5
5
  /**
6
6
  * @check role-img-text-alternative-present
7
7
  * @atomic true
8
- * @summary Accessible elements with role="img" must have an alternative text via aria-label or aria-labelledby
8
+ * @summary Accessible elements with role="img"/"graphics-symbol"/"graphics-document" must have a text alternative
9
9
  * @standard WCAG 2.2
10
10
  * @sc 1.1.1
11
11
  * @applicability
12
- * Applies to elements with role="img" that are exposed to assistive technologies.
13
- * Elements otherwise hidden from the accessibility tree remain applicable
14
- * if they are focusable or referenced by IDREF relationships (per engine eligibility checks).
12
+ * Applies to elements with role="img", role="graphics-symbol" or
13
+ * role="graphics-document" that are included in the accessibility tree
14
+ * (ACT 23a2a8's "programmatically hidden" exemption:
15
+ * display:none/visibility:hidden/aria-hidden="true" on the element or an
16
+ * ancestor, with no carve-out for focusable or IDREF-referenced elements,
17
+ * aria-hidden-focus and duplicate-id-aria own those separately). Per ACT
18
+ * 7d6734, this reaches any element carrying one of these roles, not only
19
+ * the <svg> root, e.g. a <circle role="graphics-symbol"> nested inside a
20
+ * plain <svg>. The <svg> root itself is left to svg-text-alternative-present's
21
+ * own (already ACT-clean) title/aria-name handling; the <img> tag is
22
+ * excluded here since it has its own dedicated rule.
15
23
  * @expectation
16
- * Each applicable element with role="img" has an accessible text alternative:
24
+ * Each applicable element has an accessible text alternative:
17
25
  * - aria-label with a non-empty value; OR
18
26
  * - aria-labelledby referencing at least one existing element that contributes non-empty text; OR
19
- * - a non-empty title attribute (last-resort accessible-name source per HTML-AAM).
27
+ * - a non-empty title attribute (last-resort accessible-name source per HTML-AAM); OR
28
+ * - for an SVG-namespace element, a non-empty first-child <title> (SVG-AAM's own naming mechanism, not only for the <svg> root).
20
29
  */
21
30
 
22
31
  const id = 'role-img-text-alternative-present';
23
32
 
24
33
  const meta = {
25
- title: '[role="img"] must have an accessible text alternative',
34
+ title:
35
+ '[role="img"/"graphics-symbol"/"graphics-document"] must have an accessible text alternative',
26
36
  description:
27
- 'Checks that elements with role="img" provide an accessible text alternative via aria-label, aria-labelledby, or a title attribute.',
37
+ 'Checks that elements with role="img", "graphics-symbol" or "graphics-document" provide an accessible text alternative using aria-label, aria-labelledby, a title attribute, or (for SVG elements) a first-child <title>.',
28
38
  i18n: {
29
39
  titleKey: 'roleImg_textAlternativePresent_title',
30
40
  descriptionKey: 'roleImg_textAlternativePresent_description'
@@ -86,7 +96,7 @@ function runInPage(ctx) {
86
96
 
87
97
  const imgElements = (() => {
88
98
  // do not consider element "img" because it has its own rule
89
- const sel = '[role="img" i]:not(img)';
99
+ const sel = '[role="img" i]:not(img), [role="graphics-symbol" i], [role="graphics-document" i]';
90
100
  try {
91
101
  return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
92
102
  } catch {
@@ -101,8 +111,16 @@ function runInPage(ctx) {
101
111
  const occurrences = [];
102
112
  let applicableCount = 0;
103
113
 
114
+ // ACT 23a2a8 (same rule as native img-alt-present) exempts programmatically
115
+ // hidden images and that glossary term has no focusability carve-out, so a
116
+ // tabbable/IDREF-referenced role="img" inside aria-hidden stays out of
117
+ // scope here too; aria-hidden-focus reports that markup instead.
104
118
  const isAccTreeEligible =
105
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
119
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
120
+ ? helpers.isIncludedInAccessibilityTree
121
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
122
+ ? helpers.isAccTreeEligible
123
+ : null;
106
124
 
107
125
  const getAccessibleNameInfo =
108
126
  helpers && typeof helpers.getAccessibleNameInfo === 'function'
@@ -122,10 +140,21 @@ function runInPage(ctx) {
122
140
  }
123
141
  })();
124
142
  if (elig && elig.eligible === false) continue;
143
+ // isIncludedInAccessibilityTree returns a plain boolean, not an
144
+ // {eligible, reasons} object like isAccTreeEligible's fallback shape.
145
+ if (typeof elig === 'boolean' && elig === false) continue;
125
146
  }
126
147
 
127
148
  applicableCount += 1;
128
149
 
150
+ const matchedRole = (() => {
151
+ try {
152
+ return trim(el.getAttribute('role')).split(/\s+/)[0].toLowerCase();
153
+ } catch {
154
+ return 'img';
155
+ }
156
+ })();
157
+
129
158
  // Expectation: aria-label OR aria-labelledby. We use helper name-info when available,
130
159
  // but we also validate the source to keep this rule scoped/deterministic.
131
160
 
@@ -163,6 +192,25 @@ function runInPage(ctx) {
163
192
  })();
164
193
  const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
165
194
 
195
+ // SVG-AAM's own accessible-name mechanism: a first-child <title>
196
+ // element (not the HTML title attribute) is the standard way to name
197
+ // any element in the SVG namespace, not only the <svg> root -- a
198
+ // role="graphics-symbol" <circle> named only this way still has a real
199
+ // text alternative, same as a role="img" <svg>.
200
+ const svgTitleChildText = (() => {
201
+ try {
202
+ const isSvgNamespace = el.namespaceURI === 'http://www.w3.org/2000/svg';
203
+ if (!isSvgNamespace) return '';
204
+ const first = el.firstElementChild;
205
+ const firstTag = first ? (first.localName || first.tagName || '').toLowerCase() : '';
206
+ if (firstTag !== 'title') return '';
207
+ return trim(first.textContent);
208
+ } catch {
209
+ return '';
210
+ }
211
+ })();
212
+ const hasValidTitleSource = hasValidTitle || !!svgTitleChildText;
213
+
166
214
  let nameInfo = null;
167
215
 
168
216
  // Fast outcomes first (no helper needed)
@@ -170,19 +218,19 @@ function runInPage(ctx) {
170
218
  let hasName = false;
171
219
 
172
220
  if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
173
- if (hasValidTitle) {
221
+ if (hasValidTitleSource) {
174
222
  hasName = true;
175
223
  } else {
176
224
  reasonCode = 'missingTextAlternative';
177
225
  }
178
226
  } else if (hasAriaLabelAttr && !hasValidAriaLabel) {
179
- if (hasValidTitle) {
227
+ if (hasValidTitleSource) {
180
228
  hasName = true;
181
229
  } else {
182
230
  reasonCode = 'emptyAriaLabel';
183
231
  }
184
232
  } else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
185
- if (hasValidTitle) {
233
+ if (hasValidTitleSource) {
186
234
  hasName = true;
187
235
  } else {
188
236
  reasonCode = 'emptyAriaLabelledby';
@@ -214,12 +262,12 @@ function runInPage(ctx) {
214
262
  const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
215
263
 
216
264
  const baseOccurrence = {
217
- summary: 'Missing text alternative on element with role="img".',
218
- hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this image a text alternative.',
265
+ summary: `Missing text alternative on element with role="${matchedRole}".`,
266
+ hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this element a text alternative.',
219
267
  i18n: {
220
268
  summaryKey: 'roleImg_textAlternativePresent_summary_fail',
221
269
  hintKey: 'roleImg_textAlternativePresent_hint_fail',
222
- params: { role: 'img' }
270
+ params: { role: matchedRole }
223
271
  },
224
272
  data: {
225
273
  details: {
@@ -2,11 +2,31 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check searchbox-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="searchbox" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="searchbox" (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="searchbox" is name-from-author-only, so subtree text is never
20
+ * accepted: text sitting inside a custom searchbox widget is not reliably
21
+ * exposed as its name. On a labelable element (<input role="searchbox">)
22
+ * an associated <label> counts as well.
23
+ */
24
+
5
25
  const id = 'searchbox-name-present';
6
26
 
7
27
  const meta = {
8
- title: 'Accessible name is present',
9
- description: 'Checks that elements expose a non-empty accessible name.',
28
+ title: 'Searchboxes have an accessible name',
29
+ description: 'Checks that elements with role="searchbox" expose a non-empty accessible name.',
10
30
  i18n: {
11
31
  titleKey: 'searchboxNamePresent_title',
12
32
  descriptionKey: 'searchboxNamePresent_description'
@@ -51,7 +71,7 @@ function runInPage(ctx) {
51
71
  }
52
72
 
53
73
  function getConservativeSubtreeText(document, container) {
54
- // "Name from content" — recurses into descendants and uses each one's
74
+ // "Name from content", recurses into descendants and uses each one's
55
75
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
56
76
  // it has one, not just literal text nodes. See getContentNameInfo's
57
77
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -68,7 +88,7 @@ function runInPage(ctx) {
68
88
 
69
89
  // A <label> contributes a name via its own aria-label/aria-labelledby
70
90
  // (checked first, same ARIA-over-content precedence any element's
71
- // accessible name gives — e.g. <label aria-label="Search"><svg
91
+ // accessible name gives, e.g. <label aria-label="Search"><svg
72
92
  // aria-hidden="true">...</svg></label> names its control "Search" even
73
93
  // though the label's only child content is aria-hidden) or, failing
74
94
  // that, its rendered content (getConservativeSubtreeText).
@@ -83,10 +103,10 @@ function runInPage(ctx) {
83
103
  if (content) return content;
84
104
  // Final fallback per the general accname text-alternative algorithm,
85
105
  // which applies to any element being asked for its name regardless of
86
- // why (own aria-label, an aria-labelledby reference, or — here — native
106
+ // why (own aria-label, an aria-labelledby reference, or, here, native
87
107
  // <label for> association): title, when nothing else yields a name.
88
108
  // Purely additive (only fills in a name where there was none before),
89
- // so it carries no false-positive risk — see dialog-name-present.js's
109
+ // so it carries no false-positive risk, see dialog-name-present.js's
90
110
  // identical <iframe>-title-fallback fix for the concrete real-world
91
111
  // trigger this same accname step covers elsewhere.
92
112
  return getAttr(lab, 'title');
@@ -96,7 +116,7 @@ function runInPage(ctx) {
96
116
  const raw = getAttr(el, 'aria-labelledby');
97
117
  if (!raw) return '';
98
118
  // Delegates to the shared getTextFromIdRefs helper instead of computing
99
- // name-from-content of the referenced element — see dialog-name-
119
+ // name-from-content of the referenced element, see dialog-name-
100
120
  // present.js's identical fix for the full rationale (an <iframe>
101
121
  // aria-labelledby target's only name source is its title attribute,
102
122
  // which name-from-content alone can never see).
@@ -231,7 +251,7 @@ function runInPage(ctx) {
231
251
  occurrences.push(
232
252
  helpers.reportOccurrence(el, {
233
253
  summary: 'This element has no accessible name.',
234
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this searchbox's accessible name.",
254
+ hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this searchbox's accessible name.",
235
255
  i18n: {
236
256
  summaryKey: 'searchboxNamePresent_summary_fail',
237
257
  hintKey: 'searchboxNamePresent_hint_fail',
@@ -13,7 +13,7 @@
13
13
  * @expectation
14
14
  * The image does not use ismap at all. Server-side image maps depend on
15
15
  * the browser sending click coordinates to the server, which has no
16
- * keyboard-operable equivalent — there is no way to determine or expose
16
+ * keyboard-operable equivalent, there is no way to determine or expose
17
17
  * individual clickable regions to assistive technology or keyboard
18
18
  * users. Client-side image maps (<map>/<area>, each with real href/alt)
19
19
  * are the accessible alternative and are not flagged by this rule.
@@ -2,6 +2,27 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check slider-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="slider" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="slider" (the attribute must name that
13
+ * role alone, not a fallback list) that are included in the accessibility
14
+ * tree. An element with the matching implicit role but no role attribute
15
+ * is out of scope. A native <input type="range"> without the role belongs
16
+ * to form-control-programmatic-label-present.
17
+ * @expectation
18
+ * The element has a non-empty accessible name from aria-label, from an
19
+ * aria-labelledby that resolves to non-empty text, or from title.
20
+ * role="slider" is name-from-author-only, so subtree text is never
21
+ * accepted: text sitting inside a custom slider widget is not reliably
22
+ * exposed as its name. On a labelable element (<input role="slider">) an
23
+ * associated <label> counts as well.
24
+ */
25
+
5
26
  const id = 'slider-name-present';
6
27
 
7
28
  const meta = {
@@ -67,7 +88,7 @@ function runInPage(ctx) {
67
88
  }
68
89
 
69
90
  function getConservativeSubtreeText(document, container) {
70
- // "Name from content" — recurses into descendants and uses each one's
91
+ // "Name from content", recurses into descendants and uses each one's
71
92
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
72
93
  // it has one, not just literal text nodes. See getContentNameInfo's
73
94
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -84,7 +105,7 @@ function runInPage(ctx) {
84
105
 
85
106
  // A <label> contributes a name via its own aria-label/aria-labelledby
86
107
  // (checked first, same ARIA-over-content precedence any element's
87
- // accessible name gives — e.g. <label aria-label="Search"><svg
108
+ // accessible name gives, e.g. <label aria-label="Search"><svg
88
109
  // aria-hidden="true">...</svg></label> names its control "Search" even
89
110
  // though the label's only child content is aria-hidden) or, failing
90
111
  // that, its rendered content (getConservativeSubtreeText).
@@ -99,10 +120,10 @@ function runInPage(ctx) {
99
120
  if (content) return content;
100
121
  // Final fallback per the general accname text-alternative algorithm,
101
122
  // which applies to any element being asked for its name regardless of
102
- // why (own aria-label, an aria-labelledby reference, or — here — native
123
+ // why (own aria-label, an aria-labelledby reference, or, here, native
103
124
  // <label for> association): title, when nothing else yields a name.
104
125
  // Purely additive (only fills in a name where there was none before),
105
- // so it carries no false-positive risk — see dialog-name-present.js's
126
+ // so it carries no false-positive risk, see dialog-name-present.js's
106
127
  // identical <iframe>-title-fallback fix for the concrete real-world
107
128
  // trigger this same accname step covers elsewhere.
108
129
  return getAttr(lab, 'title');
@@ -112,7 +133,7 @@ function runInPage(ctx) {
112
133
  const raw = getAttr(el, 'aria-labelledby');
113
134
  if (!raw) return '';
114
135
  // Delegates to the shared getTextFromIdRefs helper instead of computing
115
- // name-from-content of the referenced element — see dialog-name-
136
+ // name-from-content of the referenced element, see dialog-name-
116
137
  // present.js's identical fix for the full rationale (an <iframe>
117
138
  // aria-labelledby target's only name source is its title attribute,
118
139
  // which name-from-content alone can never see).
@@ -209,7 +230,7 @@ function runInPage(ctx) {
209
230
  }
210
231
 
211
232
  // role="slider" is name-from-author-only per WAI-ARIA (unlike e.g.
212
- // role="option"/"tab"), so it must NOT fall back to subtree content —
233
+ // role="option"/"tab"), so it must NOT fall back to subtree content,
213
234
  // visible text near/inside a custom slider widget is not reliably
214
235
  // exposed as its accessible name by assistive technologies.
215
236
  return { ok: false, method: 'none' };
@@ -2,11 +2,31 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check spinbutton-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="spinbutton" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="spinbutton" (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="spinbutton" is name-from-author-only, so subtree text is never
20
+ * accepted: text sitting inside a custom spinbutton widget is not reliably
21
+ * exposed as its name. On a labelable element (<input role="spinbutton">)
22
+ * an associated <label> counts as well.
23
+ */
24
+
5
25
  const id = 'spinbutton-name-present';
6
26
 
7
27
  const meta = {
8
- title: 'Accessible name is present',
9
- description: 'Checks that elements expose a non-empty accessible name.',
28
+ title: 'Spinbuttons have an accessible name',
29
+ description: 'Checks that elements with role="spinbutton" expose a non-empty accessible name.',
10
30
  i18n: {
11
31
  titleKey: 'spinbuttonNamePresent_title',
12
32
  descriptionKey: 'spinbuttonNamePresent_description'
@@ -51,7 +71,7 @@ function runInPage(ctx) {
51
71
  }
52
72
 
53
73
  function getConservativeSubtreeText(document, container) {
54
- // "Name from content" — recurses into descendants and uses each one's
74
+ // "Name from content", recurses into descendants and uses each one's
55
75
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
56
76
  // it has one, not just literal text nodes. See getContentNameInfo's
57
77
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -68,7 +88,7 @@ function runInPage(ctx) {
68
88
 
69
89
  // A <label> contributes a name via its own aria-label/aria-labelledby
70
90
  // (checked first, same ARIA-over-content precedence any element's
71
- // accessible name gives — e.g. <label aria-label="Search"><svg
91
+ // accessible name gives, e.g. <label aria-label="Search"><svg
72
92
  // aria-hidden="true">...</svg></label> names its control "Search" even
73
93
  // though the label's only child content is aria-hidden) or, failing
74
94
  // that, its rendered content (getConservativeSubtreeText).
@@ -83,10 +103,10 @@ function runInPage(ctx) {
83
103
  if (content) return content;
84
104
  // Final fallback per the general accname text-alternative algorithm,
85
105
  // which applies to any element being asked for its name regardless of
86
- // why (own aria-label, an aria-labelledby reference, or — here — native
106
+ // why (own aria-label, an aria-labelledby reference, or, here, native
87
107
  // <label for> association): title, when nothing else yields a name.
88
108
  // Purely additive (only fills in a name where there was none before),
89
- // so it carries no false-positive risk — see dialog-name-present.js's
109
+ // so it carries no false-positive risk, see dialog-name-present.js's
90
110
  // identical <iframe>-title-fallback fix for the concrete real-world
91
111
  // trigger this same accname step covers elsewhere.
92
112
  return getAttr(lab, 'title');
@@ -96,7 +116,7 @@ function runInPage(ctx) {
96
116
  const raw = getAttr(el, 'aria-labelledby');
97
117
  if (!raw) return '';
98
118
  // Delegates to the shared getTextFromIdRefs helper instead of computing
99
- // name-from-content of the referenced element — see dialog-name-
119
+ // name-from-content of the referenced element, see dialog-name-
100
120
  // present.js's identical fix for the full rationale (an <iframe>
101
121
  // aria-labelledby target's only name source is its title attribute,
102
122
  // which name-from-content alone can never see).
@@ -231,7 +251,7 @@ function runInPage(ctx) {
231
251
  occurrences.push(
232
252
  helpers.reportOccurrence(el, {
233
253
  summary: 'This element has no accessible name.',
234
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this spinbutton's accessible name.",
254
+ hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this spinbutton's accessible name.",
235
255
  i18n: {
236
256
  summaryKey: 'spinbuttonNamePresent_summary_fail',
237
257
  hintKey: 'spinbuttonNamePresent_hint_fail',
@@ -2,6 +2,22 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check summary-name-present
7
+ * @atomic true
8
+ * @summary <summary> elements must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to <summary> elements included in the accessibility tree,
13
+ * wherever they appear, a <summary> outside a <details> is still matched.
14
+ * @expectation
15
+ * The element has a non-empty accessible name from aria-label, from an
16
+ * aria-labelledby that resolves to non-empty text, from title, or from its
17
+ * own subtree text, where a descendant's own name (an <img alt>,
18
+ * aria-label or title) counts as that descendant's contribution.
19
+ */
20
+
5
21
  const id = 'summary-name-present';
6
22
 
7
23
  const meta = {
@@ -51,7 +67,7 @@ function runInPage(ctx) {
51
67
  }
52
68
 
53
69
  function getConservativeSubtreeText(document, container) {
54
- // "Name from content" — recurses into descendants and uses each one's
70
+ // "Name from content", recurses into descendants and uses each one's
55
71
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
56
72
  // it has one, not just literal text nodes. See getContentNameInfo's
57
73
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -70,7 +86,7 @@ function runInPage(ctx) {
70
86
  const raw = getAttr(el, 'aria-labelledby');
71
87
  if (!raw) return '';
72
88
  // Delegates to the shared getTextFromIdRefs helper instead of computing
73
- // name-from-content of the referenced element — see dialog-name-
89
+ // name-from-content of the referenced element, see dialog-name-
74
90
  // present.js's identical fix for the full rationale (an <iframe>
75
91
  // aria-labelledby target's only name source is its title attribute,
76
92
  // which name-from-content alone can never see).
@@ -109,7 +109,7 @@ function runInPage(ctx) {
109
109
  }
110
110
 
111
111
  // <desc> counts when it is the first child, or the second child
112
- // immediately following a <title> — the standard <title>+<desc> pairing.
112
+ // immediately following a <title>, the standard <title>+<desc> pairing.
113
113
  // A <desc> appearing later than that is not reliably read by AT.
114
114
  function descText(el) {
115
115
  try {
@@ -13,12 +13,15 @@
13
13
  * "Intended to be conveyed" is approximated deterministically by at least one of:
14
14
  * - role="img", role="graphics-symbol", or role="graphics-document"
15
15
  * on the SVG root element itself (the WAI-ARIA Graphics Module
16
- * roles, alongside img). Deliberately does NOT extend to arbitrary
17
- * role="graphics-symbol" descendants nested inside an <svg> — this
18
- * check's scope is the <svg> root only; a broader feature, not
19
- * attempted here.
16
+ * roles, alongside img). Does NOT extend to arbitrary
17
+ * role="img"/"graphics-symbol"/"graphics-document" descendants
18
+ * nested inside an <svg>. This check's scope is the <svg> root
19
+ * only; role-img-text-alternative-present covers those same three
20
+ * roles on any other element, including nested SVG shapes (ACT
21
+ * 7d6734's own failed example: a bare `<svg>` root with a nested
22
+ * `<circle role="graphics-symbol">`).
20
23
  * - aria-label / aria-labelledby present
21
- * - <title> or <desc> present (desc alone is an applicability signal only — see @expectation)
24
+ * - <title> or <desc> present (desc alone is an applicability signal only, see @expectation)
22
25
  * - focusable/tabbable (e.g., tabindex, native focusability)
23
26
  *
24
27
  * Images with role="presentation" or role="none" are excluded only when they are not focusable.
@@ -28,7 +31,7 @@
28
31
  * Each applicable <svg> element provides a text alternative via:
29
32
  * - non-empty <title> text, OR
30
33
  * - an ARIA name (aria-label / aria-labelledby).
31
- * A <desc> element alone does NOT satisfy this — per the SVG Accessibility
34
+ * A <desc> element alone does NOT satisfy this, per the SVG Accessibility
32
35
  * API Mappings spec §7.1, <desc> only ever contributes to the accessible
33
36
  * DESCRIPTION, never the accessible NAME. An <svg> with only a
34
37
  * <desc> and no <title>/ARIA name is still "applicable" (desc signals
@@ -40,7 +43,7 @@ const id = 'svg-text-alternative-present';
40
43
  const meta = {
41
44
  title: '<svg> must provide a text alternative',
42
45
  description:
43
- 'Checks that inline <svg> elements provide a text alternative via a <title> element or an ARIA name (a <desc> element alone does not count — it only contributes to the accessible description, not the name).',
46
+ 'Checks that inline <svg> elements provide a text alternative via a <title> element or an ARIA name (a <desc> element alone does not count).',
44
47
  i18n: {
45
48
  titleKey: 'svg_textAltPresent_title',
46
49
  descriptionKey: 'svg_textAltPresent_description'
@@ -124,7 +127,7 @@ function runInPage(ctx) {
124
127
  }
125
128
 
126
129
  // <desc> counts when it is the first child, or the second child
127
- // immediately following a <title> — the standard <title>+<desc> pairing
130
+ // immediately following a <title>, the standard <title>+<desc> pairing
128
131
  // (e.g. <svg><title>...</title><desc>...</desc>...</svg>). A <desc>
129
132
  // appearing later than that is not reliably read by AT.
130
133
  function nonEmptyDescText(svg) {
@@ -277,7 +280,7 @@ function runInPage(ctx) {
277
280
  }
278
281
 
279
282
  // Per SVG-AAM §7.1: <desc> contributes only to the accessible
280
- // DESCRIPTION, never the accessible NAME — so descText does not
283
+ // DESCRIPTION, never the accessible NAME, so descText does not
281
284
  // count here even though it does count toward applicability above.
282
285
  const ok = !!titleText || hasAriaName;
283
286
  if (ok) continue;
@@ -293,7 +296,7 @@ function runInPage(ctx) {
293
296
 
294
297
  const baseOccurrence = {
295
298
  summary: 'Missing text alternative for <svg>.',
296
- hint: 'Provide a <title> element with text, or an ARIA name (aria-label/aria-labelledby) — a <desc> element alone does not provide an accessible name.',
299
+ hint: 'Provide a <title> element with text, or an ARIA name (aria-label/aria-labelledby); a <desc> element alone does not provide an accessible name.',
297
300
  i18n: {
298
301
  summaryKey: 'svg_textAltPresent_summary_fail',
299
302
  hintKey: 'svg_textAltPresent_hint_fail',
@@ -2,6 +2,25 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check tab-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="tab" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="tab" (the attribute must name that
13
+ * role alone, not a fallback list) that are included in the accessibility
14
+ * tree. An element with the matching implicit role but no role attribute
15
+ * 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, from title, or,
19
+ * role="tab" being name-from-content, from its own subtree text, where a
20
+ * descendant's own name (an <img alt>, aria-label or title) counts as that
21
+ * descendant's contribution rather than only its text nodes.
22
+ */
23
+
5
24
  const id = 'tab-name-present';
6
25
 
7
26
  const meta = {
@@ -51,7 +70,7 @@ function runInPage(ctx) {
51
70
  }
52
71
 
53
72
  function getConservativeSubtreeText(document, container) {
54
- // "Name from content" — recurses into descendants and uses each one's
73
+ // "Name from content", recurses into descendants and uses each one's
55
74
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
56
75
  // it has one, not just literal text nodes. See getContentNameInfo's
57
76
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -70,7 +89,7 @@ function runInPage(ctx) {
70
89
  const raw = getAttr(el, 'aria-labelledby');
71
90
  if (!raw) return '';
72
91
  // Delegates to the shared getTextFromIdRefs helper instead of computing
73
- // name-from-content of the referenced element — see dialog-name-
92
+ // name-from-content of the referenced element, see dialog-name-
74
93
  // present.js's identical fix for the full rationale (an <iframe>
75
94
  // aria-labelledby target's only name source is its title attribute,
76
95
  // which name-from-content alone can never see).