Skip to content
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
11 changes: 11 additions & 0 deletions .changeset/lucky-keys-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@siteimprove/alfa-rules": minor
---

**Added:** The new rule R120 checks that the access keys an element declares are usable.

It makes three checks. No other element may declare the same key, since two elements competing for one key means at most one of them can be reached and which one wins is left to the user agent. Each key must be a single character, since anything longer is not a key a user can press. And an element may not declare the same key twice.

Case is folded throughout, so `A` and `a` are the same key. Two elements declaring them compete, and `accesskey="a A"` on one element declares that key twice and is reported for the repeat. That is stricter than the W3C checker, which compares the tokens of a single value literally and accepts it.

The attribute holds a set of space-separated tokens, so `accesskey="a b"` declares two keys and each is checked separately.
2 changes: 2 additions & 0 deletions packages/alfa-rules/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import R111 from "./sia-r111/rule.ts";
import R113 from "./sia-r113/rule.ts";
import R116 from "./sia-r116/rule.ts";
import R119 from "./sia-r119/rule.ts";
import R120 from "./sia-r120/rule.ts";

export {
R1,
Expand Down Expand Up @@ -180,4 +181,5 @@ export {
R113,
R116,
R119,
R120,
};
178 changes: 178 additions & 0 deletions packages/alfa-rules/src/sia-r120/rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Diagnostic, Rule } from "@siteimprove/alfa-act";
import { Element, Namespace, Node, Query } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Err, Ok } from "@siteimprove/alfa-result";
import { Sequence } from "@siteimprove/alfa-sequence";
import { Style } from "@siteimprove/alfa-style";
import type { Page } from "@siteimprove/alfa-web";

import { expectation } from "../common/act/index.ts";
import { BestPractice } from "../requirements/index.ts";

import { Scope, Stability } from "../tags/index.ts";

const { hasNamespace } = Element;
const { and } = Predicate;
const { isRendered } = Style;
const { getElementDescendants } = Query;

/**
* This rule checks that the access keys an element declares are usable: that no
* other element declares the same key, that each key is a single character, and
* that the element does not declare the same key twice.
*
* Two elements competing for one key means at most one of them can be reached
* by it, and which one wins is left to the user agent.
*
* Case is folded throughout, since `A` and `a` reach the same physical key, so
* `accesskey="a A"` declares one key twice and is reported for the repeat. That
* is stricter than the specification, which asks only that the tokens not be
* "identical to another token", a literal comparison the W3C checker
* implements with a case-sensitive equality.
*
* Elements that are not rendered are left out, since their access keys cannot
* be activated, and a key declared only on such an element competes with
* nothing.
*
* {@link https://html.spec.whatwg.org/multipage/interaction.html#the-accesskey-attribute}
*/
export default Rule.Atomic.of<Page, Element>({
uri: "https://alfa.siteimprove.com/rules/sia-r120",
requirements: [BestPractice.of("accesskey-unique")],
tags: [Scope.Page, Stability.Stable],
evaluate({ device, document }) {
const elements = getElementDescendants(document, Node.fullTree)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot you mentioned "access keys are scoped to their individual Document." and that ", matching keys in a parent document and an <iframe> are incorrectly reported as competing". Please provide the link to online documentation where it has been mentioned that the keys in a parent document and an <iframe> must be unique with no overlap across.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I couldn’t find a source that explicitly says a parent document and an <iframe> may or may not reuse the same accesskey. The references I was relying on were the HTML Standard’s accesskey section and the browsing-context/document model:

So my earlier note was an inference from those sections, not a directly quoted requirement.

.filter(and(hasNamespace(Namespace.HTML), declaresAccesskey))
.filter(isRendered(device));

const accessKeyElementPairs = new Map<string, Array<Element>>();

for (const element of elements) {
for (const key of accesskeys(element)) {
const elementsForKey = accessKeyElementPairs.get(key) ?? [];

elementsForKey.push(element);
accessKeyElementPairs.set(key, elementsForKey);
}
}

return {
applicability() {
return elements;
},

expectations(target) {
const contested = accesskeys(target).filter(
(key) => (accessKeyElementPairs.get(key)?.length ?? 0) > 1,
);
const tooLong = tokens(target).reject((token) => token.length === 1);

const duplicated = repeatedTokens(target);

return {
1: expectation(
contested.isEmpty(),
() => Outcomes.HasUniqueAccesskeys,
() => Outcomes.HasNonUniqueAccesskeys(contested),
),
2: expectation(
tooLong.isEmpty(),
() => Outcomes.HasSingleCharacterAccesskeys,
() => Outcomes.HasMultiCharacterAccesskeys(tooLong),
),
3: expectation(
duplicated.isEmpty(),
() => Outcomes.HasDistinctAccesskeys,
() => Outcomes.HasRepeatedAccesskeys(duplicated),
),
};
},
};
},
});

function tokens(element: Element): Sequence<string> {
return Sequence.from(element.attribute("accesskey")).flatMap((attribute) =>
attribute.tokens(),
);
}

function accesskeys(element: Element): Sequence<string> {
return tokens(element)
.map((token) => token.toLowerCase())
.distinct();
}

function repeatedTokens(element: Element): Sequence<string> {
const seen = new Set<string>();
const repeated = new Set<string>();

for (const token of tokens(element).map((token) => token.toLowerCase())) {
if (seen.has(token)) {
repeated.add(token);
} else {
seen.add(token);
}
}

return Sequence.from(repeated);
}

const declaresAccesskey: Predicate<Element> = (element) =>
!tokens(element).isEmpty();

/**
* @public
*/
export namespace Outcomes {
export const HasUniqueAccesskeys = Ok.of(
Diagnostic.of(
`No access key of the element is declared by another element.`,
),
);

export const HasNonUniqueAccesskeys = (keys: Iterable<string>) =>
Err.of(
Diagnostic.of(
many(keys)
? `More than one element declares the access keys ${list(keys)}.`
: `More than one element declares the access key ${list(keys)}.`,
),
);

export const HasSingleCharacterAccesskeys = Ok.of(
Diagnostic.of(`Every access key of the element is a single character.`),
);

export const HasMultiCharacterAccesskeys = (keys: Iterable<string>) =>
Err.of(
Diagnostic.of(
many(keys)
? `The access keys ${list(keys)} are not single characters.`
: `The access key ${list(keys)} is not a single character.`,
),
);

export const HasDistinctAccesskeys = Ok.of(
Diagnostic.of(`The element declares each of its access keys once.`),
);

export const HasRepeatedAccesskeys = (keys: Iterable<string>) =>
Err.of(
Diagnostic.of(
many(keys)
? `The element declares the access keys ${list(keys)} more than once.`
: `The element declares the access key ${list(keys)} more than once.`,
),
);
}

function list(keys: Iterable<string>): string {
return Sequence.from(keys)
.map((key) => `"${key}"`)
.join(", ");
}

function many(keys: Iterable<string>): boolean {
return Sequence.from(keys).size > 1;
}
1 change: 1 addition & 0 deletions packages/alfa-rules/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"./sia-r117/rule.ts",
"./sia-r118/rule.ts",
"./sia-r119/rule.ts",
"./sia-r120/rule.ts",
"./tags/index.ts",
"./tags/stability.ts",
"./tags/scope.ts",
Expand Down
Loading
Loading