@surea11y/core 1.5.0 → 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 (145) hide show
  1. package/CHANGELOG.md +193 -149
  2. package/README.md +27 -6
  3. package/docs/ACT_RULE_MAPPING.md +243 -0
  4. package/docs/API_STABILITY.md +2 -2
  5. package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
  6. package/docs/DESIGN_CHALLENGES.md +301 -0
  7. package/docs/ENGINE_OPTIONS.md +16 -4
  8. package/docs/I18N.md +4 -4
  9. package/docs/INTEGRATION.md +1 -1
  10. package/docs/LIMITATIONS.md +6 -4
  11. package/docs/REPORT.md +1 -1
  12. package/docs/RULE_AUTHORING.md +53 -25
  13. package/docs/RULE_CATALOG.md +1878 -169
  14. package/docs/RULE_TAXONOMY.md +2 -2
  15. package/docs/TROUBLESHOOTING.md +2 -2
  16. package/docs/WCAG_CONFORMANCE.md +25 -9
  17. package/package.json +3 -7
  18. package/src/baseline.js +3 -3
  19. package/src/checks/automatic/area-alt-present.js +2 -2
  20. package/src/checks/automatic/aria-allowed-attr.js +68 -10
  21. package/src/checks/automatic/aria-allowed-role.js +2 -2
  22. package/src/checks/automatic/aria-braille-equivalent.js +3 -3
  23. package/src/checks/automatic/aria-conditional-attr.js +5 -5
  24. package/src/checks/automatic/aria-deprecated-role.js +1 -1
  25. package/src/checks/automatic/aria-hidden-body.js +2 -2
  26. package/src/checks/automatic/aria-hidden-focus.js +5 -5
  27. package/src/checks/automatic/aria-prohibited-attr.js +18 -18
  28. package/src/checks/automatic/aria-prohibited-children.js +130 -37
  29. package/src/checks/automatic/aria-required-attr.js +60 -12
  30. package/src/checks/automatic/aria-required-children.js +21 -14
  31. package/src/checks/automatic/aria-required-parent.js +61 -9
  32. package/src/checks/automatic/aria-role-name-present.js +36 -22
  33. package/src/checks/automatic/aria-valid-attr-value.js +15 -12
  34. package/src/checks/automatic/aria-valid-attr.js +1 -1
  35. package/src/checks/automatic/autocomplete-valid.js +2 -2
  36. package/src/checks/automatic/binary-control-name-present.js +27 -5
  37. package/src/checks/automatic/button-name-present.js +92 -6
  38. package/src/checks/automatic/combobox-name-present.js +26 -6
  39. package/src/checks/automatic/contrast-computable.js +32 -0
  40. package/src/checks/automatic/contrast-enhanced.js +21 -1
  41. package/src/checks/automatic/contrast-minimum.js +21 -1
  42. package/src/checks/automatic/css-orientation-lock.js +96 -19
  43. package/src/checks/automatic/definition-list-children-valid.js +7 -8
  44. package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
  45. package/src/checks/automatic/dialog-name-present.js +20 -2
  46. package/src/checks/automatic/duplicate-id-aria.js +5 -3
  47. package/src/checks/automatic/duplicate-id.js +198 -0
  48. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  49. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  50. package/src/checks/automatic/form-control-single-label.js +1 -1
  51. package/src/checks/automatic/iframe-focusable-content.js +63 -7
  52. package/src/checks/automatic/iframe-name-present.js +37 -3
  53. package/src/checks/automatic/iframe-title-unique.js +1 -1
  54. package/src/checks/automatic/img-alt-present.js +12 -4
  55. package/src/checks/automatic/label-in-name.js +172 -18
  56. package/src/checks/automatic/link-in-text-block.js +10 -10
  57. package/src/checks/automatic/link-name-present.js +22 -1
  58. package/src/checks/automatic/list-children-valid.js +6 -6
  59. package/src/checks/automatic/listbox-name-present.js +28 -8
  60. package/src/checks/automatic/listitem-parent-valid.js +4 -4
  61. package/src/checks/automatic/menuitem-name-present.js +20 -2
  62. package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
  63. package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
  64. package/src/checks/automatic/meter-name-present.js +23 -4
  65. package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
  66. package/src/checks/automatic/option-name-present.js +23 -4
  67. package/src/checks/automatic/page-title-present.js +21 -3
  68. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  69. package/src/checks/automatic/progressbar-name-present.js +23 -4
  70. package/src/checks/automatic/role-img-alt-present.js +64 -16
  71. package/src/checks/automatic/searchbox-name-present.js +28 -8
  72. package/src/checks/automatic/server-side-image-map-absent.js +1 -1
  73. package/src/checks/automatic/slider-name-present.js +27 -6
  74. package/src/checks/automatic/spinbutton-name-present.js +28 -8
  75. package/src/checks/automatic/summary-name-present.js +18 -2
  76. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  77. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  78. package/src/checks/automatic/tab-name-present.js +21 -2
  79. package/src/checks/automatic/table-headers-attr-valid.js +43 -8
  80. package/src/checks/automatic/table-th-has-data-cells.js +61 -5
  81. package/src/checks/automatic/target-size-minimum.js +71 -53
  82. package/src/checks/automatic/td-has-header.js +5 -5
  83. package/src/checks/automatic/textbox-name-present.js +28 -8
  84. package/src/checks/automatic/tooltip-name-present.js +21 -2
  85. package/src/checks/automatic/treeitem-name-present.js +23 -4
  86. package/src/checks/automatic/valid-lang.js +92 -7
  87. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  88. package/src/checks/manual/accesskeys-manual.js +3 -3
  89. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  90. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  91. package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
  92. package/src/checks/manual/aria-text-manual.js +4 -4
  93. package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
  94. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  95. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  96. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  97. package/src/checks/manual/empty-heading-manual.js +58 -11
  98. package/src/checks/manual/empty-table-header-manual.js +8 -8
  99. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  100. package/src/checks/manual/form-control-label-quality-manual.js +453 -0
  101. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
  102. package/src/checks/manual/heading-order-manual.js +3 -3
  103. package/src/checks/manual/heading-quality-manual.js +338 -0
  104. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  105. package/src/checks/manual/image-redundant-alt-manual.js +4 -4
  106. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  107. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  108. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  109. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  110. package/src/checks/manual/label-title-only-manual.js +4 -4
  111. package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
  112. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
  113. package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
  114. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
  115. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
  116. package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
  117. package/src/checks/manual/landmark-one-main-manual.js +6 -6
  118. package/src/checks/manual/landmark-unique-manual.js +9 -9
  119. package/src/checks/manual/link-name-quality-manual.js +161 -32
  120. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  121. package/src/checks/manual/meta-viewport-large-manual.js +2 -2
  122. package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
  123. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  124. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  125. package/src/checks/manual/p-as-heading-manual.js +4 -4
  126. package/src/checks/manual/page-has-heading-one-manual.js +6 -6
  127. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  128. package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
  129. package/src/checks/manual/region-manual.js +19 -19
  130. package/src/checks/manual/scope-attr-valid-manual.js +2 -2
  131. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  132. package/src/checks/manual/skip-link-manual.js +5 -5
  133. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  134. package/src/checks/manual/tabindex-manual.js +2 -2
  135. package/src/checks/manual/table-duplicate-name-manual.js +2 -2
  136. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  137. package/src/checks/manual/video-caption-manual.js +3 -3
  138. package/src/checks/manual-review.js +17 -1
  139. package/src/core.js +8965 -1647
  140. package/src/report.js +2 -2
  141. package/surea11y.browser.js +3768 -611
  142. package/surea11y.i18n.de.js +1 -1
  143. package/surea11y.i18n.es.js +1 -1
  144. package/surea11y.i18n.fr.js +1 -1
  145. package/bin/surea11y-core.js +0 -20
