Skip to content

Commit

Permalink
SIA-R16: only apply to explicit role (#1185)
Browse files Browse the repository at this point in the history
* Restrict R16 to explicit roles
* Correct implicit role mapping of select elements
* Add inapplicable examples

Co-authored-by: Zsófia Tóth <[email protected]>
  • Loading branch information
Jym77 and Zsófia Tóth authored Jul 12, 2022
1 parent 0e9d541 commit 23c6d4d
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 53 deletions.
4 changes: 4 additions & 0 deletions docs/review/api/alfa-aria.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export namespace DOM {
hasHeadingLevel: typeof dom.hasHeadingLevel, // (undocumented)
hasImplicitRole: typeof dom.hasImplicitRole, // (undocumented)
hasIncorrectRoleWithoutName: typeof dom.hasIncorrectRoleWithoutName, // (undocumented)
hasNonDefaultRole: typeof dom.hasNonDefaultRole, // (undocumented)
hasNonEmptyAccessibleName: typeof dom.hasNonEmptyAccessibleName, // (undocumented)
hasRole: typeof dom.hasRole, // (undocumented)
isIgnored: typeof dom.isIgnored, // (undocumented)
Expand Down Expand Up @@ -216,6 +217,9 @@ function hasName_2(predicate?: Predicate<Name>): Predicate<Node>;
// @public (undocumented)
function hasName_2(name: string, ...rest: Array<string>): Predicate<Node>;

// @public (undocumented)
function hasNonDefaultRole(element: Element_2): boolean;

// @public (undocumented)
function hasNonEmptyAccessibleName<T extends Element_2 | Text_2>(device: Device): Predicate<T>;

Expand Down
1 change: 1 addition & 0 deletions packages/alfa-aria/src/dom/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./predicate/has-explicit-role";
export * from "./predicate/has-heading-level";
export * from "./predicate/has-implicit-role";
export * from "./predicate/has-incorrect-role-without-name";
export * from "./predicate/has-non-default-role";
export * from "./predicate/has-non-empty-accessible-name";
export * from "./predicate/has-role";
export * from "./predicate/is-included-accessibility-tree";
Expand Down
21 changes: 21 additions & 0 deletions packages/alfa-aria/src/dom/predicate/has-non-default-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Element } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";
import { hasExplicitRole } from "./has-explicit-role";
import { hasImplicitRole } from "./has-implicit-role";

const { not, test } = Predicate;

/**
* @public
*/
export function hasNonDefaultRole(element: Element): boolean {
return test(
hasExplicitRole((explicit) =>
test(
not(hasImplicitRole((implicit) => implicit.equals(explicit))),
element
)
),
element
);
}
14 changes: 7 additions & 7 deletions packages/alfa-aria/src/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,13 +597,13 @@ const Features: Features = {
section: html(() => Option.of(Role.of("region"))),

select: html(
() =>
// Despite what the HTML AAM specifies, we always map <select> elements
// to a listbox widget as they currently have no way of mapping to a
// valid combobox widget. As a combobox requires an owned textarea and a
// list of options, we will always end up mapping <select> elements to
// an invalid combobox widget.
Option.of(Role.of("listbox")),
(element) =>
test(
Element.hasDisplaySize((size) => size > 1),
element
)
? Option.of(Role.of("listbox"))
: Option.of(Role.of("combobox")),
function* (element) {
// https://w3c.github.io/html-aam/#att-disabled
for (const _ of element.attribute("disabled")) {
Expand Down
1 change: 1 addition & 0 deletions packages/alfa-aria/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export namespace DOM {
hasHeadingLevel,
hasImplicitRole,
hasIncorrectRoleWithoutName,
hasNonDefaultRole,
hasNonEmptyAccessibleName,
hasRole,
isIgnored,
Expand Down
1 change: 1 addition & 0 deletions packages/alfa-aria/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"src/dom/predicate/has-heading-level.ts",
"src/dom/predicate/has-implicit-role.ts",
"src/dom/predicate/has-incorrect-role-without-name.ts",
"src/dom/predicate/has-non-default-role.ts",
"src/dom/predicate/has-non-empty-accessible-name.ts",
"src/dom/predicate/has-role.ts",
"src/dom/predicate/is-included-accessibility-tree.ts",
Expand Down
4 changes: 2 additions & 2 deletions packages/alfa-rules/src/sia-r16/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as aria from "@siteimprove/alfa-aria";
import { expectation } from "../common/act/expectation";
import { Scope } from "../tags";

const { hasRole, isIncludedInTheAccessibilityTree } = DOM;
const { hasNonDefaultRole, isIncludedInTheAccessibilityTree } = DOM;
const { hasAttribute, hasInputType, hasName, hasNamespace, isElement } =
Element;
const { isEmpty } = Iterable;
Expand All @@ -33,7 +33,7 @@ export default Rule.Atomic.of<Page, Element>({
.descendants(Node.composedNested)
.filter(isElement)
.filter(
and(hasNamespace(Namespace.HTML, Namespace.SVG), hasRole(device))
and(hasNamespace(Namespace.HTML, Namespace.SVG), hasNonDefaultRole)
)
.filter(isIncludedInTheAccessibilityTree(device));
},
Expand Down
56 changes: 12 additions & 44 deletions packages/alfa-rules/test/sia-r16/rule.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,6 @@ test(`evaluate() passes a <div> element with a role of checkbox and an
]);
});

test(`evaluate() passes an <input> element with a type of checkbox`, async (t) => {
const target = <input type="checkbox" />;

const document = h.document([target]);

t.deepEqual(await evaluate(R16, { document }), [
passed(R16, target, {
1: Outcomes.HasAllStates(
RoleAndRequiredAttributes.of("", "checkbox", ["aria-checked"], [])
),
}),
]);
});

test(`evaluate() passes an <hr> element`, async (t) => {
const target = <hr />;

const document = h.document([target]);

t.deepEqual(await evaluate(R16, { document }), [
passed(R16, target, {
1: Outcomes.HasAllStates(
RoleAndRequiredAttributes.of("", "separator", [], [])
),
}),
]);
});

test(`evaluate() passes a non-focusable <div> element with a role of separator`, async (t) => {
const target = <div role="separator" />;

Expand Down Expand Up @@ -129,22 +101,18 @@ test("evaluate() is inapplicable to elements that are not exposed", async (t) =>
t.deepEqual(await evaluate(R16, { document }), [inapplicable(R16)]);
});

test(`evaluate() passes a native \`<input type="text" list="foo">\` combobox`, async (t) => {
const target = <input type="text" list="foo" />;
const datalist = (
<datalist id="foo">
<option value="foo">foo</option>
<option value="bar">bar</option>
</datalist>
);
test("evaluate() is inapplicable to elements with no explicit role", async (t) => {
const target = <input type="checkbox" />;

const document = h.document([target, datalist]);
const document = h.document([target]);

t.deepEqual(await evaluate(R16, { document }), [
passed(R16, target, {
1: Outcomes.HasAllStates(
RoleAndRequiredAttributes.of("", "combobox", ["aria-expanded"], [])
),
}),
]);
t.deepEqual(await evaluate(R16, { document }), [inapplicable(R16)]);
});

test("evaluate() is inapplicable to elements with same explicit and implicit role", async (t) => {
const target = <input type="checkbox" role="checkbox" />;

const document = h.document([target]);

t.deepEqual(await evaluate(R16, { document }), [inapplicable(R16)]);
});

0 comments on commit 23c6d4d

Please sign in to comment.