@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
@@ -3,7 +3,8 @@
3
3
  'use strict';
4
4
 
5
5
  /**
6
- * Generic name-presence rule for selected ARIA composite/widget roles that should expose an accessible name.
6
+ * Generic name-presence rule for the ARIA roles WAI-ARIA *requires* an
7
+ * accessible name for.
7
8
  *
8
9
  * Notes:
9
10
  * - This rule intentionally focuses on author-provided naming mechanisms:
@@ -11,14 +12,34 @@
11
12
  * - It does NOT treat descendant text content as a valid name source for these roles
12
13
  * (to avoid false passes from labelled children inside composite widgets).
13
14
  * - Eligibility is based on helpers.isAccTreeEligible(node, ctx) per engine checks.
15
+ * - The role set is generated from aria-query by
16
+ * scripts/generate-aria-tables.js; see that script for why each role is in
17
+ * or out, and for the name-required roles no rule covers yet.
18
+ *
19
+ * @applicability
20
+ * Applies to elements whose role attribute is exactly one of grid, meter,
21
+ * progressbar, radiogroup or tree, and that are included in the
22
+ * accessibility tree. Membership is decided by WAI-ARIA's own "Accessible
23
+ * Name Required: True" characteristic, not by whether a role merely permits
24
+ * a name: tablist, toolbar, menu, menubar and scrollbar are name-from-author
25
+ * roles the spec does not require a name for, and are out of
26
+ * scope. meter and progressbar are also covered by meter-name-present and
27
+ * progressbar-name-present, which map to SC 1.1.1; this rule is what gives
28
+ * those two roles their 4.1.2 coverage.
29
+ * @expectation
30
+ * The element has a non-empty aria-label, an aria-labelledby that resolves
31
+ * to non-empty text, or a non-empty title. Every role in the set is
32
+ * name-from-author-only, so descendant text is not accepted:
33
+ * a labelled child inside a composite widget would otherwise pass the
34
+ * container that has no name of its own.
14
35
  */
15
36
 
16
37
  const id = 'aria-role-name-present';
17
38
 
