From 3f2f1e58dfde1aa854eeff13b989aed6f64d50f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 5 May 2023 13:39:34 +0200 Subject: [PATCH] feat: add a11y `autocomplete-valid` (#8520) Part of #820 --------- Co-authored-by: Simon Holthausen --- CHANGELOG.md | 5 + src/compiler/compile/compiler_warnings.ts | 4 + src/compiler/compile/nodes/Element.ts | 14 ++- src/compiler/compile/utils/a11y.ts | 102 ++++++++++++++++++ .../a11y-autocomplete-valid/input.svelte | 21 ++++ .../a11y-autocomplete-valid/warnings.json | 38 +++++++ 6 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 test/validator/samples/a11y-autocomplete-valid/input.svelte create mode 100644 test/validator/samples/a11y-autocomplete-valid/warnings.json diff --git a/CHANGELOG.md b/CHANGELOG.md index bc75b301a7..730a5b8cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ * 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)) * 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 `` ([#8507](https://github.com/sveltejs/svelte/pull/8507)) +* Add `devicePixelRatio` binding for `` ([#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 diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index d778ca1dbc..c0d595f20d 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -162,6 +162,10 @@ export default { code: 'a11y-missing-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 ` + }), a11y_img_redundant_alt: { code: 'a11y-img-redundant-alt', message: 'A11y: Screenreaders already announce elements as an image.' diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 44d84f7566..9d45c84fae 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -25,7 +25,7 @@ import { Literal } from 'estree'; import compiler_warnings from '../compiler_warnings'; import compiler_errors from '../compiler_errors'; 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_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"'); } } + + // 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') { diff --git a/src/compiler/compile/utils/a11y.ts b/src/compiler/compile/utils/a11y.ts index 4409f80262..0d24f5dbda 100644 --- a/src/compiler/compile/utils/a11y.ts +++ b/src/compiler/compile/utils/a11y.ts @@ -6,6 +6,7 @@ import { } from 'aria-query'; import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query'; import Attribute from '../nodes/Attribute'; +import { regex_whitespaces } from '../../utils/patterns'; const aria_roles = roles_map.keys(); 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; } + +// 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; +} diff --git a/test/validator/samples/a11y-autocomplete-valid/input.svelte b/test/validator/samples/a11y-autocomplete-valid/input.svelte new file mode 100644 index 0000000000..1f47878e37 --- /dev/null +++ b/test/validator/samples/a11y-autocomplete-valid/input.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/validator/samples/a11y-autocomplete-valid/warnings.json b/test/validator/samples/a11y-autocomplete-valid/warnings.json new file mode 100644 index 0000000000..dfde14a51f --- /dev/null +++ b/test/validator/samples/a11y-autocomplete-valid/warnings.json @@ -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 ", + "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 ", + "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 ", + "start": { + "column": 19, + "line": 21 + } + } +]