@surea11y/core 1.4.0 → 1.5.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 +88 -7
- package/README.md +19 -3
- package/bin/surea11y-core.js +0 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/ENGINE_OPTIONS.md +14 -10
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +28 -6
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +2 -0
- package/docs/RULE_AUTHORING.md +53 -0
- package/docs/RULE_CATALOG.md +6 -6
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +8 -1
- package/src/checks/automatic/aria-allowed-attr.js +661 -99
- package/src/checks/automatic/aria-allowed-role.js +14 -16
- package/src/checks/automatic/aria-braille-equivalent.js +17 -19
- package/src/checks/automatic/aria-conditional-attr.js +17 -19
- package/src/checks/automatic/aria-deprecated-role.js +122 -41
- package/src/checks/automatic/aria-hidden-body.js +2 -9
- package/src/checks/automatic/aria-hidden-focus.js +99 -18
- package/src/checks/automatic/aria-prohibited-attr.js +54 -55
- package/src/checks/automatic/aria-prohibited-children.js +26 -26
- package/src/checks/automatic/aria-required-attr.js +14 -17
- package/src/checks/automatic/aria-required-children.js +17 -20
- package/src/checks/automatic/aria-required-parent.js +17 -20
- package/src/checks/automatic/aria-roles-valid.js +66 -27
- package/src/checks/automatic/aria-valid-attr-value.js +18 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -17
- package/src/checks/automatic/autocomplete-valid.js +52 -18
- package/src/checks/automatic/avoid-inline-spacing.js +192 -34
- package/src/checks/automatic/binary-control-name-present.js +30 -24
- package/src/checks/automatic/button-name-present.js +73 -45
- package/src/checks/automatic/canvas-text-alternative-present.js +15 -8
- package/src/checks/automatic/combobox-name-present.js +23 -18
- package/src/checks/automatic/css-orientation-lock.js +22 -22
- package/src/checks/automatic/definition-list-children-valid.js +18 -21
- package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
- package/src/checks/automatic/dialog-name-present.js +24 -19
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +45 -37
- package/src/checks/automatic/form-control-programmatic-label-present.js +48 -4
- package/src/checks/automatic/form-control-single-label.js +109 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +29 -31
- package/src/checks/automatic/iframe-name-present.js +15 -17
- package/src/checks/automatic/iframe-title-unique.js +18 -23
- package/src/checks/automatic/img-alt-present.js +16 -9
- package/src/checks/automatic/input-image-alt-present.js +99 -48
- package/src/checks/automatic/label-in-name.js +102 -33
- package/src/checks/automatic/language-page-present.js +5 -1
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +75 -47
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +23 -18
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +24 -19
- package/src/checks/automatic/meta-refresh-no-exceptions.js +44 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +44 -21
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +51 -32
- package/src/checks/automatic/meter-name-present.js +24 -19
- package/src/checks/automatic/nested-interactive-controls-absent.js +179 -51
- package/src/checks/automatic/object-text-alternative-present.js +14 -7
- package/src/checks/automatic/option-name-present.js +24 -19
- package/src/checks/automatic/progressbar-name-present.js +27 -22
- package/src/checks/automatic/searchbox-name-present.js +27 -18
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +26 -19
- package/src/checks/automatic/spinbutton-name-present.js +27 -18
- package/src/checks/automatic/summary-name-present.js +24 -19
- package/src/checks/automatic/tab-name-present.js +24 -19
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +82 -25
- package/src/checks/automatic/target-size-minimum.js +104 -81
- package/src/checks/automatic/td-has-header.js +15 -20
- package/src/checks/automatic/textbox-name-present.js +23 -18
- package/src/checks/automatic/tooltip-name-present.js +24 -19
- package/src/checks/automatic/treeitem-name-present.js +24 -19
- package/src/checks/automatic/valid-lang.js +31 -20
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/{automatic/bypass-blocks-present.js → manual/bypass-blocks-present-manual.js} +98 -44
- package/src/checks/manual/empty-heading-manual.js +15 -17
- package/src/checks/manual/empty-table-header-manual.js +27 -30
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
- package/src/checks/manual/heading-order-manual.js +17 -22
- package/src/checks/manual/image-redundant-alt-manual.js +14 -17
- package/src/checks/manual/input-image-alt-decorative-manual.js +24 -0
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +25 -39
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +21 -27
- package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
- package/src/checks/manual/landmark-one-main-manual.js +2 -9
- package/src/checks/manual/landmark-unique-manual.js +22 -27
- package/src/checks/manual/link-name-quality-manual.js +15 -17
- package/src/checks/manual/meta-viewport-large-manual.js +14 -17
- package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
- package/src/checks/manual/page-has-heading-one-manual.js +2 -9
- package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
- package/src/checks/manual/region-manual.js +13 -6
- package/src/checks/manual/scope-attr-valid-manual.js +14 -17
- package/src/checks/manual/skip-link-manual.js +39 -47
- package/src/checks/manual/tabindex-manual.js +14 -17
- package/src/checks/manual/table-duplicate-name-manual.js +14 -17
- package/src/core.js +8818 -4330
- package/src/report.js +14 -0
- package/src/sarif.js +2 -2
- package/surea11y.browser.js +4023 -3911
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
|
@@ -256,10 +256,6 @@ function runInPage(ctx) {
|
|
|
256
256
|
for (const entry of owned) {
|
|
257
257
|
if (entry.role && requiredSet.has(entry.role)) continue;
|
|
258
258
|
|
|
259
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(entry.el) : 'html';
|
|
260
|
-
const html = helpers.getOuterHtmlSnippet
|
|
261
|
-
? helpers.getOuterHtmlSnippet(entry.el)
|
|
262
|
-
: entry.el.outerHTML || '';
|
|
263
259
|
const containerSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
264
260
|
|
|
265
261
|
const isRoleless = !entry.role;
|
|
@@ -286,29 +282,33 @@ function runInPage(ctx) {
|
|
|
286
282
|
hintKey = 'ariaProhibitedChildren_hint_fail';
|
|
287
283
|
}
|
|
288
284
|
|
|
289
|
-
occurrences.push(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
285
|
+
occurrences.push(
|
|
286
|
+
helpers.reportOccurrence(entry.el, {
|
|
287
|
+
summary,
|
|
288
|
+
hint,
|
|
289
|
+
i18n: {
|
|
290
|
+
summaryKey,
|
|
291
|
+
hintKey,
|
|
292
|
+
params: isRoleless
|
|
293
|
+
? { attr: entry.attr, containerRole: role }
|
|
294
|
+
: {
|
|
295
|
+
childRole: entry.role,
|
|
296
|
+
containerRole: role,
|
|
297
|
+
allowedRoles: requiredOwned.join(', ')
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
data: {
|
|
301
|
+
details: {
|
|
302
|
+
reasonCode: isRoleless ? 'ARIA_PROHIBITED_CHILD_ROLELESS' : 'ARIA_PROHIBITED_CHILD',
|
|
303
|
+
childRole: entry.role,
|
|
304
|
+
attr: entry.attr,
|
|
305
|
+
containerRole: role,
|
|
306
|
+
containerSelector,
|
|
307
|
+
allowedOwnedRoles: requiredOwned
|
|
308
|
+
}
|
|
309
309
|
}
|
|
310
|
-
}
|
|
311
|
-
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
@@ -129,24 +129,21 @@ function runInPage(ctx) {
|
|
|
129
129
|
|
|
130
130
|
if (!missing.length) continue;
|
|
131
131
|
|
|
132
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
133
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
134
|
-
|
|
135
132
|
for (const attr of missing) {
|
|
136
|
-
occurrences.push(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
133
|
+
occurrences.push(
|
|
134
|
+
helpers.reportOccurrence(el, {
|
|
135
|
+
summary: 'This attribute is required for this element’s role, but is missing.',
|
|
136
|
+
hint: 'Add this attribute with a valid value for this role.',
|
|
137
|
+
i18n: {
|
|
138
|
+
summaryKey: 'ariaRequiredAttr_summary_fail',
|
|
139
|
+
hintKey: 'ariaRequiredAttr_hint_fail',
|
|
140
|
+
params: { attr, role }
|
|
141
|
+
},
|
|
142
|
+
data: {
|
|
143
|
+
details: { reasonCode: 'ARIA_ATTR_REQUIRED_MISSING', attr, role }
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
);
|
|
150
147
|
}
|
|
151
148
|
}
|
|
152
149
|
|
|
@@ -253,27 +253,24 @@ function runInPage(ctx) {
|
|
|
253
253
|
|
|
254
254
|
if (found) continue;
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
|
|
272
|
-
role,
|
|
273
|
-
requiredOwnedRoles: requiredOwned
|
|
256
|
+
occurrences.push(
|
|
257
|
+
helpers.reportOccurrence(el, {
|
|
258
|
+
summary: 'This container role has no owned child with a required role.',
|
|
259
|
+
hint: 'Add a descendant (or aria-owns-referenced element) with one of the required owned roles.',
|
|
260
|
+
i18n: {
|
|
261
|
+
summaryKey: 'ariaRequiredChildren_summary_fail',
|
|
262
|
+
hintKey: 'ariaRequiredChildren_hint_fail',
|
|
263
|
+
params: { role, requiredRoles: requiredOwned.join(', ') }
|
|
264
|
+
},
|
|
265
|
+
data: {
|
|
266
|
+
details: {
|
|
267
|
+
reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
|
|
268
|
+
role,
|
|
269
|
+
requiredOwnedRoles: requiredOwned
|
|
270
|
+
}
|
|
274
271
|
}
|
|
275
|
-
}
|
|
276
|
-
|
|
272
|
+
})
|
|
273
|
+
);
|
|
277
274
|
}
|
|
278
275
|
|
|
279
276
|
if (applicableCount === 0) {
|
|
@@ -210,27 +210,24 @@ function runInPage(ctx) {
|
|
|
210
210
|
|
|
211
211
|
if (hasContext) continue;
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
229
|
-
role,
|
|
230
|
-
requiredContextRoles: requiredContext
|
|
213
|
+
occurrences.push(
|
|
214
|
+
helpers.reportOccurrence(el, {
|
|
215
|
+
summary: 'This role requires a specific ancestor/owner context role, which was not found.',
|
|
216
|
+
hint: 'Place this element inside (or aria-owns-reference it from) an element with an acceptable context role.',
|
|
217
|
+
i18n: {
|
|
218
|
+
summaryKey: 'ariaRequiredParent_summary_fail',
|
|
219
|
+
hintKey: 'ariaRequiredParent_hint_fail',
|
|
220
|
+
params: { role, requiredRoles: requiredContext.join(', ') }
|
|
221
|
+
},
|
|
222
|
+
data: {
|
|
223
|
+
details: {
|
|
224
|
+
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
225
|
+
role,
|
|
226
|
+
requiredContextRoles: requiredContext
|
|
227
|
+
}
|
|
231
228
|
}
|
|
232
|
-
}
|
|
233
|
-
|
|
229
|
+
})
|
|
230
|
+
);
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
if (applicableCount === 0) {
|
|
@@ -67,41 +67,80 @@ function runInPage(ctx) {
|
|
|
67
67
|
const occurrences = [];
|
|
68
68
|
let applicableCount = 0;
|
|
69
69
|
|
|
70
|
+
// Programmatically hidden per the ACT glossary: display:none, visibility not
|
|
71
|
+
// visible, or aria-hidden on the element or an ancestor. inert is treated
|
|
72
|
+
// the same although the glossary predates it -- an inert subtree is out of
|
|
73
|
+
// the accessibility tree entirely, so a role on it reaches no one.
|
|
74
|
+
function isHidden(el) {
|
|
75
|
+
try {
|
|
76
|
+
if (typeof helpers.isDomVisibleEligible === 'function') {
|
|
77
|
+
if (!helpers.isDomVisibleEligible(el, ctx)) return true;
|
|
78
|
+
}
|
|
79
|
+
// Walk the composed tree, not parentElement: that stops at a shadow
|
|
80
|
+
// root, so a host carrying aria-hidden or inert would never be seen
|
|
81
|
+
// from inside its own shadow content.
|
|
82
|
+
const up =
|
|
83
|
+
typeof helpers.composedParent === 'function'
|
|
84
|
+
? helpers.composedParent
|
|
85
|
+
: (n) => n.parentElement;
|
|
86
|
+
|
|
87
|
+
// A shadow root has no getAttribute, so skip past it rather than
|
|
88
|
+
// stopping: the host one step further up is the node that matters.
|
|
89
|
+
for (let n = el; n; n = up(n)) {
|
|
90
|
+
if (!n.getAttribute) continue;
|
|
91
|
+
if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
|
|
92
|
+
if (n.hasAttribute && n.hasAttribute('inert')) return true;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
for (const el of nodes) {
|
|
71
101
|
if (!el || !el.getAttribute) continue;
|
|
72
102
|
|
|
73
|
-
|
|
74
|
-
if (
|
|
103
|
+
// ACT 674b10 is not applicable to a programmatically hidden element.
|
|
104
|
+
if (isHidden(el)) continue;
|
|
75
105
|
|
|
76
|
-
|
|
106
|
+
// role takes a fallback list and the first token the browser recognises
|
|
107
|
+
// wins, so role="searchfield searchbox" resolves to searchbox. The rule
|
|
108
|
+
// fails only when no token names a concrete role.
|
|
109
|
+
const tokens =
|
|
110
|
+
typeof ariaHelpers.getAllRoleTokens === 'function'
|
|
111
|
+
? ariaHelpers.getAllRoleTokens(el)
|
|
112
|
+
: [ariaHelpers.getExplicitRole(el)].filter(Boolean);
|
|
113
|
+
if (!tokens.length) continue; // role="" or whitespace-only: not this rule's concern
|
|
77
114
|
|
|
78
|
-
|
|
79
|
-
const isKnown = ariaHelpers.isKnownRole(role);
|
|
115
|
+
applicableCount += 1;
|
|
80
116
|
|
|
81
|
-
|
|
117
|
+
const usable = tokens.find((t) => ariaHelpers.isKnownRole(t) && !ariaHelpers.isAbstractRole(t));
|
|
118
|
+
if (usable) continue;
|
|
82
119
|
|
|
120
|
+
const role = tokens[0];
|
|
121
|
+
const isKnown = tokens.some((t) => ariaHelpers.isKnownRole(t));
|
|
83
122
|
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
123
|
+
|
|
124
|
+
occurrences.push(
|
|
125
|
+
helpers.reportOccurrence(el, {
|
|
126
|
+
summary: !isKnown
|
|
127
|
+
? 'The role attribute value is not a recognized ARIA role.'
|
|
128
|
+
: 'The role attribute value is an abstract ARIA role, which must not be used directly.',
|
|
129
|
+
hint: !isKnown
|
|
130
|
+
? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
|
|
131
|
+
: 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
|
|
132
|
+
i18n: {
|
|
133
|
+
summaryKey: !isKnown
|
|
134
|
+
? 'ariaRolesValid_summary_invalid'
|
|
135
|
+
: 'ariaRolesValid_summary_abstract',
|
|
136
|
+
hintKey: !isKnown ? 'ariaRolesValid_hint_invalid' : 'ariaRolesValid_hint_abstract',
|
|
137
|
+
params: { role }
|
|
138
|
+
},
|
|
139
|
+
data: {
|
|
140
|
+
details: { reasonCode, role }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
);
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
if (applicableCount === 0) {
|
|
@@ -105,29 +105,26 @@ function runInPage(ctx) {
|
|
|
105
105
|
|
|
106
106
|
if (!invalid || !invalid.length) continue;
|
|
107
107
|
|
|
108
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
109
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
110
|
-
|
|
111
108
|
for (const item of invalid) {
|
|
112
|
-
occurrences.push(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
occurrences.push(
|
|
110
|
+
helpers.reportOccurrence(el, {
|
|
111
|
+
summary: 'This element has an ARIA attribute with an invalid value.',
|
|
112
|
+
hint: 'Use a value that matches the attribute’s expected type (see the WAI-ARIA specification for this attribute).',
|
|
113
|
+
i18n: {
|
|
114
|
+
summaryKey: 'ariaValidAttrValue_summary_fail',
|
|
115
|
+
hintKey: 'ariaValidAttrValue_hint_fail',
|
|
116
|
+
params: { attr: item.name, value: item.value }
|
|
117
|
+
},
|
|
118
|
+
data: {
|
|
119
|
+
details: {
|
|
120
|
+
reasonCode: 'ARIA_ATTR_VALUE_INVALID',
|
|
121
|
+
attr: item.name,
|
|
122
|
+
value: item.value,
|
|
123
|
+
valueReason: item.reason
|
|
124
|
+
}
|
|
128
125
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
126
|
+
})
|
|
127
|
+
);
|
|
131
128
|
}
|
|
132
129
|
}
|
|
133
130
|
|
|
@@ -86,24 +86,21 @@ function runInPage(ctx) {
|
|
|
86
86
|
|
|
87
87
|
if (!invalidNames || !invalidNames.length) continue;
|
|
88
88
|
|
|
89
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
91
|
-
|
|
92
89
|
for (const name of invalidNames) {
|
|
93
|
-
occurrences.push(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
90
|
+
occurrences.push(
|
|
91
|
+
helpers.reportOccurrence(el, {
|
|
92
|
+
summary: 'This element has an attribute that is not a recognized ARIA attribute.',
|
|
93
|
+
hint: 'Correct the attribute name (check for typos), or remove it if not needed.',
|
|
94
|
+
i18n: {
|
|
95
|
+
summaryKey: 'ariaValidAttr_summary_fail',
|
|
96
|
+
hintKey: 'ariaValidAttr_hint_fail',
|
|
97
|
+
params: { attr: name }
|
|
98
|
+
},
|
|
99
|
+
data: {
|
|
100
|
+
details: { reasonCode: 'ARIA_ATTR_INVALID', attr: name }
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
);
|
|
107
104
|
}
|
|
108
105
|
}
|
|
109
106
|
|
|
@@ -130,7 +130,15 @@ function runInPage(ctx) {
|
|
|
130
130
|
if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
|
|
131
131
|
i += 1;
|
|
132
132
|
if (tokens[i] === 'shipping' || tokens[i] === 'billing') i += 1;
|
|
133
|
-
|
|
133
|
+
// A contact modality token is only allowed when the field that follows is
|
|
134
|
+
// a contact field, so "work photo" is invalid while "work email" is not.
|
|
135
|
+
if (CONTACT_MODALITY.has(tokens[i])) {
|
|
136
|
+
const next = tokens[i + 1];
|
|
137
|
+
const isContactField =
|
|
138
|
+
next === 'email' || next === 'impp' || next === 'tel' || (next || '').startsWith('tel-');
|
|
139
|
+
if (!isContactField) return false;
|
|
140
|
+
i += 1;
|
|
141
|
+
}
|
|
134
142
|
|
|
135
143
|
let end = tokens.length;
|
|
136
144
|
if (tokens[end - 1] === 'webauthn') end -= 1;
|
|
@@ -147,33 +155,59 @@ function runInPage(ctx) {
|
|
|
147
155
|
const occurrences = [];
|
|
148
156
|
let applicableCount = 0;
|
|
149
157
|
|
|
158
|
+
// ACT 73f2c2 exempts controls where the attribute cannot describe an input
|
|
159
|
+
// purpose: the on/off toggle, disabled controls, input types with a fixed
|
|
160
|
+
// value, and controls that take no input.
|
|
161
|
+
const FIXED_VALUE_TYPES = new Set([
|
|
162
|
+
'button',
|
|
163
|
+
'checkbox',
|
|
164
|
+
'file',
|
|
165
|
+
'image',
|
|
166
|
+
'radio',
|
|
167
|
+
'reset',
|
|
168
|
+
'submit'
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
function isExempt(el) {
|
|
172
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
173
|
+
if (tag === 'input') {
|
|
174
|
+
const type = String(el.getAttribute('type') || 'text').toLowerCase();
|
|
175
|
+
if (FIXED_VALUE_TYPES.has(type)) return true;
|
|
176
|
+
}
|
|
177
|
+
if (el.hasAttribute && el.hasAttribute('disabled')) return true;
|
|
178
|
+
if (String(el.getAttribute('aria-disabled') || '').toLowerCase() === 'true') return true;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
150
182
|
for (const el of nodes) {
|
|
151
183
|
if (!el || !el.getAttribute) continue;
|
|
152
184
|
const raw = String(el.getAttribute('autocomplete') || '').trim();
|
|
153
185
|
if (!raw) continue;
|
|
154
186
|
|
|
187
|
+
const tokens = raw.toLowerCase().split(/\s+/).filter(Boolean);
|
|
188
|
+
if (tokens.length === 1 && (tokens[0] === 'on' || tokens[0] === 'off')) continue;
|
|
189
|
+
if (isExempt(el)) continue;
|
|
190
|
+
|
|
155
191
|
applicableCount += 1;
|
|
156
192
|
|
|
157
193
|
if (isValidAutocomplete(raw)) continue;
|
|
158
194
|
|
|
159
195
|
const tag = el.tagName.toLowerCase();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
});
|
|
196
|
+
|
|
197
|
+
occurrences.push(
|
|
198
|
+
helpers.reportOccurrence(el, {
|
|
199
|
+
summary: 'This autocomplete attribute value is not a valid autofill value.',
|
|
200
|
+
hint: 'Use "on"/"off", or a valid autofill token list (e.g. "shipping street-address", "cc-number").',
|
|
201
|
+
i18n: {
|
|
202
|
+
summaryKey: 'autocompleteValid_summary_fail',
|
|
203
|
+
hintKey: 'autocompleteValid_hint_fail',
|
|
204
|
+
params: { element: tag, value: raw }
|
|
205
|
+
},
|
|
206
|
+
data: {
|
|
207
|
+
details: { reasonCode: 'AUTOCOMPLETE_VALUE_INVALID', element: tag, value: raw }
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
);
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
if (applicableCount === 0) {
|