feat: add a11y `autocomplete-valid` (#8520)

Part of #820

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/8567/head
Christoph Rüßler 2 years ago committed by GitHub
parent 83679e9700
commit 3f2f1e58df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,11 @@
* Update interpolated style directive properly when using spread ([#8438](https://github.com/sveltejs/svelte/issues/8438)) * Update interpolated style directive properly when using spread ([#8438](https://github.com/sveltejs/svelte/issues/8438))
* Remove style directive property when value is `undefined` ([#8462](https://github.com/sveltejs/svelte/issues/8462)) * Remove style directive property when value is `undefined` ([#8462](https://github.com/sveltejs/svelte/issues/8462))
* Ensure version is typed as `string` instead of the literal `__VERSION__` ([#8498](https://github.com/sveltejs/svelte/issues/8498)) * Ensure version is typed as `string` instead of the literal `__VERSION__` ([#8498](https://github.com/sveltejs/svelte/issues/8498))
* Add `a11y-autocomplete-valid` warning ([#8520](https://github.com/sveltejs/svelte/pull/8520))
* Handle nested array rest destructuring ([#8554](https://github.com/sveltejs/svelte/issues/8554), [#8552](https://github.com/sveltejs/svelte/issues/8552))
* Add `fullscreenElement` and `visibilityState` bindings for `<svelte:document>` ([#8507](https://github.com/sveltejs/svelte/pull/8507))
* Add `devicePixelRatio` binding for `<svelte:window>` ([#8285](https://github.com/sveltejs/svelte/issues/8285))
* Relax `a11y-no-redundant-roles` warning ([#8536](https://github.com/sveltejs/svelte/pull/8536))
## 3.58.0 ## 3.58.0

@ -162,6 +162,10 @@ export default {
code: 'a11y-missing-attribute', code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute` message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
}), }),
a11y_autocomplete_valid: (type: null | true | string, value: null | true | string) => ({
code: 'a11y-autocomplete-valid',
message: `A11y: The value '${value}' is not supported by the attribute 'autocomplete' on element <input type="${type}">`
}),
a11y_img_redundant_alt: { a11y_img_redundant_alt: {
code: 'a11y-img-redundant-alt', code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.' message: 'A11y: Screenreaders already announce <img> elements as an image.'

@ -25,7 +25,7 @@ import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role, is_static_element, has_disabled_attribute } from '../utils/a11y'; import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role, is_static_element, has_disabled_attribute, is_valid_autocomplete } from '../utils/a11y';
const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const aria_attribute_set = new Set(aria_attributes); const aria_attribute_set = new Set(aria_attributes);
@ -849,6 +849,18 @@ export default class Element extends Node {
should_have_attribute(this, required_attributes, 'input type="image"'); should_have_attribute(this, required_attributes, 'input type="image"');
} }
} }
// autocomplete-valid
const autocomplete = attribute_map.get('autocomplete');
if (type && autocomplete) {
const type_value = type.get_static_value();
const autocomplete_value = autocomplete.get_static_value();
if (!is_valid_autocomplete(type_value, autocomplete_value)) {
component.warn(autocomplete, compiler_warnings.a11y_autocomplete_valid(type_value, autocomplete_value));
}
}
} }
if (this.name === 'img') { if (this.name === 'img') {

@ -6,6 +6,7 @@ import {
} from 'aria-query'; } from 'aria-query';
import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query'; import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
import Attribute from '../nodes/Attribute'; import Attribute from '../nodes/Attribute';
import { regex_whitespaces } from '../../utils/patterns';
const aria_roles = roles_map.keys(); const aria_roles = roles_map.keys();
const abstract_roles = new Set(aria_roles.filter(role => roles_map.get(role).abstract)); const abstract_roles = new Set(aria_roles.filter(role => roles_map.get(role).abstract));
@ -223,3 +224,104 @@ export function is_semantic_role_element(role: ARIARoleDefinitionKey, tag_name:
} }
return false; return false;
} }
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
const address_type_tokens = new Set(['shipping', 'billing']);
const autofill_field_name_tokens = new Set([
'',
'on',
'off',
'name',
'honorific-prefix',
'given-name',
'additional-name',
'family-name',
'honorific-suffix',
'nickname',
'username',
'new-password',
'current-password',
'one-time-code',
'organization-title',
'organization',
'street-address',
'address-line1',
'address-line2',
'address-line3',
'address-level4',
'address-level3',
'address-level2',
'address-level1',
'country',
'country-name',
'postal-code',
'cc-name',
'cc-given-name',
'cc-additional-name',
'cc-family-name',
'cc-number',
'cc-exp',
'cc-exp-month',
'cc-exp-year',
'cc-csc',
'cc-type',
'transaction-currency',
'transaction-amount',
'language',
'bday',
'bday-day',
'bday-month',
'bday-year',
'sex',
'url',
'photo'
]);
const contact_type_tokens = new Set(['home', 'work', 'mobile', 'fax', 'pager']);
const autofill_contact_field_name_tokens = new Set([
'tel',
'tel-country-code',
'tel-national',
'tel-area-code',
'tel-local',
'tel-local-prefix',
'tel-local-suffix',
'tel-extension',
'email',
'impp'
]);
export function is_valid_autocomplete(type: null | true | string, autocomplete: null | true | string) {
if (typeof autocomplete !== 'string' || typeof type !== 'string') {
return false;
}
const tokens = autocomplete.trim().toLowerCase().split(regex_whitespaces);
if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) {
tokens.shift();
}
if (address_type_tokens.has(tokens[0])) {
tokens.shift();
}
if (autofill_field_name_tokens.has(tokens[0])) {
tokens.shift();
} else {
if (contact_type_tokens.has(tokens[0])) {
tokens.shift();
}
if (autofill_contact_field_name_tokens.has(tokens[0])) {
tokens.shift();
} else {
return false;
}
}
if (tokens[0] === 'webauthn') {
tokens.shift();
}
return tokens.length === 0;
}

@ -0,0 +1,21 @@
<!-- VALID -->
<input type="text" />
<input type="text" autocomplete="name" />
<input type="text" autocomplete="off" />
<input type="text" autocomplete="on" />
<input type="text" autocomplete="billing family-name" />
<input type="hidden" autocomplete="section-blue shipping street-address" />
<input type="text" autocomplete="section-somewhere shipping work email" />
<input type="text" autocomplete="section-somewhere shipping work email webauthn" />
<input type="text" autocomplete="SECTION-SOMEWHERE SHIPPING WORK EMAIL WEBAUTHN" />
<input type="TEXT" autocomplete="ON" />
<input type="email" autocomplete="url" />
<input type="text" autocomplete="section-blue shipping street-address" />
<input type="hidden" autocomplete="off" />
<input type="hidden" autocomplete="on" />
<input type="text" autocomplete="" />
<!-- INVALID -->
<input type="text" autocomplete />
<input type="text" autocomplete="incorrect" />
<input type="text" autocomplete="webauthn" />

@ -0,0 +1,38 @@
[
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 31,
"line": 19
},
"message": "A11y: The value 'true' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 19
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 43,
"line": 20
},
"message": "A11y: The value 'incorrect' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 20
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 42,
"line": 21
},
"message": "A11y: The value 'webauthn' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 21
}
}
]
Loading…
Cancel
Save