@surea11y/core 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- 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 +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- 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 +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -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 +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -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 +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- 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 → role-img-text-alternative-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 +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- 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 +563 -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-complementary-is-top-level-manual.js +231 -0
- 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/password-paste-enabled-manual.js +255 -0
- 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 +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/bin/surea11y-core.js +0 -20
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @check password-paste-enabled
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary An authentication field must not block pasting into it
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 3.3.8
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to any control whose autocomplete token is current-password,
|
|
13
|
+
* new-password or one-time-code, and to <input type="password"> unless its
|
|
14
|
+
* autocomplete names another purpose. A disabled or readonly field takes no
|
|
15
|
+
* input to block, and one outside the accessibility tree is not being asked
|
|
16
|
+
* for, so neither is in scope.
|
|
17
|
+
* @expectation
|
|
18
|
+
* A reviewer confirms the field can still be pasted into. Remembering a
|
|
19
|
+
* password is a cognitive function test, and 3.3.8 asks for a mechanism
|
|
20
|
+
* that helps the user through one; a password manager, or the clipboard
|
|
21
|
+
* for a one-time code, is that mechanism.
|
|
22
|
+
* @implementation-notes
|
|
23
|
+
* - Advisory and capped at cantTell. Whether a handler really stops the user
|
|
24
|
+
* depends on script the markup does not carry, so no reading of it is safe
|
|
25
|
+
* enough to fail on: a split one-time-code field cancels the default paste
|
|
26
|
+
* and then spreads the digits across its boxes. The two cases are reported
|
|
27
|
+
* apart -- a handler that only cancels, and one that goes on to do more --
|
|
28
|
+
* so a reviewer knows which to look at first. `return true` allows the
|
|
29
|
+
* paste and reports nothing.
|
|
30
|
+
* - type="password" masks rather than authenticates, and a card security code
|
|
31
|
+
* is masked the same way, so an autocomplete naming another purpose puts the
|
|
32
|
+
* field out of scope. A masked field with no autocomplete cannot be told
|
|
33
|
+
* apart, which is the one case left.
|
|
34
|
+
* - `autocomplete="off"` is not reported. Browsers override it for password
|
|
35
|
+
* fields so password managers keep working, and the mechanism survives.
|
|
36
|
+
* - A handler attached with addEventListener leaves nothing to read, the
|
|
37
|
+
* limit every static scan shares.
|
|
38
|
+
* - 3.3.8 also excuses the test where the process offers another
|
|
39
|
+
* authentication method. A sibling button proves nothing about whether it
|
|
40
|
+
* works or replaces this step, so it is not inferred; the facet is recorded
|
|
41
|
+
* as partial for that reason.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const id = 'password-paste-enabled';
|
|
45
|
+
|
|
46
|
+
const meta = {
|
|
47
|
+
title: 'Authentication fields must not block pasting',
|
|
48
|
+
description:
|
|
49
|
+
'Checks that a password or one-time-code field carries no inline paste handler that cancels the paste, which would remove the password manager or clipboard that WCAG 3.3.8 relies on as the assisting mechanism.',
|
|
50
|
+
i18n: {
|
|
51
|
+
titleKey: 'passwordPasteEnabled_title',
|
|
52
|
+
descriptionKey: 'passwordPasteEnabled_description'
|
|
53
|
+
},
|
|
54
|
+
helpUrl: null,
|
|
55
|
+
tags: ['wcag22aa', 'wcag338', 'forms', 'authentication', 'atomic', 'manual', 'acc'],
|
|
56
|
+
wcagSc: ['3.3.8'],
|
|
57
|
+
normativeMappings: [
|
|
58
|
+
{
|
|
59
|
+
standard: 'WCAG',
|
|
60
|
+
version: '2.2',
|
|
61
|
+
requirement: '3.3.8',
|
|
62
|
+
title: 'Accessible Authentication (Minimum)',
|
|
63
|
+
conformanceLevel: 'AA'
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
defaultSeverity: 'serious',
|
|
67
|
+
category: 'understandable',
|
|
68
|
+
type: 'manual',
|
|
69
|
+
defaultConfidence: 'medium',
|
|
70
|
+
coverage: { facetsBySc: { '3.3.8': ['authentication-paste-not-blocked'] } }
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function runInPage(ctx) {
|
|
74
|
+
const { helpers, rule } = ctx;
|
|
75
|
+
|
|
76
|
+
// Declared inside runInPage; see scripts/build-core.js header
|
|
77
|
+
// ("runInPage MUST be self-contained").
|
|
78
|
+
const AUTH_AUTOCOMPLETE_TOKENS = ['current-password', 'new-password', 'one-time-code'];
|
|
79
|
+
|
|
80
|
+
function normalizeWs(s) {
|
|
81
|
+
return String(s || '')
|
|
82
|
+
.replace(/\s+/g, ' ')
|
|
83
|
+
.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function autocompleteTokens(el) {
|
|
87
|
+
return normalizeWs(el.getAttribute && el.getAttribute('autocomplete'))
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.split(' ')
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A field an authentication step reads: a password input, or any control
|
|
94
|
+
// whose autocomplete token names an authentication secret. The autocomplete
|
|
95
|
+
// route matters for one-time codes, which are ordinary text inputs.
|
|
96
|
+
//
|
|
97
|
+
// type="password" is a masking control, not only an authentication one: a
|
|
98
|
+
// card security code is routinely masked the same way. An explicit
|
|
99
|
+
// autocomplete purpose that is not an authentication one says so, and takes
|
|
100
|
+
// the field back out of scope.
|
|
101
|
+
function isAuthField(el) {
|
|
102
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
103
|
+
if (tag !== 'input' && tag !== 'textarea') return false;
|
|
104
|
+
|
|
105
|
+
const tokens = autocompleteTokens(el);
|
|
106
|
+
if (AUTH_AUTOCOMPLETE_TOKENS.some((t) => tokens.includes(t))) return true;
|
|
107
|
+
|
|
108
|
+
const type = normalizeWs(el.getAttribute && el.getAttribute('type')).toLowerCase();
|
|
109
|
+
if (tag !== 'input' || type !== 'password') return false;
|
|
110
|
+
|
|
111
|
+
const declaresOtherPurpose = tokens.some(
|
|
112
|
+
(t) => t !== 'on' && t !== 'off' && !AUTH_AUTOCOMPLETE_TOKENS.includes(t)
|
|
113
|
+
);
|
|
114
|
+
return !declaresOtherPurpose;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A field that takes no input at all cannot be pasted into either, so a
|
|
118
|
+
// paste handler on it blocks nothing.
|
|
119
|
+
function acceptsInput(el) {
|
|
120
|
+
if (el.hasAttribute && (el.hasAttribute('disabled') || el.hasAttribute('readonly'))) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Classifies an inline handler as 'cancelOnly', 'opaque' or 'none'.
|
|
127
|
+
//
|
|
128
|
+
// Cancelling is not the same as blocking. Replacing the default paste is
|
|
129
|
+
// how a split one-time-code field distributes the digits across its boxes,
|
|
130
|
+
// and how a password field strips stray whitespace from what was pasted --
|
|
131
|
+
// both call preventDefault and then insert the text themselves, which
|
|
132
|
+
// helps the user rather than stopping them. So a handler counts as
|
|
133
|
+
// blocking only when cancelling is the whole of what it does. Anything
|
|
134
|
+
// further -- a call, an assignment, a condition -- may well be putting the
|
|
135
|
+
// text back, and the markup does not say, so it reports cantTell.
|
|
136
|
+
function classifyHandler(source) {
|
|
137
|
+
const src = String(source || '').trim();
|
|
138
|
+
if (!src) return 'none';
|
|
139
|
+
|
|
140
|
+
const CANCEL = [
|
|
141
|
+
/\breturn\s+false\b/gi,
|
|
142
|
+
/\breturn\s*!1\b/gi,
|
|
143
|
+
/\bpreventDefault\s*\(\s*\)/gi,
|
|
144
|
+
/\breturnValue\s*=\s*(?:false|!1)\b/gi
|
|
145
|
+
];
|
|
146
|
+
// Says "yes, paste" outright, so it blocks nothing.
|
|
147
|
+
const ALLOW = [/\breturn\s+true\b/gi, /\breturn\s*!0\b/gi];
|
|
148
|
+
// Neither cancels nor re-inserts.
|
|
149
|
+
const NEUTRAL = [/\bstop(?:Immediate)?Propagation\s*\(\s*\)/gi, /\breturn\b/gi];
|
|
150
|
+
// What is left of an object reference once its method call is removed.
|
|
151
|
+
const REFS = /\b(?:window|document|this|event|evt|ev|e|arguments\[0\])\b/gi;
|
|
152
|
+
const ONLY_PUNCTUATION = /^[\s;.,()[\]{}]*$/;
|
|
153
|
+
|
|
154
|
+
let rest = src;
|
|
155
|
+
let sawCancel = false;
|
|
156
|
+
for (const re of CANCEL) {
|
|
157
|
+
const next = rest.replace(re, ' ');
|
|
158
|
+
if (next !== rest) sawCancel = true;
|
|
159
|
+
rest = next;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!sawCancel) {
|
|
163
|
+
for (const re of ALLOW) rest = rest.replace(re, ' ');
|
|
164
|
+
for (const re of NEUTRAL) rest = rest.replace(re, ' ');
|
|
165
|
+
rest = rest.replace(REFS, ' ');
|
|
166
|
+
return ONLY_PUNCTUATION.test(rest) ? 'none' : 'opaque';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const re of NEUTRAL) rest = rest.replace(re, ' ');
|
|
170
|
+
rest = rest.replace(REFS, ' ');
|
|
171
|
+
// Anything left beyond punctuation is the handler doing more than cancel.
|
|
172
|
+
return ONLY_PUNCTUATION.test(rest) ? 'cancelOnly' : 'opaque';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const nodes = helpers.queryAllSmart
|
|
176
|
+
? helpers.queryAllSmart('input, textarea')
|
|
177
|
+
: helpers.queryAll('input, textarea');
|
|
178
|
+
|
|
179
|
+
const cancelling = [];
|
|
180
|
+
const undetermined = [];
|
|
181
|
+
|
|
182
|
+
for (const el of nodes) {
|
|
183
|
+
if (!el || !el.getAttribute) continue;
|
|
184
|
+
if (!isAuthField(el)) continue;
|
|
185
|
+
|
|
186
|
+
if (!acceptsInput(el)) continue;
|
|
187
|
+
|
|
188
|
+
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
189
|
+
const eligible =
|
|
190
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
191
|
+
if (!eligible) continue;
|
|
192
|
+
|
|
193
|
+
const handler = el.getAttribute('onpaste');
|
|
194
|
+
if (handler === null) continue;
|
|
195
|
+
|
|
196
|
+
const verdict = classifyHandler(handler);
|
|
197
|
+
if (verdict === 'none') continue;
|
|
198
|
+
|
|
199
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
200
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
201
|
+
: { targetSet: 'acc', accEligible: null, reasons: [] };
|
|
202
|
+
|
|
203
|
+
if (verdict === 'cancelOnly') {
|
|
204
|
+
cancelling.push(
|
|
205
|
+
helpers.reportOccurrence(el, {
|
|
206
|
+
summary:
|
|
207
|
+
'This authentication field has a paste handler whose only effect is to cancel the paste.',
|
|
208
|
+
hint: 'Confirm by hand whether pasting still works. If it is blocked, remove the handler so a password manager, or the clipboard for a one-time code, can fill the field.',
|
|
209
|
+
i18n: {
|
|
210
|
+
summaryKey: 'passwordPasteEnabled_summary_fail',
|
|
211
|
+
hintKey: 'passwordPasteEnabled_hint_fail',
|
|
212
|
+
params: {}
|
|
213
|
+
},
|
|
214
|
+
data: {
|
|
215
|
+
visibilityFilter: eligInfo,
|
|
216
|
+
details: { reasonCode: 'PASTE_CANCELLED', handler: normalizeWs(handler) }
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// An inline handler that delegates: whether it cancels lives in code the
|
|
224
|
+
// markup does not carry.
|
|
225
|
+
undetermined.push(
|
|
226
|
+
helpers.reportOccurrence(el, {
|
|
227
|
+
summary:
|
|
228
|
+
'This authentication field has a paste handler, and whether it cancels pasting could not be determined.',
|
|
229
|
+
hint: 'Check by hand that pasting into the field still works, so a password manager or the clipboard can fill it.',
|
|
230
|
+
i18n: {
|
|
231
|
+
summaryKey: 'passwordPasteEnabled_summary_cantTell',
|
|
232
|
+
hintKey: 'passwordPasteEnabled_hint_cantTell',
|
|
233
|
+
params: {}
|
|
234
|
+
},
|
|
235
|
+
data: {
|
|
236
|
+
visibilityFilter: eligInfo,
|
|
237
|
+
details: { reasonCode: 'PASTE_HANDLER_OPAQUE', handler: normalizeWs(handler) }
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const occurrences = cancelling.concat(undetermined);
|
|
244
|
+
if (!occurrences.length) {
|
|
245
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
ruleId: rule.ruleId,
|
|
249
|
+
outcome: 'cantTell',
|
|
250
|
+
severity: rule.defaultSeverity || 'serious',
|
|
251
|
+
occurrences
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -6,28 +6,29 @@
|
|
|
6
6
|
* @check presentation-role-conflict
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary role="presentation"/"none" must not be combined with a global ARIA naming attribute or focusability
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies to elements with an explicit role="presentation" or
|
|
12
|
-
* role="none", OR an <img alt="">
|
|
13
|
-
* presentation role per HTML-AAM,
|
|
14
|
-
* at all
|
|
12
|
+
* role="none", OR an <img alt=""> carrying no explicit role of its own
|
|
13
|
+
* (empty alt gives an <img> an implicit presentation role per HTML-AAM,
|
|
14
|
+
* even with no explicit role attribute at all: `img[alt=''],
|
|
15
|
+
* [role="none"], [role="presentation"]`).
|
|
15
16
|
* @expectation
|
|
16
17
|
* The element does not also carry a WAI-ARIA *global* state/property
|
|
17
18
|
* (aria-label, aria-hidden, aria-describedby, aria-live, aria-current,
|
|
18
|
-
*
|
|
19
|
+
* ...; the full global-attribute set, not just the naming ones), AND
|
|
19
20
|
* is not focusable. Per the WAI-ARIA spec's Presentational Roles
|
|
20
21
|
* Conflict Resolution section, a presentational role is "restored" to
|
|
21
|
-
* the element's implicit semantic role when either condition holds
|
|
22
|
+
* the element's implicit semantic role when either condition holds:
|
|
22
23
|
* the presentation/none role silently stops working, contradicting the
|
|
23
24
|
* author's evident intent to hide the element from the accessibility
|
|
24
25
|
* tree.
|
|
25
26
|
* @implementation-notes
|
|
26
|
-
* - Not WCAG-normative
|
|
27
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
27
28
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
28
29
|
* header comment for the shared rationale/precedent.
|
|
29
30
|
* - `aria-hidden="true"` (the exact valid truthy value) on the
|
|
30
|
-
* presentational element itself is
|
|
31
|
+
* presentational element itself is EXCLUDED as a trigger on purpose,
|
|
31
32
|
* even though it's a global ARIA attribute: it removes the element from
|
|
32
33
|
* the accessibility tree unconditionally, so the "role restoration"
|
|
33
34
|
* this rule warns about never actually reaches assistive tech, making a
|
|
@@ -38,18 +39,23 @@
|
|
|
38
39
|
* exemption (see the code comment at the check site).
|
|
39
40
|
* - The conflicting-attribute set is the full list of ARIA attributes
|
|
40
41
|
* marked `global: true`, not a narrower naming-only list.
|
|
41
|
-
* -
|
|
42
|
+
* - Not applying an implicit-role applicability gate on purpose, one that
|
|
42
43
|
* would make the check inapplicable to role="presentation" on elements
|
|
43
44
|
* with no native implicit role to suppress (e.g. `<div
|
|
44
|
-
* role="presentation" aria-hidden="true"
|
|
45
|
+
* role="presentation" aria-hidden="true">`: a <div> has no native role,
|
|
45
46
|
* so there's nothing for the presentational role to "conflict" with).
|
|
46
47
|
* surea11y stays broader/more cautious here rather than narrower, which
|
|
47
|
-
* is the safer direction to diverge in.
|
|
48
|
-
*
|
|
49
|
-
* in ROADMAP.md §7 item 9.
|
|
48
|
+
* is the safer direction to diverge in. Adding that gate later would
|
|
49
|
+
* need a native-implicit-role table, which doesn't exist yet.
|
|
50
50
|
* - Focusability is computed via helpers.getFocusableInfo (native +
|
|
51
|
-
* tabindex), same helper aria-hidden-focus already relies on
|
|
51
|
+
* tabindex), same helper aria-hidden-focus already relies on. A
|
|
52
52
|
* `:disabled` or otherwise non-focusable element is not flagged.
|
|
53
|
+
* - An `<img alt="">` carrying an explicit role of its own (e.g. `<img
|
|
54
|
+
* alt="" role="img" aria-label="Logo">`) is out of scope: the explicit
|
|
55
|
+
* role wins over the presentation role empty alt would otherwise confer,
|
|
56
|
+
* so nothing presentational is left to conflict with. An explicit role
|
|
57
|
+
* whose tokens are all unknown confers nothing either, and the empty alt
|
|
58
|
+
* still applies.
|
|
53
59
|
*/
|
|
54
60
|
|
|
55
61
|
const id = 'presentation-role-conflict';
|
|
@@ -77,7 +83,7 @@ function runInPage(ctx) {
|
|
|
77
83
|
const { helpers, rule } = ctx;
|
|
78
84
|
|
|
79
85
|
// The full set of ARIA attributes marked `global: true` per the WAI-ARIA
|
|
80
|
-
// spec
|
|
86
|
+
// spec. Any of these present on a presentational element restores its
|
|
81
87
|
// implicit role, not just the naming ones.
|
|
82
88
|
const CONFLICTING_ATTRS = [
|
|
83
89
|
'aria-atomic',
|
|
@@ -109,6 +115,26 @@ function runInPage(ctx) {
|
|
|
109
115
|
const getFocusableInfo =
|
|
110
116
|
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
111
117
|
|
|
118
|
+
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
119
|
+
|
|
120
|
+
// The role attribute holds a fallback list; the first token naming a real
|
|
121
|
+
// role wins, and unknown tokens are skipped over. Returns '' when the
|
|
122
|
+
// element has no role attribute or none of its tokens name a role: the
|
|
123
|
+
// cases where an <img alt=""> keeps the presentation role empty alt gives
|
|
124
|
+
// it.
|
|
125
|
+
function getEffectiveRoleToken(el) {
|
|
126
|
+
const raw = el.getAttribute ? el.getAttribute('role') : null;
|
|
127
|
+
if (!raw) return '';
|
|
128
|
+
const tokens = String(raw).trim().toLowerCase().split(/\s+/);
|
|
129
|
+
for (const token of tokens) {
|
|
130
|
+
if (!token) continue;
|
|
131
|
+
if (token === 'presentation' || token === 'none') return token;
|
|
132
|
+
const known = ariaHelpers ? ariaHelpers.isValidConcreteRole(token) : true;
|
|
133
|
+
if (known) return token;
|
|
134
|
+
}
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
|
|
112
138
|
const nodes = helpers.queryAllSmart
|
|
113
139
|
? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]')
|
|
114
140
|
: helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]');
|
|
@@ -119,30 +145,37 @@ function runInPage(ctx) {
|
|
|
119
145
|
for (const el of nodes) {
|
|
120
146
|
if (!el || !el.getAttribute) continue;
|
|
121
147
|
|
|
148
|
+
// Only reachable via the img[alt=""] branch of the selector: an explicit
|
|
149
|
+
// role other than presentation/none overrides the presentation role that
|
|
150
|
+
// empty alt would confer, leaving no presentational intent to conflict
|
|
151
|
+
// with.
|
|
152
|
+
const roleToken = getEffectiveRoleToken(el);
|
|
153
|
+
if (roleToken && roleToken !== 'presentation' && roleToken !== 'none') continue;
|
|
154
|
+
|
|
122
155
|
applicableCount += 1;
|
|
123
156
|
|
|
124
157
|
// Presence, not value truthiness: the WAI-ARIA role-conflict-resolution
|
|
125
158
|
// rule triggers on a global ARIA attribute being SPECIFIED at all, even
|
|
126
|
-
// with an empty value
|
|
159
|
+
// with an empty value, e.g. <img alt="" aria-hidden="">, where
|
|
127
160
|
// aria-hidden="" (empty string) is still a specified attribute. A
|
|
128
161
|
// truthy-value check would miss this.
|
|
129
162
|
let present = CONFLICTING_ATTRS.filter((attr) =>
|
|
130
163
|
el.hasAttribute ? el.hasAttribute(attr) : el.getAttribute(attr) != null
|
|
131
164
|
);
|
|
132
165
|
|
|
133
|
-
// aria-hidden="true" (the exact, valid truthy value
|
|
166
|
+
// aria-hidden="true" (the exact, valid truthy value, not the
|
|
134
167
|
// empty-string case above, which never actually hides anything) is a
|
|
135
168
|
// special case: it removes the element and its subtree from the
|
|
136
169
|
// accessibility tree unconditionally, independent of role. That makes
|
|
137
170
|
// the "role restoration" this rule warns about ("...which restores its
|
|
138
171
|
// implicit role and cancels the presentational intent") factually
|
|
139
|
-
// inert
|
|
172
|
+
// inert. No AT will ever expose the restored role OR any of the other
|
|
140
173
|
// conflicting attributes (aria-label, aria-describedby, ...) present
|
|
141
174
|
// alongside it, since the whole element stays out of the tree
|
|
142
175
|
// regardless. This pattern is extremely common (e.g. <svg
|
|
143
|
-
// role="presentation" aria-hidden="true"> decorative icons
|
|
176
|
+
// role="presentation" aria-hidden="true"> decorative icons, a
|
|
144
177
|
// defensive belt-and-suspenders double-hide, not an authoring mistake).
|
|
145
|
-
// Focusability is NOT covered by this exemption
|
|
178
|
+
// Focusability is NOT covered by this exemption: a keyboard user can
|
|
146
179
|
// still tab onto an aria-hidden="true" focusable element (the
|
|
147
180
|
// aria-hidden-focus anti-pattern), a real, independent hazard
|
|
148
181
|
// aria-hidden does nothing to prevent.
|
|
@@ -165,12 +198,9 @@ function runInPage(ctx) {
|
|
|
165
198
|
const parts = present.slice();
|
|
166
199
|
if (isFocusable) parts.push('focusable');
|
|
167
200
|
|
|
168
|
-
// No
|
|
201
|
+
// No role token means this matched via the img[alt=""]
|
|
169
202
|
// implicit-presentation case.
|
|
170
|
-
const role =
|
|
171
|
-
String(el.getAttribute('role') || '')
|
|
172
|
-
.trim()
|
|
173
|
-
.toLowerCase() || 'presentation';
|
|
203
|
+
const role = roleToken || 'presentation';
|
|
174
204
|
|
|
175
205
|
occurrences.push(
|
|
176
206
|
helpers.reportOccurrence(el, {
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* @check region
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Page content should be contained within a landmark region
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies to any element under <body> that directly carries visible text
|
|
12
|
-
* (or other own content
|
|
12
|
+
* (or other own content, see @implementation-notes) and is not itself a
|
|
13
13
|
* landmark, live region, dialog, button, <svg>, <iframe>/<frame>, or a
|
|
14
14
|
* resolvable skip-link.
|
|
15
15
|
* @expectation
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
* search), so assistive technology users navigating by landmark do not
|
|
19
19
|
* miss content that was never placed inside one.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* - Not WCAG-normative
|
|
21
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
22
22
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
23
23
|
* header comment for the shared rationale/precedent and the landmark-
|
|
24
24
|
* detection model.
|
|
25
25
|
* - Recursive tree walk, not a direct-<body>-children-only scan: a
|
|
26
26
|
* direct-children-only scope is nearly inert on the single most common
|
|
27
27
|
* real-world page shape, a modern framework's single root mount div
|
|
28
|
-
* (`<body><div id="root">...everything...</div></body>`)
|
|
28
|
+
* (`<body><div id="root">...everything...</div></body>`). That shape
|
|
29
29
|
* gives at most one candidate for the entire page and either misses
|
|
30
30
|
* every real gap inside it or collapses the whole page into one
|
|
31
31
|
* undifferentiated report.
|
|
@@ -37,30 +37,30 @@
|
|
|
37
37
|
* live region, dialog, button, <svg>, <iframe>/<frame>, or a
|
|
38
38
|
* resolvable skip-link), mark it and every ancestor up to <body> as
|
|
39
39
|
* "has a stopper" and don't recurse further into it (an <iframe>/
|
|
40
|
-
* <frame> is additionally reported as its own occurrence
|
|
40
|
+
* <frame> is additionally reported as its own occurrence, since its
|
|
41
41
|
* content is opaque to this engine, so from the outer page's
|
|
42
42
|
* perspective it IS unplaced content).
|
|
43
43
|
* 3. Otherwise, if the node has OWN content (a direct child text node,
|
|
44
|
-
* being an inherently visual element, or an aria-label)
|
|
45
|
-
* non-recursively
|
|
46
|
-
* children never short-circuits the walk into its descendants
|
|
44
|
+
* being an inherently visual element, or an aria-label), checked
|
|
45
|
+
* non-recursively so a plain wrapper <div> with only nested
|
|
46
|
+
* children never short-circuits the walk into its descendants,
|
|
47
47
|
* collect it as a candidate and stop recursing into it.
|
|
48
48
|
* 4. Otherwise recurse into its element children.
|
|
49
49
|
* Each collected candidate is then walked back UP through parents while
|
|
50
50
|
* the parent has no "stopper" marker and isn't <body> itself, collapsing
|
|
51
51
|
* contiguous unplaced content into one occurrence per real gap instead
|
|
52
|
-
* of reporting every individual text-bearing leaf
|
|
52
|
+
* of reporting every individual text-bearing leaf. This is what keeps
|
|
53
53
|
* the walk from being noisy on ordinary pages that mix landmarked and
|
|
54
54
|
* stray content.
|
|
55
55
|
* - "Stopper" exemptions (button, dialog, <svg>, resolvable skip-links)
|
|
56
|
-
* are a
|
|
56
|
+
* are a scope choice, not an oversight: these
|
|
57
57
|
* are extremely common real-world patterns (floating action buttons,
|
|
58
58
|
* modal dialogs, decorative/icon SVGs, "skip to content" links) that
|
|
59
59
|
* aren't the kind of "content organization" gap this rule exists to
|
|
60
60
|
* catch, and flagging them would reintroduce the false-positive noise
|
|
61
61
|
* the original narrow scope was trying to avoid.
|
|
62
|
-
* - The "own content" check for aria-label
|
|
63
|
-
* aria-labelledby
|
|
62
|
+
* - The "own content" check for aria-label does NOT resolve
|
|
63
|
+
* aria-labelledby, a known, narrow scope gap (an element named only via aria-labelledby, with no
|
|
64
64
|
* own text/aria-label, and no other content anywhere in its subtree,
|
|
65
65
|
* could be silently skipped) accepted to avoid a full accessible-name
|
|
66
66
|
* computation (recursive itself) inside an already-recursive structural
|
|
@@ -137,11 +137,11 @@ function runInPage(ctx) {
|
|
|
137
137
|
|
|
138
138
|
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
139
139
|
// question "does this element sit inside a sectioning-content/<main>
|
|
140
|
-
// ancestor that suppresses its conditional implicit role"
|
|
140
|
+
// ancestor that suppresses its conditional implicit role": role-aware
|
|
141
141
|
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
142
142
|
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
143
143
|
// rather than a local tag-only copy. See that function's header comment
|
|
144
|
-
// in src/core/aria-helpers.js for the full algorithm
|
|
144
|
+
// in src/core/aria-helpers.js for the full algorithm, e.g. an
|
|
145
145
|
// <aside role="dialog"> containing its own <header>, where the <header>
|
|
146
146
|
// keeps its banner role.
|
|
147
147
|
function hasSectioningAncestor(el, includeMain) {
|
|
@@ -189,7 +189,7 @@ function runInPage(ctx) {
|
|
|
189
189
|
const SKIP_TAGS = new Set(['script', 'style', 'template', 'noscript', 'link', 'meta', 'title']);
|
|
190
190
|
|
|
191
191
|
// Roles/attributes that make an element its own self-contained
|
|
192
|
-
// announced area
|
|
192
|
+
// announced area: not literally a WAI-ARIA landmark, but not "content
|
|
193
193
|
// that needs a landmark" either.
|
|
194
194
|
const LIVE_REGION_ROLES = new Set(['alert', 'status', 'log', 'marquee', 'timer']);
|
|
195
195
|
|
|
@@ -217,8 +217,8 @@ function runInPage(ctx) {
|
|
|
217
217
|
return false;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
// A "skip to content" link is
|
|
221
|
-
//
|
|
220
|
+
// A "skip to content" link is placed outside the main content flow
|
|
221
|
+
// on purpose, at the very top of the page. Exempting it (when its
|
|
222
222
|
// fragment actually resolves to a real target, not a dead "#"
|
|
223
223
|
// placeholder) avoids flagging a helpful, common accessibility pattern
|
|
224
224
|
// as the very thing this rule is meant to catch.
|
|
@@ -249,8 +249,8 @@ function runInPage(ctx) {
|
|
|
249
249
|
|
|
250
250
|
const VISUAL_CONTENT_TAGS = new Set(['img', 'video', 'audio', 'canvas', 'object', 'embed']);
|
|
251
251
|
|
|
252
|
-
// Non-recursive "does THIS element, on its own, carry content" check
|
|
253
|
-
//
|
|
252
|
+
// Non-recursive "does THIS element, on its own, carry content" check.
|
|
253
|
+
// Mirrors only the direct-content half of getContentNameInfo on purpose,
|
|
254
254
|
// not a full name-from-content recursion: the whole point is to keep
|
|
255
255
|
// recursing through plain wrapper elements (a framework's root mount
|
|
256
256
|
// <div> included) until reaching the actual content-bearing node, rather
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check scope-attr-valid
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary The scope attribute must have a valid value
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies to elements with a non-empty scope attribute.
|
|
12
12
|
* @expectation
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* assistive technology, silently losing the row/column header
|
|
16
16
|
* association it was meant to declare.
|
|
17
17
|
* @implementation-notes
|
|
18
|
-
* - Not WCAG-normative
|
|
18
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
19
19
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
20
20
|
* header comment for the shared rationale/precedent.
|
|
21
21
|
*/
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 2.1.1, 2.1.3
|
|
11
11
|
* @applicability
|
|
12
|
-
*
|
|
12
|
+
* Scoped on purpose to a fixed set of likely-to-scroll container tags
|
|
13
13
|
* (div, section, article, aside, main, nav, pre, table, blockquote, ul,
|
|
14
14
|
* ol, textarea) with computed `overflow-x`/`overflow-y` of `auto` or
|
|
15
|
-
* `scroll
|
|
15
|
+
* `scroll`, not every element on the page, to keep this deterministic
|
|
16
16
|
* and performant (same style of scope-down as `region`).
|
|
17
17
|
* @expectation
|
|
18
18
|
* A region whose CSS declares it may scroll (`auto`/`scroll`) should be
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
* scroll from), or the region itself carries a non-negative `tabindex`.
|
|
23
23
|
* @implementation-notes
|
|
24
24
|
* - jsdom does not perform layout, so `scrollHeight`/`clientHeight` are
|
|
25
|
-
* not available to confirm the region's content actually overflows
|
|
25
|
+
* not available to confirm the region's content actually overflows,
|
|
26
26
|
* only that the CSS declares it *may*. Many elements declare
|
|
27
27
|
* `overflow: auto` defensively without their content ever actually
|
|
28
28
|
* overflowing, which would be a false positive if treated as a hard
|
|
29
29
|
* `fail`. For that reason this is authored as `type: 'manual'`
|
|
30
|
-
* (cantTell-capped, never fail) rather than `automatic
|
|
31
|
-
* layout-dependent gap as `iframe-focusable-content`'s
|
|
30
|
+
* (cantTell-capped, never fail) rather than `automatic`, the same class
|
|
31
|
+
* of layout-dependent gap as `iframe-focusable-content`'s
|
|
32
32
|
* `contentDocument` limitation.
|
|
33
33
|
* - "Has a focusable descendant" is a presence check (link/button/form
|
|
34
34
|
* control/`[tabindex]`/`iframe`/`[contenteditable]`), not a full
|
|
35
|
-
* focusability computation (disabled state, visibility, etc.)
|
|
36
|
-
*
|
|
35
|
+
* focusability computation (disabled state, visibility, etc.). Kept
|
|
36
|
+
* simple on purpose, to keep this rule self-contained and fast.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
const id = 'scrollable-region-focusable';
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* @check skip-link
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary A "skip" link must resolve to a real, usable target
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies to <a href="#fragment"> elements whose accessible name
|
|
12
12
|
* matches a common "skip to ..." / "jump to ..." authoring convention
|
|
13
|
-
* (case-insensitive "skip" or "jump to" in the name)
|
|
13
|
+
* (case-insensitive "skip" or "jump to" in the name), the recognizable
|
|
14
14
|
* pattern for a skip-navigation link, not every same-page anchor link
|
|
15
15
|
* on the page. "jump to" is included alongside "skip" since real skip
|
|
16
16
|
* links use both conventions (e.g. a "Jump to section" link, which a
|
|
17
17
|
* purely positional match would catch but a "skip"-only text pattern
|
|
18
|
-
* would miss). Text-pattern matching itself stays
|
|
19
|
-
* implementation-notes)
|
|
18
|
+
* would miss). Text-pattern matching itself stays intentional (see
|
|
19
|
+
* implementation-notes); this only widens the known-convention list.
|
|
20
20
|
* @expectation
|
|
21
21
|
* The link's fragment resolves to a real element in the document
|
|
22
22
|
* (via a matching id, or a legacy <a name="...">), and that target is
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* whose target is missing or effectively unusable does not provide a
|
|
26
26
|
* reliable bypass destination.
|
|
27
27
|
* @implementation-notes
|
|
28
|
-
* - Not WCAG-normative
|
|
28
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
29
29
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
30
30
|
* header comment for the shared rationale/precedent.
|
|
31
31
|
* - Keyed on the "skip" text-pattern convention rather than positional
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to inline <svg> elements that already carry a text alternative:
|
|
14
|
+
* non-empty <title> or <desc> text, a non-empty aria-label, or an
|
|
15
|
+
* aria-labelledby that resolves to non-empty text. <desc> counts here as
|
|
16
|
+
* something to review even though it never contributes to the accessible
|
|
17
|
+
* name: that distinction is svg-text-alternative-present's. The element
|
|
18
|
+
* must be included in the accessibility tree, and
|
|
19
|
+
* role="presentation"/"none" takes it out of scope unless it is focusable,
|
|
20
|
+
* which restores its role.
|
|
12
21
|
* @expectation
|
|
13
22
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
23
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check tabindex
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary tabindex should not be greater than 0
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies to elements with a tabindex attribute whose value parses as
|
|
12
12
|
* a valid integer.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* page changes and usually indicates the natural DOM order should be
|
|
17
17
|
* fixed instead.
|
|
18
18
|
* @implementation-notes
|
|
19
|
-
* - Not WCAG-normative
|
|
19
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
20
20
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
21
21
|
* header comment for the shared rationale/precedent.
|
|
22
22
|
*/
|