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

SIA-R16: only apply to explicit role #1185

Merged
merged 5 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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 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
48 changes: 0 additions & 48 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 @@ -128,23 +100,3 @@ 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>
);

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

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