Skip to content

Commit 1a42dee

Browse files
authored
fix: skip unnamed forms in landmark navigation (#267)
1 parent 870d1a6 commit 1a42dee

4 files changed

Lines changed: 70 additions & 2 deletions

File tree

‎src/commands/getIndexByRoleAndAttributes.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { matchesAccessibleAttributes, matchesRoles } from "./nodeMatchers";
1+
import {
2+
matchesAccessibleAttributes,
3+
matchesAccessibleNameRequirement,
4+
matchesRoles,
5+
} from "./nodeMatchers";
26
import { AccessibilityNode } from "../createAccessibilityTree";
37
import type { AriaAttributes } from "./types";
48
import { END_OF_ROLE_PREFIX } from "../flattenTree";
@@ -9,6 +13,9 @@ export interface GetIndexFilters {
913

1014
/** Matches a node only if the node has **all** of these aria attributes */
1115
ariaAttributes?: Readonly<AriaAttributes>;
16+
17+
/** Matches a node with any of these roles only if it has an accessible name */
18+
rolesRequiringAccessibleName?: Readonly<string[]>;
1219
}
1320

1421
export function getIndexByRoleAndAttributes({
@@ -24,7 +31,11 @@ export function getIndexByRoleAndAttributes({
2431
(node) =>
2532
!node.spokenRole.startsWith(END_OF_ROLE_PREFIX) &&
2633
matchesRoles(node, filters.roles) &&
27-
matchesAccessibleAttributes(node, filters.ariaAttributes)
34+
matchesAccessibleAttributes(node, filters.ariaAttributes) &&
35+
matchesAccessibleNameRequirement(
36+
node,
37+
filters.rolesRequiringAccessibleName
38+
)
2839
);
2940

3041
if (!accessibilityNode) {

‎src/commands/index.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ const quickLandmarkNavigationRoles = [
8181
"search",
8282
] as const;
8383

84+
/**
85+
* A form is not exposed as a landmark region unless it has been provided an
86+
* accessible name. An unnamed form keeps the form role, but is not a landmark.
87+
*
88+
* REF:
89+
* - https://www.w3.org/TR/html-aam-1.0/#el-form
90+
* - https://www.w3.org/TR/wai-aria-1.3/#form
91+
*/
92+
const landmarkRolesRequiringAccessibleName = ["form"] as const;
93+
8494
const quickAriaRoleNavigationRoles = [
8595
...quickLandmarkNavigationRoles,
8696
/**
@@ -1013,6 +1023,7 @@ export const commands = {
10131023
*/
10141024
moveToNextLandmark: getNextIndexByRoleAndAttributes({
10151025
roles: quickLandmarkNavigationRoles,
1026+
rolesRequiringAccessibleName: landmarkRolesRequiringAccessibleName,
10161027
}),
10171028
/**
10181029
* Move to the previous element with any [`landmark`](https://www.w3.org/TR/wai-aria-1.2/#landmark)
@@ -1046,6 +1057,7 @@ export const commands = {
10461057
*/
10471058
moveToPreviousLandmark: getPreviousIndexByRoleAndAttributes({
10481059
roles: quickLandmarkNavigationRoles,
1060+
rolesRequiringAccessibleName: landmarkRolesRequiringAccessibleName,
10491061
}),
10501062
...headingLevelNavigationCommands,
10511063
};

‎src/commands/nodeMatchers.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,14 @@ export function matchesAccessibleAttributes(
2828

2929
return true;
3030
}
31+
32+
export function matchesAccessibleNameRequirement(
33+
node: AccessibilityNode,
34+
rolesRequiringAccessibleName?: Readonly<string[]>
35+
) {
36+
if (!rolesRequiringAccessibleName?.includes(node.role)) {
37+
return true;
38+
}
39+
40+
return !!node.accessibleName;
41+
}

‎test/int/moveToRole.int.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,40 @@ describe("Move To Role", () => {
211211
});
212212
});
213213

214+
describe("when there is a form without an accessible name in the container", () => {
215+
beforeEach(async () => {
216+
document.body.innerHTML = `
217+
<nav aria-label="Primary">Navigation content</nav>
218+
<form><input aria-label="Search query" /></form>
219+
<main aria-label="Content">Main content</main>
220+
`;
221+
222+
await virtual.start({ container: document.body });
223+
});
224+
225+
it("should skip the unnamed form when navigating to the next landmark", async () => {
226+
await virtual.perform(virtual.commands.moveToNextLandmark);
227+
await virtual.perform(virtual.commands.moveToNextLandmark);
228+
229+
expect(await virtual.spokenPhraseLog()).toEqual([
230+
"document",
231+
"navigation, Primary",
232+
"main, Content",
233+
]);
234+
});
235+
236+
it("should skip the unnamed form when navigating to the previous landmark", async () => {
237+
await virtual.perform(virtual.commands.moveToPreviousLandmark);
238+
await virtual.perform(virtual.commands.moveToPreviousLandmark);
239+
240+
expect(await virtual.spokenPhraseLog()).toEqual([
241+
"document",
242+
"main, Content",
243+
"navigation, Primary",
244+
]);
245+
});
246+
});
247+
214248
describe("when there are no matching roles in the container", () => {
215249
beforeEach(async () => {
216250
document.body.innerHTML = "";

0 commit comments

Comments
 (0)