Skip to content
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

Expose autocomplete functionality on Attribute #1724

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/quick-months-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-dom": minor
---

**Added:** An `Attribute.Autocomplete` namespace is now available, grouping functionalities around the `autocomplete` attribute.
1 change: 1 addition & 0 deletions docs/review/api/alfa-dom.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export namespace Attribute {
//
// (undocumented)
hasName: typeof predicate_3.hasName;
import Autocomplete = autocomplete.Autocomplete;
}

// @public (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion docs/review/api/alfa-rules.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import * as act from '@siteimprove/alfa-act';
import { Array as Array_2 } from '@siteimprove/alfa-array';
import type { Attribute } from '@siteimprove/alfa-dom';
import { Attribute } from '@siteimprove/alfa-dom';
import type { Document } from '@siteimprove/alfa-dom';
import * as earl from '@siteimprove/alfa-earl';
import { Element } from '@siteimprove/alfa-dom';
Expand Down
3 changes: 3 additions & 0 deletions packages/alfa-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@
"@siteimprove/alfa-lazy": "workspace:^0.95.0",
"@siteimprove/alfa-map": "workspace:^0.95.0",
"@siteimprove/alfa-option": "workspace:^0.95.0",
"@siteimprove/alfa-parser": "workspace:^0.95.0",
"@siteimprove/alfa-predicate": "workspace:^0.95.0",
"@siteimprove/alfa-rectangle": "workspace:^0.95.0",
"@siteimprove/alfa-refinement": "workspace:^0.95.0",
"@siteimprove/alfa-result": "workspace:^0.95.0",
"@siteimprove/alfa-sarif": "workspace:^0.95.0",
"@siteimprove/alfa-selective": "workspace:^0.95.0",
"@siteimprove/alfa-sequence": "workspace:^0.95.0",
"@siteimprove/alfa-slice": "workspace:^0.95.0",
"@siteimprove/alfa-string": "workspace:^0.95.0",
"@siteimprove/alfa-trampoline": "workspace:^0.95.0",
"@siteimprove/alfa-tree": "workspace:^0.95.0"
Expand Down
3 changes: 3 additions & 0 deletions packages/alfa-dom/src/node/attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Node } from "../node.js";
import type { Element } from "./element.js";

import * as predicate from "./attribute/predicate.js";
import * as autocomplete from "./attribute/autocomplete.js";

const { isEmpty } = Iterable;
const { equals, not } = Predicate;
Expand Down Expand Up @@ -282,4 +283,6 @@ export namespace Attribute {
}

export const { hasName } = predicate;

export import Autocomplete = autocomplete.Autocomplete;
}
141 changes: 141 additions & 0 deletions packages/alfa-dom/src/node/attribute/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Predicate } from "@siteimprove/alfa-predicate";
import { Parser } from "@siteimprove/alfa-parser";
import { Slice } from "@siteimprove/alfa-slice";
import { Array } from "@siteimprove/alfa-array";
import { Err, Ok } from "@siteimprove/alfa-result";

const { either, end, option, right, parseIf } = Parser;

