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
15 changes: 13 additions & 2 deletions src/commands/getIndexByRoleAndAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { matchesAccessibleAttributes, matchesRoles } from "./nodeMatchers";
import {
matchesAccessibleAttributes,
matchesAccessibleNameRequirement,
matchesRoles,
} from "./nodeMatchers";
import { AccessibilityNode } from "../createAccessibilityTree";
import type { AriaAttributes } from "./types";
import { END_OF_ROLE_PREFIX } from "../flattenTree";
Expand All @@ -9,6 +13,9 @@ export interface GetIndexFilters {

/** Matches a node only if the node has **all** of these aria attributes */
ariaAttributes?: Readonly<AriaAttributes>;

/** Matches a node with any of these roles only if it has an accessible name */
rolesRequiringAccessibleName?: Readonly<string[]>;
}

export function getIndexByRoleAndAttributes({
Expand All @@ -24,7 +31,11 @@ export function getIndexByRoleAndAttributes({
(node) =>
!node.spokenRole.startsWith(END_OF_ROLE_PREFIX) &&
matchesRoles(node, filters.roles) &&
matchesAccessibleAttributes(node, filters.ariaAttributes)
matchesAccessibleAttributes(node, filters.ariaAttributes) &&
matchesAccessibleNameRequirement(
node,
filters.rolesRequiringAccessibleName
)
);

if (!accessibilityNode) {
Expand Down
12 changes: 12 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ const quickLandmarkNavigationRoles = [
"search",
] as const;

/**
* A form is not exposed as a landmark region unless it has been provided an
* accessible name. An unnamed form keeps the form role, but is not a landmark.
*
* REF:
* - https://www.w3.org/TR/html-aam-1.0/#el-form
* - https://www.w3.org/TR/wai-aria-1.3/#form
*/
const landmarkRolesRequiringAccessibleName = ["form"] as const;

const quickAriaRoleNavigationRoles = [
...quickLandmarkNavigationRoles,
/**
Expand Down Expand Up @@ -1013,6 +1023,7 @@ export const commands = {
*/
moveToNextLandmark: getNextIndexByRoleAndAttributes({
roles: quickLandmarkNavigationRoles,
rolesRequiringAccessibleName: landmarkRolesRequiringAccessibleName,
}),
/**
* Move to the previous element with any [`landmark`](https://www.w3.org/TR/wai-aria-1.2/#landmark)
Expand Down Expand Up @@ -1046,6 +1057,7 @@ export const commands = {
*/
moveToPreviousLandmark: getPreviousIndexByRoleAndAttributes({
roles: quickLandmarkNavigationRoles,
rolesRequiringAccessibleName: landmarkRolesRequiringAccessibleName,
}),
...headingLevelNavigationCommands,
};
Expand Down
11 changes: 11 additions & 0 deletions src/commands/nodeMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ export function matchesAccessibleAttributes(

return true;
}

export function matchesAccessibleNameRequirement(
node: AccessibilityNode,
rolesRequiringAccessibleName?: Readonly<string[]>
) {
if (!rolesRequiringAccessibleName?.includes(node.role)) {
return true;
}

return !!node.accessibleName;
}
34 changes: 34 additions & 0 deletions test/int/moveToRole.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,40 @@ describe("Move To Role", () => {
});
});

describe("when there is a form without an accessible name in the container", () => {
beforeEach(async () => {
document.body.innerHTML = `
<nav aria-label="Primary">Navigation content</nav>
<form><input aria-label="Search query" /></form>
<main aria-label="Content">Main content</main>
`;

await virtual.start({ container: document.body });
});

it("should skip the unnamed form when navigating to the next landmark", async () => {
await virtual.perform(virtual.commands.moveToNextLandmark);
await virtual.perform(virtual.commands.moveToNextLandmark);

expect(await virtual.spokenPhraseLog()).toEqual([
"document",
"navigation, Primary",
"main, Content",
]);
});

it("should skip the unnamed form when navigating to the previous landmark", async () => {
await virtual.perform(virtual.commands.moveToPreviousLandmark);
await virtual.perform(virtual.commands.moveToPreviousLandmark);

expect(await virtual.spokenPhraseLog()).toEqual([
"document",
"main, Content",
"navigation, Primary",
]);
});
});

describe("when there are no matching roles in the container", () => {
beforeEach(async () => {
document.body.innerHTML = "";
Expand Down
Loading