Skip to content

Commit

Permalink
feat: add a11y autocomplete-valid
Browse files Browse the repository at this point in the history
Part of sveltejs#820
  • Loading branch information
cruessler committed Apr 20, 2023
1 parent 6ba2f72 commit db072c3
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/compiler/compile/compiler_warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <input type="${type}">`
}),
a11y_img_redundant_alt: {
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
Expand Down
14 changes: 13 additions & 1 deletion src/compiler/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -848,6 +848,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: null | true | string = type.get_static_value();
const autocomplete_value: null | true | string = 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') {
Expand Down
192 changes: 192 additions & 0 deletions src/compiler/compile/utils/a11y.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -223,3 +224,194 @@ 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([
'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'
]);

const control_group_text_types = new Set(['hidden', 'text', 'search']);
const control_group_multiline_types = new Set(['hidden']);
const control_group_password_types = new Set(['hidden', 'text', 'search', 'password']);
const control_group_url_types = new Set(['hidden', 'text', 'search', 'url']);
const control_group_username_types = new Set(['hidden', 'text', 'search', 'email']);
const control_group_telephone_types = new Set(['hidden', 'text', 'search', 'tel']);
const control_group_numeric_types = new Set(['hidden', 'text', 'search', 'number']);
const control_group_month_types = new Set(['hidden', 'text', 'search', 'month']);
const control_group_date_types = new Set(['hidden', 'text', 'search', 'date']);

const appropriate_types_for_field_names = new Map([
['name', control_group_text_types],
['honorific-prefix', control_group_text_types],
['given-name', control_group_text_types],
['additional-name', control_group_text_types],
['family-name', control_group_text_types],
['honorific-suffix', control_group_text_types],
['nickname', control_group_text_types],
['organization-title', control_group_text_types],
['username', control_group_username_types],
['new-password', control_group_password_types],
['current-password', control_group_password_types],
['one-time-code', control_group_password_types],
['organization', control_group_text_types],
['street-address', control_group_multiline_types],
['address-line1', control_group_text_types],
['address-line2', control_group_text_types],
['address-line3', control_group_text_types],
['address-level4', control_group_text_types],
['address-level3', control_group_text_types],
['address-level2', control_group_text_types],
['address-level1', control_group_text_types],
['country', control_group_text_types],
['country-name', control_group_text_types],
['postal-code', control_group_text_types],
['cc-name', control_group_text_types],
['cc-given-name', control_group_text_types],
['cc-additional-name', control_group_text_types],
['cc-family-name', control_group_text_types],
['cc-number', control_group_text_types],
['cc-exp', control_group_month_types],
['cc-exp-month', control_group_numeric_types],
['cc-exp-year', control_group_numeric_types],
['cc-csc', control_group_text_types],
['cc-type', control_group_text_types],
['transaction-currency', control_group_text_types],
['transaction-amount', control_group_numeric_types],
['language', control_group_text_types],
['bday', control_group_date_types],
['bday-day', control_group_numeric_types],
['bday-month', control_group_numeric_types],
['bday-year', control_group_numeric_types],
['sex', control_group_text_types],
['url', control_group_url_types],
['photo', control_group_url_types],
['tel', control_group_telephone_types],
['tel-country-code', control_group_text_types],
['tel-national', control_group_text_types],
['tel-area-code', control_group_text_types],
['tel-local', control_group_text_types],
['tel-local-prefix', control_group_text_types],
['tel-local-suffix', control_group_text_types],
['tel-extension', control_group_text_types],
['email', control_group_username_types],
['impp', control_group_url_types]
]);

function is_appropriate_type_for_field_name(type: string, field_name: string) {
if (autofill_field_name_tokens.has(field_name)) {
return appropriate_types_for_field_names.get(field_name)?.has(type);
}

return false;
}

function is_appropriate_type_for_contact_field_name(type: string, field_name: string) {
if (autofill_contact_field_name_tokens.has(field_name)) {
return appropriate_types_for_field_names.get(field_name)?.has(type);
}

return false;
}

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);
const normalized_type = type.toLowerCase();

const input_wears_autofill_anchor_mantle = normalized_type === 'hidden';
const input_wears_autofill_expectation_mantle = !input_wears_autofill_anchor_mantle;

if (input_wears_autofill_expectation_mantle) {
if (tokens[0] === 'on' || tokens[0] === 'off') {
return tokens.length === 1;
}
}

if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) {
tokens.shift();
}

if (address_type_tokens.has(tokens[0])) {
tokens.shift();
}

if (is_appropriate_type_for_field_name(normalized_type, tokens[0])) {
tokens.shift();
} else {
if (contact_type_tokens.has(tokens[0])) {
tokens.shift();
}

if (is_appropriate_type_for_contact_field_name(normalized_type, tokens[0])) {
tokens.shift();
} else {
return false;
}
}

if (tokens[0] === 'webauthn') {
tokens.shift();
}

return tokens.length === 0;
}
22 changes: 22 additions & 0 deletions test/validator/samples/a11y-autocomplete-valid/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- 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" />

<!-- INVALID -->
<input type="text" autocomplete />
<input type="hidden" autocomplete="off" />
<input type="hidden" autocomplete="on" />
<input type="text" autocomplete="" />
<input type="text" autocomplete="incorrect" />
<input type="email" autocomplete="url" />
<!-- `street-address` is only valid for Control Group "Multiline" -->
<input type="text" autocomplete="section-blue shipping street-address" />
<input type="text" autocomplete="webauthn" />
98 changes: 98 additions & 0 deletions test/validator/samples/a11y-autocomplete-valid/warnings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
[
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 31,
"line": 14
},
"message": "A11y: The value 'true' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 14
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 39,
"line": 15
},
"message": "A11y: The value 'off' is not supported by the attribute 'autocomplete' on element <input type=\"hidden\">",
"start": {
"column": 21,
"line": 15
}
},
{
"code": "a11y-autocomplete-valid",
"message": "A11y: The value 'on' is not supported by the attribute 'autocomplete' on element <input type=\"hidden\">",
"end": {
"column": 38,
"line": 16
},
"start": {
"column": 21,
"line": 16
}
},
{
"code": "a11y-autocomplete-valid",
"message": "A11y: The value '' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"end": {
"column": 34,
"line": 17
},
"start": {
"column": 19,
"line": 17
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 43,
"line": 18
},
"message": "A11y: The value 'incorrect' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 18
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 38,
"line": 19
},
"message": "A11y: The value 'url' is not supported by the attribute 'autocomplete' on element <input type=\"email\">",
"start": {
"column": 20,
"line": 19
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 70,
"line": 21
},
"message": "A11y: The value 'section-blue shipping street-address' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 21
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 42,
"line": 22
},
"message": "A11y: The value 'webauthn' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 22
}
}
]

0 comments on commit db072c3

Please sign in to comment.