18
39
  const meta = {
19
- title: 'ARIA widget/container roles have an accessible name',
40
+ title: 'ARIA roles that require an accessible name have one',
20
41
  description:
21
- 'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
42
+ 'Checks that the ARIA roles WAI-ARIA requires an accessible name for expose a non-empty one.',
22
43
  i18n: {
23
44
  titleKey: 'ariaRoleNamePresent_title',
24
45
  descriptionKey: 'ariaRoleNamePresent_description'
@@ -93,7 +114,7 @@ function runInPage(ctx) {
93
114
  // button-name-present et al.), which correctly includes hidden/aria-hidden
94
115
  // referenced nodes per the Accessible Name and Description Computation
95
116
  // spec (a hidden node directly referenced by aria-labelledby still
96
- // supplies its text — this is a standard visually-hidden-label pattern).
117
+ // supplies its text, this is a standard visually-hidden-label pattern).
97
118
  const resolveLabelledby = (el, maxRefs) => {
98
119
  if (!getAriaLabelledByInfo) return '';
99
120
  try {
@@ -115,23 +136,16 @@ function runInPage(ctx) {
115
136
  }
116
137
  };
117
138
 
118
- // Frozen allowlist of roles to check in this generic rule.
119
- // (Keep this small, explicit, and standards-based for determinism.)
120
- const roleSet = new Set([
121
- 'scrollbar',
122
- 'toolbar',
123
- 'tablist',
124
- 'radiogroup',
125
- 'tree',
126
- 'grid',
127
- 'menu',
128
- 'menubar',
129
- 'meter',
130
- 'progressbar'
131
- ]);
132
-
133
- const selector =
134
- '[role="scrollbar"],[role="toolbar"],[role="tablist"],[role="radiogroup"],[role="tree"],[role="grid"],[role="menu"],[role="menubar"],[role="meter"],[role="progressbar"]';
139
+ // Roles WAI-ARIA marks "Accessible Name Required: True" and names from the
140
+ // author only. Generated from aria-query by scripts/generate-aria-tables.js:
141
+ // a role that merely *allows* an author name (tablist, toolbar, menu,
142
+ // menubar, scrollbar) is not a 4.1.2 failure when unnamed and is not listed.
143
+ // <generated:aria-name-required-roles>
144
+ const NAME_REQUIRED_ROLES = new Set(['grid', 'meter', 'progressbar', 'radiogroup', 'tree']);
145
+ // </generated:aria-name-required-roles>
146
+
147
+ // Derived from the set above so the two cannot drift apart.
148
+ const selector = [...NAME_REQUIRED_ROLES].map((r) => `[role="${r}"]`).join(',');
135
149
 
136
150
  const nodes = (() => {
137
151
  try {
@@ -161,7 +175,7 @@ function runInPage(ctx) {
161
175
  return '';
162
176
  }
163
177
  })();
164
- if (!roleSet.has(role)) continue;
178
+ if (!NAME_REQUIRED_ROLES.has(role)) continue;
165
179
 
166
180
  if (!isEligible(el)) continue;
167
181
 
@@ -15,9 +15,14 @@
15
15
  * @expectation
16
16
  * Each attribute's value conforms to its WAI-ARIA-declared value type:
17
17
  * boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
18
- * from a fixed enumerated set, an integer, a real number, or an empty
19
- * value or ID reference (list) that resolves to an existing element in
20
- * the document.
18
+ * from a fixed enumerated set, an integer, a real number, or an ID
19
+ * reference (list) that resolves to an existing element in the document.
20
+ * Per ACT 6a7281's own applicability ("any state or property that is
21
+ * NOT empty"), an explicitly empty value, including a bare boolean-style
22
+ * attribute with no "=value" at all, e.g. `aria-checked` alone, is out
23
+ * of scope for every value type, not a violation: a common, deliberate
24
+ * pattern in templated markup (e.g. React conditionally rendering
25
+ * `aria-describedby={hasError ? errorId : ''}`).
21
26
  * @implementation-notes
22
27
  * - Not rule-gated on isAccTreeEligible: this remains a static-markup
23
28
  * property, while engine-level hidden-subtree filtering still applies
@@ -25,16 +30,14 @@
25
30
  * - ID-reference resolution (see aria-helpers.js's idExists) only flags
26
31
  * idref-list attributes (aria-labelledby, aria-describedby,
27
32
  * aria-controls, aria-owns, etc.) when NONE of the space-separated ids
28
- * resolve — a partially-dangling list (some ids exist, some don't) is
33
+ * resolve, a partially-dangling list (some ids exist, some don't) is
29
34
  * left unflagged (only invalidate when every token fails to resolve).
30
- * Single-idref attributes (aria-activedescendant, aria-errormessage) are
31
- * flagged whenever their one id doesn't resolve.
32
- * - An explicitly-EMPTY idref/idref-list value (e.g.
33
- * `aria-describedby=""`) is valid, not a violation — `allowEmpty` holds
34
- * for every idref/idref-list ARIA attribute. A common, deliberate pattern
35
- * in templated markup (e.g. React conditionally rendering
36
- * `aria-describedby={hasError ? errorId : ''}`); flagging it would be a
37
- * false positive.
35
+ * Of the two single-idref attributes, aria-activedescendant is flagged
36
+ * whenever its one id doesn't resolve; aria-errormessage's existence is
37
+ * never checked (format only), ACT 6a7281's own Background
38
+ * text names it as a non-required property whose target "may be created
39
+ * in response to an event that may or may not happen" (a validation
40
+ * error message rendered only once the error actually occurs).
38
41
  */
39
42
 
40
43
  const id = 'aria-valid-attr-value';
@@ -18,7 +18,7 @@
18
18
  * deterministic defect).
19
19
  * @implementation-notes
20
20
  * - Distinct from aria-valid-attr-value (which validates the VALUE
21
- * of a recognized attribute) — this rule only validates the attribute
21
+ * of a recognized attribute), this rule only validates the attribute
22
22
  * NAME.
23
23
  * - Not rule-gated on isAccTreeEligible: this remains a static-markup
24
24
  * property, while engine-level hidden-subtree filtering still applies
@@ -26,7 +26,7 @@
26
26
  * order) with the full fixed field-name vocabulary from the HTML
27
27
  * Standard, rather than validating every field-specific constraint
28
28
  * (e.g. which contact-modality tokens are legal for which field
29
- * names) — matches this engine's established "deliberately scoped"
29
+ * names), matches this engine's established "scoped"
30
30
  * precedent (see aria-helpers.js) for keeping high-confidence fail
31
31
  * without reimplementing the entire spec.
32
32
  */
@@ -63,7 +63,7 @@ const meta = {
63
63
  function runInPage(ctx) {
64
64
  const { helpers, rule } = ctx;
65
65
 
66
- // Declared inside runInPage — see scripts/build-core.js header
66
+ // Declared inside runInPage, see scripts/build-core.js header
67
67
  // ("runInPage MUST be self-contained").
68
68
  const FIELD_NAMES = new Set([
69
69
  'name',
@@ -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).
@@ -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
  );
@@ -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).
@@ -235,7 +255,7 @@ function runInPage(ctx) {
235
255
  occurrences.push(
236
256
  helpers.reportOccurrence(el, {
237
257
  summary: 'This combobox has no accessible name.',
238
- hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this combobox's 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.",
239
259
  i18n: {
240
260
  summaryKey: 'comboboxNamePresent_summary_fail',
241
261
  hintKey: 'comboboxNamePresent_hint_fail',
@@ -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: {