@surea11y/core 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +212 -128
- package/README.md +46 -9
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +4 -4
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +30 -14
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +29 -7
- package/docs/LIMITATIONS.md +6 -4
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +3 -1
- package/docs/RULE_AUTHORING.md +104 -23
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +4 -4
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +8 -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 +95 -40
- package/src/checks/automatic/aria-allowed-role.js +16 -18
- package/src/checks/automatic/aria-braille-equivalent.js +19 -21
- package/src/checks/automatic/aria-conditional-attr.js +22 -24
- package/src/checks/automatic/aria-deprecated-role.js +63 -50
- package/src/checks/automatic/aria-hidden-body.js +4 -11
- package/src/checks/automatic/aria-hidden-focus.js +104 -23
- package/src/checks/automatic/aria-prohibited-attr.js +71 -72
- package/src/checks/automatic/aria-prohibited-children.js +154 -61
- package/src/checks/automatic/aria-required-attr.js +74 -29
- package/src/checks/automatic/aria-required-children.js +38 -34
- package/src/checks/automatic/aria-required-parent.js +78 -29
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +33 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -18
- package/src/checks/automatic/autocomplete-valid.js +17 -19
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +46 -26
- package/src/checks/automatic/button-name-present.js +115 -34
- package/src/checks/automatic/combobox-name-present.js +40 -22
- 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 +118 -41
- package/src/checks/automatic/definition-list-children-valid.js +25 -29
- package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
- package/src/checks/automatic/dialog-name-present.js +36 -20
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +50 -40
- 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 +39 -41
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +92 -38
- package/src/checks/automatic/iframe-name-present.js +53 -21
- package/src/checks/automatic/iframe-title-unique.js +19 -24
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +198 -49
- package/src/checks/automatic/link-in-text-block.js +29 -31
- package/src/checks/automatic/link-name-present.js +47 -31
- package/src/checks/automatic/list-children-valid.js +21 -23
- package/src/checks/automatic/listbox-name-present.js +42 -24
- package/src/checks/automatic/listitem-parent-valid.js +18 -21
- package/src/checks/automatic/menuitem-name-present.js +36 -20
- package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
- package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +38 -21
- package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
- package/src/checks/automatic/option-name-present.js +39 -22
- 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 +41 -24
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +46 -24
- package/src/checks/automatic/server-side-image-map-absent.js +16 -19
- package/src/checks/automatic/slider-name-present.js +42 -23
- package/src/checks/automatic/spinbutton-name-present.js +46 -24
- package/src/checks/automatic/summary-name-present.js +34 -20
- 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 +37 -20
- package/src/checks/automatic/table-headers-attr-valid.js +57 -24
- package/src/checks/automatic/table-th-has-data-cells.js +76 -24
- package/src/checks/automatic/target-size-minimum.js +172 -131
- package/src/checks/automatic/td-has-header.js +20 -25
- package/src/checks/automatic/textbox-name-present.js +42 -24
- package/src/checks/automatic/tooltip-name-present.js +37 -20
- package/src/checks/automatic/treeitem-name-present.js +39 -22
- package/src/checks/automatic/valid-lang.js +107 -24
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +21 -22
- 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 +23 -26
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
- 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 +73 -28
- package/src/checks/manual/empty-table-header-manual.js +33 -36
- 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 +4 -11
- package/src/checks/manual/heading-order-manual.js +20 -25
- 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 +18 -21
- 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 +19 -21
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
- package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
- package/src/checks/manual/landmark-one-main-manual.js +8 -15
- package/src/checks/manual/landmark-unique-manual.js +31 -36
- package/src/checks/manual/link-name-quality-manual.js +162 -35
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +16 -19
- package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
- 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 +8 -15
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
- package/src/checks/manual/region-manual.js +32 -25
- package/src/checks/manual/scope-attr-valid-manual.js +16 -19
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +44 -52
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +16 -19
- package/src/checks/manual/table-duplicate-name-manual.js +16 -19
- 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 +13086 -5169
- package/src/report.js +16 -2
- package/surea11y.browser.js +5665 -4219
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
- package/bin/surea11y-core.js +0 -20
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check binary-control-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Checkboxes, radios and switches must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="checkbox", role="radio" or
|
|
13
|
+
* role="switch" (the attribute must name one of those roles alone, not a
|
|
14
|
+
* fallback list) that are included in the accessibility tree. A native
|
|
15
|
+
* <input type="checkbox">/<input type="radio"> is in scope only when it
|
|
16
|
+
* carries one of those roles explicitly; without a role attribute it
|
|
17
|
+
* belongs to form-control-programmatic-label-present.
|
|
18
|
+
* @expectation
|
|
19
|
+
* The control has a non-empty accessible name from aria-label, from an
|
|
20
|
+
* aria-labelledby that resolves to non-empty text, or from title. A native
|
|
21
|
+
* checkbox or radio additionally accepts an associated <label>, the
|
|
22
|
+
* labels API, a wrapping <label>, or label[for], with at most four labels
|
|
23
|
+
* read for determinism, and any other element accepts its own subtree
|
|
24
|
+
* text, those roles being name-from-content.
|
|
25
|
+
*/
|
|
26
|
+
|
|
5
27
|
const id = 'binary-control-name-present';
|
|
6
28
|
|
|
7
29
|
const meta = {
|
|
@@ -79,7 +101,7 @@ function runInPage(ctx) {
|
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
function getConservativeSubtreeText(document, container) {
|
|
82
|
-
// "Name from content"
|
|
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).
|
|
@@ -272,28 +294,26 @@ function runInPage(ctx) {
|
|
|
272
294
|
}
|
|
273
295
|
})()
|
|
274
296
|
: null;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
summaryKey: 'binaryControlNamePresent_summary_fail',
|
|
285
|
-
hintKey: 'binaryControlNamePresent_hint_fail',
|
|
286
|
-
params: { controlType }
|
|
287
|
-
},
|
|
288
|
-
data: {
|
|
289
|
-
details: {
|
|
290
|
-
reasonCode: 'name_missing',
|
|
291
|
-
controlType,
|
|
292
|
-
methodTried: res.method
|
|
297
|
+
|
|
298
|
+
occurrences.push(
|
|
299
|
+
helpers.reportOccurrence(el, {
|
|
300
|
+
summary: 'This control has no accessible name.',
|
|
301
|
+
hint: 'Provide a label, aria-label, aria-labelledby, or other accessible-name mechanism so assistive technologies can identify the control.',
|
|
302
|
+
i18n: {
|
|
303
|
+
summaryKey: 'binaryControlNamePresent_summary_fail',
|
|
304
|
+
hintKey: 'binaryControlNamePresent_hint_fail',
|
|
305
|
+
params: { controlType }
|
|
293
306
|
},
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
307
|
+
data: {
|
|
308
|
+
details: {
|
|
309
|
+
reasonCode: 'name_missing',
|
|
310
|
+
controlType,
|
|
311
|
+
methodTried: res.method
|
|
312
|
+
},
|
|
313
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
);
|
|
297
317
|
}
|
|
298
318
|
|
|
299
319
|
if (applicableCount === 0) {
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check button-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Buttons must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to <button>, <input type="button">, <input type="submit">,
|
|
13
|
+
* <input type="reset"> and elements with role="button", where the element
|
|
14
|
+
* is included in the accessibility tree. role="presentation"/"none" takes
|
|
15
|
+
* an element out of scope unless a global ARIA attribute or focusability
|
|
16
|
+
* restores its role, per presentational roles conflict resolution.
|
|
17
|
+
* @expectation
|
|
18
|
+
* The element has a non-empty accessible name. A programmatic name is
|
|
19
|
+
* taken first (aria-labelledby, aria-label, an associated <label>, title).
|
|
20
|
+
* Failing that, an <input> button falls back to its value attribute, and
|
|
21
|
+
* type="submit"/type="reset" fall back to the user agent's own
|
|
22
|
+
* "Submit"/"Reset" default, which is why those two are never nameless.
|
|
23
|
+
* Failing both, a button whose role is name-from-content falls back to its
|
|
24
|
+
* subtree text, counting each descendant's own name (an <img alt>,
|
|
25
|
+
* aria-label or title) rather than only text nodes.
|
|
26
|
+
*/
|
|
27
|
+
|
|
5
28
|
// NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
|
|
6
29
|
// File name intentionally has no prefix, per request.
|
|
7
30
|
const id = 'button-name-present';
|
|
@@ -45,7 +68,7 @@ function runInPage(ctx) {
|
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
function getConservativeSubtreeText(container) {
|
|
48
|
-
// "Name from content"
|
|
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
|
);
|
|
@@ -170,35 +256,30 @@ function runInPage(ctx) {
|
|
|
170
256
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
171
257
|
: null;
|
|
172
258
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
inputValueNameLength: inputValueName.length,
|
|
195
|
-
contentNameLength: contentName.length,
|
|
196
|
-
explicitProgrammatic: explicitProg ? 1 : 0
|
|
197
|
-
},
|
|
198
|
-
refs: { accessibleName: nameInfo || null }
|
|
259
|
+
occurrences.push(
|
|
260
|
+
helpers.reportOccurrence(el, {
|
|
261
|
+
summary: 'This button has no accessible name.',
|
|
262
|
+
hint: 'Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.',
|
|
263
|
+
i18n: {
|
|
264
|
+
summaryKey: 'buttonNamePresent_summary_fail',
|
|
265
|
+
hintKey: 'buttonNamePresent_hint_fail',
|
|
266
|
+
params: { element: tag }
|
|
267
|
+
},
|
|
268
|
+
data: {
|
|
269
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
270
|
+
details: {
|
|
271
|
+
reasonCode: 'name_missing',
|
|
272
|
+
metrics: {
|
|
273
|
+
trustedProgrammaticNameLength: trustedProgrammaticName.length,
|
|
274
|
+
inputValueNameLength: inputValueName.length,
|
|
275
|
+
contentNameLength: contentName.length,
|
|
276
|
+
explicitProgrammatic: explicitProg ? 1 : 0
|
|
277
|
+
},
|
|
278
|
+
refs: { accessibleName: nameInfo || null }
|
|
279
|
+
}
|
|
199
280
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
281
|
+
})
|
|
282
|
+
);
|
|
202
283
|
}
|
|
203
284
|
}
|
|
204
285
|
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check combobox-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="combobox" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="combobox" (the attribute must name
|
|
13
|
+
* that role alone, not a fallback list) that are included in the
|
|
14
|
+
* accessibility tree. An element with the matching implicit role but no
|
|
15
|
+
* role attribute is out of scope.
|
|
16
|
+
* @expectation
|
|
17
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
18
|
+
* aria-labelledby that resolves to non-empty text, or from title.
|
|
19
|
+
* role="combobox" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom combobox widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<input role="combobox">) an
|
|
22
|
+
* associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'combobox-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
@@ -66,7 +86,7 @@ function runInPage(ctx) {
|
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
function getConservativeSubtreeText(document, container) {
|
|
69
|
-
// "Name from content"
|
|
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).
|
|
@@ -231,24 +251,22 @@ function runInPage(ctx) {
|
|
|
231
251
|
}
|
|
232
252
|
})()
|
|
233
253
|
: null;
|
|
234
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
235
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
236
254
|
|
|
237
|
-
occurrences.push(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
255
|
+
occurrences.push(
|
|
256
|
+
helpers.reportOccurrence(el, {
|
|
257
|
+
summary: 'This combobox has no accessible name.',
|
|
258
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this combobox's accessible name.",
|
|
259
|
+
i18n: {
|
|
260
|
+
summaryKey: 'comboboxNamePresent_summary_fail',
|
|
261
|
+
hintKey: 'comboboxNamePresent_hint_fail',
|
|
262
|
+
params: {}
|
|
263
|
+
},
|
|
264
|
+
data: {
|
|
265
|
+
details: { reasonCode: 'name_missing', controlType: 'combobox', methodTried: res.method },
|
|
266
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
);
|
|
252
270
|
}
|
|
253
271
|
|
|
254
272
|
if (applicableCount === 0) {
|
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check contrast-computable
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Color contrast must be computable from CSS for rendered text
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 1.4.3
|
|
11
|
+
* @sc 1.4.6
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to every visible text node in scope, plus the label of <input
|
|
14
|
+
* type="button">/[type="submit"]/[type="reset"], which is rendered from
|
|
15
|
+
* the value attribute and so is invisible to a text-node walk. Text counts
|
|
16
|
+
* only when its element is DOM-visible under the run's visibility mode, is
|
|
17
|
+
* not clipped out of sight by the sr-only technique (clip or clip-path),
|
|
18
|
+
* and belongs neither to a disabled control nor to the label of one,
|
|
19
|
+
* WCAG's inactive-user-interface-component exception. Subtrees excluded
|
|
20
|
+
* via engineOptions.excludeSelectors are skipped, and open shadow roots
|
|
21
|
+
* are walked as roots in their own right.
|
|
22
|
+
* @expectation
|
|
23
|
+
* Both sides of the contrast calculation can be established from CSS for
|
|
24
|
+
* every applicable text node: an effective background resolving to an
|
|
25
|
+
* opaque color, and a parsable foreground color. Where either cannot be,
|
|
26
|
+
* a background image or gradient, mix-blend-mode, a filter or
|
|
27
|
+
* backdrop-filter, a text-shadow (which may add contrast this engine has
|
|
28
|
+
* no glyph-rendering model to account for), ancestor opacity, a root
|
|
29
|
+
* background that never becomes opaque, or a color that does not parse,
|
|
30
|
+
* the result is cantTell naming the blocker. This rule is the one that
|
|
31
|
+
* reports that uncertainty, which is what lets contrast-minimum and
|
|
32
|
+
* contrast-enhanced stay silent on the same text instead of guessing at
|
|
33
|
+
* a ratio.
|
|
34
|
+
*/
|
|
35
|
+
|
|
5
36
|
const id = 'contrast-computable';
|
|
6
37
|
|
|
7
38
|
const meta = {
|
|
@@ -118,6 +149,7 @@ function runInPage(ctx) {
|
|
|
118
149
|
else summaryKey = 'contrastComputable_cantTell_filterOrBackdropFilter';
|
|
119
150
|
} else if (rc === 'BACKGROUND_NOT_OPAQUE_AT_ROOT')
|
|
120
151
|
summaryKey = 'contrastComputable_cantTell_rootNotOpaque';
|
|
152
|
+
else if (rc === 'TEXT_SHADOW') summaryKey = 'contrastComputable_cantTell_textShadow';
|
|
121
153
|
|
|
122
154
|
const details = Object.assign(
|
|
123
155
|
{ reasonCode: rc },
|
|
@@ -2,10 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check contrast-enhanced
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Text must meet the WCAG AAA contrast ratio for its size
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 1.4.6
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to the visible text contrast-computable applies to, see that
|
|
13
|
+
* rule for the eligibility gates, narrowed to text whose background and
|
|
14
|
+
* foreground are actually computable. Eligible text that is not computable
|
|
15
|
+
* leaves this rule notApplicable rather than cantTell: reporting that
|
|
16
|
+
* uncertainty belongs to contrast-computable, so the two never report the
|
|
17
|
+
* same text twice.
|
|
18
|
+
* @expectation
|
|
19
|
+
* Every computable text node reaches the ratio SC 1.4.6 requires for its
|
|
20
|
+
* size: 4.5:1 for large text, 7:1 for everything else. Text is large at
|
|
21
|
+
* 24px or more, or at 14pt (about 18.667px) or more when the computed font
|
|
22
|
+
* weight is 700 or higher.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'contrast-enhanced';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: 'Text
|
|
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: {
|