Skip to content

feat: add a11y autocomplete-valid #8520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
109 changes: 109 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,111 @@ 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']);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not able to find any of the following tokens in either axobject-query or aria-query.

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'
]);

export function is_valid_autocomplete(type: null | true | string, autocomplete: null | true | string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types come from the inferred return type of get_static_value. I was unsure whether to add them, but wanted to be cautious in order not to miss edge cases.

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The language comes from the spec. Given that axe-core’s implementation does not implement this part of the spec, do you want me to remove it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly do you mean by that? If it's about simplifying this to always allow on and off, then yes, let's relax this


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 (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;
}
21 changes: 21 additions & 0 deletions test/validator/samples/a11y-autocomplete-valid/input.svelte
Original file line number Diff line number Diff line change
@@ -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" />

<!-- INVALID -->
<input type="text" autocomplete />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<input type="hidden" autocomplete="off" />
<input type="hidden" autocomplete="on" />
<input type="text" autocomplete="" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@dummdidumm dummdidumm Apr 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say let's relax this then to allow the empty and boolean value. We also need a test that checks if it succeeds if a synamic value is given, like autocomplete={foo}

<input type="text" autocomplete="incorrect" />
<input type="text" autocomplete="webauthn" />
74 changes: 74 additions & 0 deletions test/validator/samples/a11y-autocomplete-valid/warnings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 31,
"line": 16
},
"message": "A11y: The value 'true' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 16
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 39,
"line": 17
},
"message": "A11y: The value 'off' is not supported by the attribute 'autocomplete' on element <input type=\"hidden\">",
"start": {
"column": 21,
"line": 17
}
},
{
"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": 18
},
"start": {
"column": 21,
"line": 18
}
},
{
"code": "a11y-autocomplete-valid",
"message": "A11y: The value '' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"end": {
"column": 34,
"line": 19
},
"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
}
}
]