/**
* @public
*/
export namespace Autocomplete {
srnjcbsn marked this conversation as resolved.
Show resolved Hide resolved

/**
* Autofill detail tokens from steps 2-4 of the list in {@link https://html.spec.whatwg.org/multipage/#autofill-detail-tokens}.
*/
export namespace AutofillDetailTokens {
export const unmodifiables = [
"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",
];

export const modifiables = [
"tel",
"tel-country-code",
"tel-national",
"tel-area-code",
"tel-local",
"tel-local-prefix",
"tel-local-suffix",
"tel-extension",
"email",
"impp",
];

export const modifiers = ["home", "work", "mobile", "fax", "pager"];
export const addressTypes = ["shipping", "billing"];
export const webauthn = "webauthn";
}

/**
* Checks if the provided string contains a valid autocomplete string according to {@link https://html.spec.whatwg.org/multipage/#autofill-detail-tokens}
*/
export const isValid: Predicate<string> = (autocomplete) => {
// The following line comments each refers to the corresponding position in the HTML specification linked above at the time of writing
const parse = right(
option(section), // 1.
right(
option(addressType), // 2.
right(
// 3.
either(
unmodifiable, // 3.a
right(option(modifier) /*3.b.1*/, modifiable /*3.b.2*/),
),
right(
option(webauthn), // 4.
end((token) => `Expected EOF, but got ${token}`),
),
),
),
);

return parse(Slice.of(tokenize(autocomplete))).isOk();
}

export function tokenize(autocomplete: string): Array<string> {
return Array.from(autocomplete.toLowerCase().trim().split(/\s+/));
}

const parseFirst: Parser<Slice<string>, string, string> = (
input: Slice<string>,
) =>
input
.first()
.map((token) => Ok.of<[Slice<string>, string]>([input.rest(), token]))
.getOr(Err.of("No token left"));

function parserOf(
tokens: Array<string>,
): Parser<Slice<string>, string, string> {
return parseIf(
(token): token is string => tokens.includes(token),
parseFirst,
(token) => `Expected valid token, but got ${token}`,
);
}

const addressType = parserOf(AutofillDetailTokens.addressTypes);
const unmodifiable = parserOf(AutofillDetailTokens.unmodifiables);
const section: Parser<Slice<string>, string, string> = parseIf(
(token): token is string => token.startsWith("section-"),
parseFirst,
(token) => `Expected token beginning with \`section-\`, but got ${token}`,
);
const modifiable = parserOf(AutofillDetailTokens.modifiables);
const modifier = parserOf(AutofillDetailTokens.modifiers);
const webauthn = parserOf([AutofillDetailTokens.webauthn]);
}
4 changes: 4 additions & 0 deletions packages/alfa-dom/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"./node/attribute.ts",
"./node/attribute/predicate.ts",
"./node/attribute/predicate/has-name.ts",
"./node/attribute/autocomplete.ts",
"./node/comment.ts",
"./node/document.ts",
"./node/element.ts",
Expand Down Expand Up @@ -88,12 +89,15 @@
{ "path": "../../alfa-lazy" },
{ "path": "../../alfa-map" },
{ "path": "../../alfa-option" },
{ "path": "../../alfa-parser" },
{ "path": "../../alfa-predicate" },
{ "path": "../../alfa-rectangle" },
{ "path": "../../alfa-refinement" },
{ "path": "../../alfa-result" },
{ "path": "../../alfa-sarif" },
{ "path": "../../alfa-selective" },
{ "path": "../../alfa-sequence" },
{ "path": "../../alfa-slice" },
{ "path": "../../alfa-string" },
{ "path": "../../alfa-trampoline" },
{ "path": "../../alfa-tree" }
Expand Down
2 changes: 0 additions & 2 deletions packages/alfa-rules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
"@siteimprove/alfa-map": "workspace:^0.95.0",
"@siteimprove/alfa-math": "workspace:^0.95.0",
"@siteimprove/alfa-option": "workspace:^0.95.0",
"@siteimprove/alfa-parser": "workspace:^0.95.0",
"@siteimprove/alfa-predicate": "workspace:^0.95.0",
"@siteimprove/alfa-record": "workspace:^0.95.0",
"@siteimprove/alfa-rectangle": "workspace:^0.95.0",
Expand All @@ -65,7 +64,6 @@
"@siteimprove/alfa-selector": "workspace:^0.95.0",
"@siteimprove/alfa-sequence": "workspace:^0.95.0",
"@siteimprove/alfa-set": "workspace:^0.95.0",
"@siteimprove/alfa-slice": "workspace:^0.95.0",
"@siteimprove/alfa-string": "workspace:^0.95.0",
"@siteimprove/alfa-style": "workspace:^0.95.0",
"@siteimprove/alfa-table": "workspace:^0.95.0",
Expand Down
130 changes: 3 additions & 127 deletions packages/alfa-rules/src/sia-r10/rule.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Diagnostic, Rule } from "@siteimprove/alfa-act";
import { DOM, Node } from "@siteimprove/alfa-aria";
import { Array } from "@siteimprove/alfa-array";
import type { Attribute} from "@siteimprove/alfa-dom";
import { Element, Namespace, Query } from "@siteimprove/alfa-dom";
import { Parser } from "@siteimprove/alfa-parser";
import { Element, Namespace, Query, Attribute } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Err, Ok } from "@siteimprove/alfa-result";
import { Slice } from "@siteimprove/alfa-slice";
import { String } from "@siteimprove/alfa-string";
import { Style } from "@siteimprove/alfa-style";
import { Criterion } from "@siteimprove/alfa-wcag";
Expand All @@ -22,7 +18,6 @@ const { hasRole, isPerceivableForAll } = DOM;
const { hasAttribute, hasInputType, hasName, hasNamespace } = Element;
const { and, or, not } = Predicate;
const { isTabbable } = Style;
const { either, end, option, right, parseIf } = Parser;
const { getElementDescendants } = Query;