@@ -2,6 +2,24 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check menuitem-name-present
7
+ * @atomic true
8
+ * @summary Menu items must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="menuitem", role="menuitemcheckbox" or
13
+ * role="menuitemradio" (the attribute must name one of those roles alone,
14
+ * not a fallback list) that are included in the accessibility tree.
15
+ * @expectation
16
+ * The element has a non-empty accessible name from aria-label, from an
17
+ * aria-labelledby that resolves to non-empty text, from title, or, all
18
+ * three roles being name-from-content, from its own subtree text, where a
19
+ * descendant's own name (an <img alt>, aria-label or title) counts as that
20
+ * descendant's contribution rather than only its text nodes.
21
+ */
22
+
5
23
  const id = 'menuitem-name-present';
6
24
 
7
25
  const meta = {
@@ -52,7 +70,7 @@ function runInPage(ctx) {
52
70
  }
53
71
 
54
72
  function getConservativeSubtreeText(document, container) {
55
- // "Name from content" — recurses into descendants and uses each one's
73
+ // "Name from content", recurses into descendants and uses each one's
56
74
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
57
75
  // it has one, not just literal text nodes. See getContentNameInfo's
58
76
  // header comment in src/core/dom-helpers.js for the full rationale
@@ -71,7 +89,7 @@ function runInPage(ctx) {
71
89
  const raw = getAttr(el, 'aria-labelledby');
72
90
  if (!raw) return '';
73
91
  // Delegates to the shared getTextFromIdRefs helper instead of computing
74
- // name-from-content of the referenced element — see dialog-name-
92
+ // name-from-content of the referenced element, see dialog-name-
75
93
  // present.js's identical fix for the full rationale (an <iframe>
76
94
  // aria-labelledby target's only name source is its title attribute,
77
95
  // which name-from-content alone can never see).
@@ -9,20 +9,24 @@
9
9
  * @standard WCAG 2.2
10
10
  * @sc 2.2.4, 3.2.5
11
11
  * @applicability
12
- * Applies to any <meta http-equiv="refresh"> element with a non-empty
13
- * content attribute.
12
+ * Applies to the first <meta http-equiv="refresh"> element, in document
13
+ * order, with a valid content attribute, per HTML's shared declarative
14
+ * refresh steps, a document only ever acts on its first valid meta
15
+ * refresh, so a later one (valid or not) is inert and out of scope.
14
16
  * @expectation
15
- * No meta refresh is present, regardless of delay. This is the
16
- * stricter AAA-level counterpart of meta-refresh-timing-absent
17
- * (the A-level rule, which only forbids a positive delay and allows
18
- * delay="0" as an immediate redirect). At AAA, WCAG 2.2.4
19
- * (Interruptions) and 3.2.5 (Change on Request) require that automatic
20
- * context changes — including an immediate meta-refresh redirect —
21
- * happen only at the user's request, with no exception for a zero
22
- * delay.
17
+ * Running the shared declarative refresh steps against that element's
18
+ * content value results in a delay of exactly 0. An immediate (delay=0)
19
+ * redirect still passes at AAA, same as the A-level rule. There is
20
+ * nothing for a user to be interrupted mid-read by when nothing is
21
+ * displayed first. Any positive delay fails, with none of the A-level
22
+ * rule's >20-hour exemption: at AAA, WCAG 2.2.4 (Interruptions) and
23
+ * 3.2.5 (Change on Request) require that a *timed* automatic context
24
+ * change happen only at the user's request, regardless of how long the
25
+ * timer is.
23
26
  * @implementation-notes
24
- * - Distinct, atomic decision from meta-refresh-timing-absent:
25
- * that rule's delay="0" pass case is this rule's fail case.
27
+ * - Distinct, atomic decision from meta-refresh-timing-absent: that
28
+ * rule's >20-hour exemption for a positive delay is this rule's fail
29
+ * case (no exemption at AAA), but delay=0 passes in both rules.
26
30
  * - A <meta> nested inside <noscript> is excluded: it only ever takes
27
31
  * effect when scripting is disabled, which is never the case for any
28
32
  * context capable of running accessibility tooling in the first place
@@ -35,7 +39,7 @@ const id = 'meta-refresh-no-exceptions';
35
39
  const meta = {
36
40
  title: 'Page must not use a meta refresh at all (AAA)',
37
41
  description:
38
- 'Checks that <meta http-equiv="refresh"> is not present at all, regardless of delay — the stricter AAA-level counterpart of the A-level positive-delay-only check.',
42
+ 'Checks that <meta http-equiv="refresh"> is not present at all, regardless of delay. This is the stricter AAA-level counterpart of the A-level positive-delay-only check.',
39
43
  i18n: {
40
44
  titleKey: 'metaRefreshNoExceptions_title',
41
45
  descriptionKey: 'metaRefreshNoExceptions_description'
@@ -122,10 +126,16 @@ function runInPage(ctx) {
122
126
  if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled
123
127
  const raw = String(el.getAttribute('content') || '').trim();
124
128
  if (!raw) continue;
125
- if (parseRefreshDelay(raw) === null) continue;
129
+ const delay = parseRefreshDelay(raw);
130
+ if (delay === null) continue;
126
131
 
132
+ // Only the first valid meta refresh in document order is ever acted
133
+ // on by a browser -- any later one is inert and stops being evaluated
134
+ // entirely once a winner has been found.
127
135
  applicableCount += 1;
128
136
 
137
+ if (delay === 0) break; // an immediate redirect is not a timed interruption
138
+
129
139
  occurrences.push(
130
140
  helpers.reportOccurrence(el, {
131
141
  summary:
@@ -141,6 +151,7 @@ function runInPage(ctx) {
141
151
  }
142
152
  })
143
153
  );
154
+ break;
144
155
  }
145
156
 
146
157
  if (applicableCount === 0) {
@@ -9,8 +9,11 @@
9
9
  * @standard WCAG 2.2
10
10
  * @sc 2.2.1
11
11
  * @applicability
12
- * Applies to <meta http-equiv="refresh"> elements that carry a non-empty
13
- * content attribute with a parseable leading delay value.
12
+ * Applies to the first <meta http-equiv="refresh"> element, in document
13
+ * order, whose content attribute has a parseable leading delay value.
14
+ * Per HTML's shared declarative refresh steps, a document only ever
15
+ * acts on its first valid meta refresh; any later one (valid or not)
16
+ * is inert markup a browser never processes, so it is not evaluated.
14
17
  * @expectation
15
18
  * The delay is 0 (an immediate redirect, which users cannot be caught by
16
19
  * mid-read), or exceeds 20 hours. Any other positive delay refreshes or
@@ -19,13 +22,13 @@
19
22
  * be possible.
20
23
  * @implementation-notes
21
24
  * - An unparseable content value (no leading numeric delay) is not
22
- * flagged — this rule only reports a clearly-detected timed refresh,
25
+ * flagged, this rule only reports a clearly-detected timed refresh,
23
26
  * matching this engine's no-false-positives policy.
24
27
  * - WCAG 2.2.1 Exception 3 exempts time limits longer than 20 hours (the
25
28
  * rationale being that a delay this long gives users enough real-world
26
29
  * time to act, so "adjustable" ceases to be a meaningful requirement).
27
30
  * A delay over 72000 seconds is therefore not flagged.
28
- * - A <meta> nested inside <noscript> is excluded — see
31
+ * - A <meta> nested inside <noscript> is excluded, see
29
32
  * meta-refresh-no-exceptions's header comment for why.
30
33
  */
31
34
 
@@ -109,17 +112,20 @@ function runInPage(ctx) {
109
112
 
110
113
  for (const el of nodes) {
111
114
  if (!el || !el.getAttribute) continue;
112
- if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled — see meta-refresh-no-exceptions.js's header comment
115
+ if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled, see meta-refresh-no-exceptions.js's header comment
113
116
  const raw = String(el.getAttribute('content') || '').trim();
114
117
  if (!raw) continue;
115
118
 
116
119
  const delay = parseRefreshDelay(raw);
117
120
  if (delay === null) continue;
118
121
 
122
+ // Only the first valid meta refresh in document order is ever acted
123
+ // on by a browser -- any later one is inert and stops being evaluated
124
+ // entirely once a winner has been found.
119
125
  applicableCount += 1;
120
126
 
121
- if (!(delay > 0)) continue;
122
- if (delay > EXEMPT_DELAY_SECONDS) continue; // WCAG 2.2.1 Exception 3 (>20 hours)
127
+ if (!(delay > 0)) break;
128
+ if (delay > EXEMPT_DELAY_SECONDS) break; // WCAG 2.2.1 Exception 3 (>20 hours)
123
129
 
124
130
  occurrences.push(
125
131
  helpers.reportOccurrence(el, {
@@ -135,6 +141,7 @@ function runInPage(ctx) {
135
141
  }
136
142
  })
137
143
  );
144
+ break;
138
145
  }
139
146
 
140
147
  if (applicableCount === 0) {
@@ -2,6 +2,25 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check meter-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="meter" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 1.1.1
11
+ * @applicability
12
+ * Applies to elements carrying role="meter" (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, or from title.
19
+ * role="meter" is name-from-author-only, so subtree text is never
20
+ * accepted: text sitting inside a custom meter widget is not reliably
21
+ * exposed as its name.
22
+ */
23
+
5
24
  const id = 'meter-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,8 +125,8 @@ function runInPage(ctx) {
106
125
  if (title) return { ok: true, method: 'title' };
107
126
 
108
127
  // role="meter" is name-from-author-only per WAI-ARIA: aria-label,
109
- // aria-labelledby, or title — no content-based naming method at all.
110
- // It must NOT fall back to subtree content — visible text near/inside a
128
+ // aria-labelledby, or title, no content-based naming method at all.
129
+ // It must NOT fall back to subtree content, visible text near/inside a
111
130
  // custom meter widget is not reliably exposed as its accessible name.
112
131
  return { ok: false, method: 'none' };
113
132
  }
@@ -137,7 +156,7 @@ function runInPage(ctx) {
137
156
  occurrences.push(
138
157
  helpers.reportOccurrence(el, {
139
158
  summary: 'This meter has no accessible name.',
140
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this meter's accessible name.",
159
+ hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this meter's accessible name.",
141
160
  i18n: {
142
161
  summaryKey: 'meterNamePresent_summary_fail',
143
162
  hintKey: 'meterNamePresent_hint_fail',
@@ -14,13 +14,13 @@
14
14
  * ARIA widget role: button, link, checkbox, radio, switch, tab, textbox,
15
15
  * combobox, listbox, menuitem, menuitemcheckbox, menuitemradio, option,
16
16
  * slider, spinbutton, searchbox, treeitem). The container is applicable
17
- * regardless of whether it is itself focusable — focusability is only
17
+ * regardless of whether it is itself focusable, focusability is only
18
18
  * used to decide whether a *descendant* nests an interactive control.
19
19
  * @expectation
20
20
  * The element does not contain, as a descendant, another *operable*
21
21
  * interactive control (e.g. a <button> wrapping a <select>, or a link
22
22
  * containing a checkbox). Nested interactive controls are not reliably
23
- * announced or operable via assistive technology — activating the outer
23
+ * announced or operable via assistive technology, activating the outer
24
24
  * control and the inner one become ambiguous, and some AT only exposes
25
25
  * one of the two.
26
26
  * @implementation-notes
@@ -89,7 +89,7 @@ const meta = {
89
89
  function runInPage(ctx) {
90
90
  const { helpers, rule } = ctx;
91
91
 
92
- // Declared inside runInPage — see scripts/build-core.js header
92
+ // Declared inside runInPage, see scripts/build-core.js header
93
93
  // ("runInPage MUST be self-contained").
94
94
  const INTERACTIVE_SELECTOR = [
95
95
  'a[href]',
@@ -228,8 +228,8 @@ function runInPage(ctx) {
228
228
  // A composite-owned child (option in a listbox/combobox, tab in a
229
229
  // tablist, ...) is not a nested interactive control: its container
230
230
  // owns it and drives its focus (roving tabindex or
231
- // aria-activedescendant). Treat it as an attribution boundary — do not
232
- // count it, and do not descend past it. Any control genuinely nested
231
+ // aria-activedescendant). Treat it as an attribution boundary, do not
232
+ // count it, and do not descend past it. Any control nested
233
233
  // inside it is attributed to the child itself (examined as its own
234
234
  // container in the main loop), keeping the report at the nearest
235
235
  // interactive ancestor rather than bubbling up to the composite.
@@ -2,11 +2,30 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check option-name-present
7
+ * @atomic true
8
+ * @summary Elements with role="option" must have an accessible name
9
+ * @standard WCAG 2.2
10
+ * @sc 4.1.2
11
+ * @applicability
12
+ * Applies to elements carrying role="option" (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="option" being name-from-content, from its own subtree text, where
20
+ * a descendant's own name (an <img alt>, aria-label or title) counts as
21
+ * that descendant's contribution rather than only its text nodes.
22
+ */
23
+
5
24
  const id = 'option-name-present';
6
25
 
7
26
  const meta = {
8
- title: 'Accessible name is present',
9
- description: 'Checks that elements expose a non-empty accessible name.',
27
+ title: 'Options have an accessible name',
28
+ description: 'Checks that elements with role="option" expose a non-empty accessible name.',
10
29
  i18n: {
11
30
  titleKey: 'optionNamePresent_title',
12
31
  descriptionKey: 'optionNamePresent_description'
@@ -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).
@@ -2,11 +2,29 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @check page-title-present
7
+ * @atomic true
8
+ * @summary The document must have a non-empty title
9
+ * @standard WCAG 2.2
10
+ * @sc 2.4.2
11
+ * @applicability
12
+ * Applies to a run over a whole document. A run narrowed by
13
+ * contextSelector, or by engineOptions.fragment, is notApplicable: whether
14
+ * the page has a title is not a property any subtree can answer.
15
+ * @expectation
16
+ * The document has a <title> element, and document.title with whitespace
17
+ * collapsed is non-empty. The element is looked for anywhere in the
18
+ * document, not only inside <head>: a <title> the parser leaves outside
19
+ * <head> is still the document title in every browser. Whether that title
20
+ * describes the page is page-title-patterns' question.
21
+ */
22
+
5
23
  const id = 'page-title-present';
6
24
 
7
25
  const meta = {
8
- title: 'Page title is present and non-empty',
9
- description: 'Checks that the document has a non-empty <title> element (WCAG 2.2 SC 2.4.2).',
26
+ title: 'Page has a non-empty title',
27
+ description: 'Checks that the page includes a non-empty <title> element.',
10
28
  i18n: {
11
29
  titleKey: 'pageTitlePresent_title',
12
30
  descriptionKey: 'pageTitlePresent_description'
@@ -47,7 +65,7 @@ function runInPage(ctx) {
47
65
  // Not restricted to `head > title`: per HTML parsing, a <title> encountered
48
66
  // after <head> has closed is not re-parented into <head>, yet the browser
49
67
  // (and document.title, used below) still recognizes it as the document's
50
- // title — matching that here avoids a false "missing title" report for a
68
+ // title, matching that here avoids a false "missing title" report for a
51
69
  // <title> that legitimately ended up outside <head>.
52
70
  const titleEl = document.querySelector('title');
53
71
  const titleText = (document.title || '').replace(/\s+/g, ' ').trim();