@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.
- package/CHANGELOG.md +193 -149
- package/README.md +27 -6
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +16 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +6 -4
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +53 -25
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +3 -7
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +68 -10
- package/src/checks/automatic/aria-allowed-role.js +2 -2
- package/src/checks/automatic/aria-braille-equivalent.js +3 -3
- package/src/checks/automatic/aria-conditional-attr.js +5 -5
- package/src/checks/automatic/aria-deprecated-role.js +1 -1
- package/src/checks/automatic/aria-hidden-body.js +2 -2
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +18 -18
- package/src/checks/automatic/aria-prohibited-children.js +130 -37
- package/src/checks/automatic/aria-required-attr.js +60 -12
- package/src/checks/automatic/aria-required-children.js +21 -14
- package/src/checks/automatic/aria-required-parent.js +61 -9
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-valid-attr-value.js +15 -12
- package/src/checks/automatic/aria-valid-attr.js +1 -1
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- package/src/checks/automatic/contrast-computable.js +32 -0
- package/src/checks/automatic/contrast-enhanced.js +21 -1
- package/src/checks/automatic/contrast-minimum.js +21 -1
- package/src/checks/automatic/css-orientation-lock.js +96 -19
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +5 -3
- package/src/checks/automatic/duplicate-id.js +198 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +1 -1
- package/src/checks/automatic/iframe-focusable-content.js +63 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +172 -18
- package/src/checks/automatic/link-in-text-block.js +10 -10
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +23 -4
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +71 -53
- package/src/checks/automatic/td-has-header.js +5 -5
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +453 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +4 -4
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +8965 -1647
- package/src/report.js +2 -2
- package/surea11y.browser.js +3768 -611
- package/surea11y.i18n.de.js +1 -1
- package/surea11y.i18n.es.js +1 -1
- package/surea11y.i18n.fr.js +1 -1
- 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
|
|
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
|
|
40
|
+
title: 'ARIA roles that require an accessible name have one',
|
|
20
41
|
description:
|
|
21
|
-
'Checks that
|
|
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
|
|
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
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 (!
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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)
|
|
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)
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
-
|
|
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)
|
|
103
|
-
// subtree content
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: {
|