export default Rule.Atomic.of<Page, Attribute>({
Expand Down Expand Up @@ -65,7 +60,7 @@ export default Rule.Atomic.of<Page, Attribute>({
expectations(target) {
return {
1: expectation(
isValidAutocomplete(target),
Attribute.Autocomplete.isValid(target.value),
Jym77 marked this conversation as resolved.
Show resolved Hide resolved
() => Outcomes.HasValidValue,
() => Outcomes.HasNoValidValue,
),
Expand All @@ -76,128 +71,9 @@ export default Rule.Atomic.of<Page, Attribute>({
});

function hasTokens(input: string): boolean {
return input.trim() !== "" && input.split(/\s+/).length > 0;
return input.trim() !== "" && Attribute.Autocomplete.tokenize(input).length > 0;
}

/**
* {@link https://html.spec.whatwg.org/multipage/#autofill-detail-tokens}
*/
const isValidAutocomplete: Predicate<Attribute> = (autocomplete) => {
const tokens = autocomplete.value.toLowerCase().trim().split(/\s+/);

// The following line comments each refers to the corresponding position in the HTML specification linked above at the time of writing
const parse = right(
option(section), // 1.
right(
option(addressType), // 2.
right(
// 3.
either(
unmodifiable, // 3.a
right(option(modifier) /*3.b.1*/, modifiable /*3.b.2*/),
),
right(
option(webauthn), // 4.
end((token) => `Expected EOF, but got ${token}`),
),
),
),
);

return parse(Slice.of(tokens)).isOk();
};

const unmodifiables = Array.from([
"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 modifiables = Array.from([
"tel",
"tel-country-code",
"tel-national",
"tel-area-code",
"tel-local",
"tel-local-prefix",
"tel-local-suffix",
"tel-extension",
"email",
"impp",
]);

const modifiers = Array.from(["home", "work", "mobile", "fax", "pager"]);

const parseFirst: Parser<Slice<string>, string, string> = (
input: Slice<string>,
) =>
input
.first()
.map((token) => Ok.of<[Slice<string>, string]>([input.rest(), token]))
.getOr(Err.of("No token left"));

function parserOf(
tokens: Array<string>,
): Parser<Slice<string>, string, string> {
return parseIf(
(token): token is string => tokens.includes(token),
parseFirst,
(token) => `Expected valid token, but got ${token}`,
);
}

const addressType = parserOf(["shipping", "billing"]);
const unmodifiable = parserOf(unmodifiables);
const section: Parser<Slice<string>, string, string> = parseIf(
(token): token is string => token.startsWith("section-"),
parseFirst,
(token) => `Expected token beginning with \`section-\`, but got ${token}`,
);
const modifiable = parserOf(modifiables);
const modifier = parserOf(modifiers);
const webauthn = parserOf(["webauthn"]);

/**
* @public
*/
Expand Down
Loading