Skip to content

Commit 83e7f65

Browse files
feat: move to link commands (#63)
1 parent 0acdbd9 commit 83e7f65

File tree

3 files changed

+155
-9
lines changed

3 files changed

+155
-9
lines changed

src/commands/index.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,15 @@ const quickLandmarkNavigationRoles = [
8383

8484
const quickAriaRoleNavigationRoles = [
8585
...quickLandmarkNavigationRoles,
86-
8786
/**
8887
* WAI-ARIA doesn't specify that assistive technologies should enable users
8988
* to quickly navigate to elements with role heading. However, it is very
9089
* common for assistive technology users to navigate between headings.
9190
*
9291
* REF:
9392
* - https://www.w3.org/TR/wai-aria-1.2/#heading
94-
* - https://webaim.org/projects/screenreadersurvey9/#heading
93+
* - https://webaim.org/projects/screenreadersurvey10/#heading
94+
* - https://webaim.org/projects/screenreadersurvey10/#finding
9595
*
9696
* MUST requirements:
9797
*
@@ -101,6 +101,16 @@ const quickAriaRoleNavigationRoles = [
101101
* REF: https://a11ysupport.io/tech/aria/heading_role
102102
*/
103103
"heading",
104+
/**
105+
* WAI-ARIA doesn't specify that assistive technologies should enable users
106+
* to quickly navigate to elements with role link. However, it is very
107+
* common for assistive technology users to navigate between links.
108+
*
109+
* REF:
110+
* - https://www.w3.org/TR/wai-aria-1.2/#link
111+
* - https://webaim.org/projects/screenreadersurvey10/#finding
112+
*/
113+
"link",
104114
] as const;
105115

106116
interface QuickAriaRoleNavigationCommands {
@@ -504,6 +514,46 @@ interface QuickAriaRoleNavigationCommands {
504514
* ```
505515
*/
506516
moveToPreviousHeading: (args: VirtualCommandArgs) => number | null;
517+
/**
518+
* Move to the next element with a [`link`](https://www.w3.org/TR/wai-aria-1.2/#link)
519+
* role.
520+
*
521+
* ```ts
522+
* import { virtual } from "@guidepup/virtual-screen-reader";
523+
*
524+
* test("example test", async () => {
525+
* // Start the Virtual Screen Reader.
526+
* await virtual.start({ container: document.body });
527+
*
528+
* // Perform action to move to the next link element.
529+
* await virtual.perform(virtual.commands.moveToNextLink);
530+
*
531+
* // Stop the Virtual Screen Reader.
532+
* await virtual.stop();
533+
* });
534+
* ```
535+
*/
536+
moveToNextLink: (args: VirtualCommandArgs) => number | null;
537+
/**
538+
* Move to the previous element with a [`link`](https://www.w3.org/TR/wai-aria-1.2/#link)
539+
* role.
540+
*
541+
* ```ts
542+
* import { virtual } from "@guidepup/virtual-screen-reader";
543+
*
544+
* test("example test", async () => {
545+
* // Start the Virtual Screen Reader.
546+
* await virtual.start({ container: document.body });
547+
*
548+
* // Perform action to move to the previous link element.
549+
* await virtual.perform(virtual.commands.moveToPreviousLink);
550+
*
551+
* // Stop the Virtual Screen Reader.
552+
* await virtual.stop();
553+
* });
554+
* ```
555+
*/
556+
moveToPreviousLink: (args: VirtualCommandArgs) => number | null;
507557
}
508558

509559
const quickAriaRoleNavigationCommands: QuickAriaRoleNavigationCommands =

test/int/moveToLink.int.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { virtual } from "../../src/index.js";
2+
3+
describe("Move To Link", () => {
4+
afterEach(async () => {
5+
await virtual.stop();
6+
document.body.innerHTML = "";
7+
});
8+
9+
describe("moveToNextLink", () => {
10+
describe("when there are links in the container", () => {
11+
beforeEach(async () => {
12+
document.body.innerHTML = `
13+
<a href="/path-1">Link 1</a>
14+
<a href="/path-2">Link 2</a>
15+
<a href="/path-3">Link 3</a>
16+
`;
17+
18+
await virtual.start({ container: document.body });
19+
});
20+
21+
it("should let you navigate to the next link", async () => {
22+
while (!(await virtual.lastSpokenPhrase()).includes("Link 3")) {
23+
await virtual.perform(virtual.commands.moveToNextLink);
24+
}
25+
26+
const spokenPhraseLog = await virtual.spokenPhraseLog();
27+
expect(spokenPhraseLog).toEqual([
28+
"document",
29+
"link, Link 1",
30+
"link, Link 2",
31+
"link, Link 3",
32+
]);
33+
});
34+
});
35+
36+
describe("when there is no link in the container", () => {
37+
beforeEach(async () => {
38+
document.body.innerHTML = "";
39+
40+
await virtual.start({ container: document.body });
41+
});
42+
43+
it("should gracefully handle being asked to move to the next link", async () => {
44+
virtual.perform(virtual.commands.moveToNextLink);
45+
46+
expect(await virtual.spokenPhraseLog()).toEqual(["document"]);
47+
});
48+
});
49+
});
50+
51+
describe("moveToPreviousLink", () => {
52+
describe("when there are links in the container", () => {
53+
beforeEach(async () => {
54+
document.body.innerHTML = `
55+
<a href="/path-1">Link 1</a>
56+
<a href="/path-2">Link 2</a>
57+
<a href="/path-3">Link 3</a>
58+
`;
59+
60+
await virtual.start({ container: document.body });
61+
});
62+
63+
it("should let you navigate to the previous link", async () => {
64+
while (!(await virtual.lastSpokenPhrase()).includes("Link 1")) {
65+
await virtual.perform(virtual.commands.moveToPreviousLink);
66+
}
67+
68+
const spokenPhraseLog = await virtual.spokenPhraseLog();
69+
expect(spokenPhraseLog).toEqual([
70+
"document",
71+
"link, Link 3",
72+
"link, Link 2",
73+
"link, Link 1",
74+
]);
75+
});
76+
});
77+
78+
describe("when there is no link in the container", () => {
79+
beforeEach(async () => {
80+
document.body.innerHTML = "";
81+
82+
await virtual.start({ container: document.body });
83+
});
84+
85+
it("should gracefully handle being asked to move to the previous link", async () => {
86+
virtual.perform(virtual.commands.moveToPreviousLink);
87+
88+
expect(await virtual.spokenPhraseLog()).toEqual(["document"]);
89+
});
90+
});
91+
});
92+
});

test/int/moveToRole.int.test.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ const quickNavigationRoles = [
1212
"search",
1313
];
1414

15-
const quickNavigationRolesWithHeading = [...quickNavigationRoles, "heading"];
15+
const quickNavigationRolesWithHeadingAndLink = [
16+
...quickNavigationRoles,
17+
"heading",
18+
"link",
19+
];
1620

1721
const roleAttributesMap: Record<string, string> = {
1822
heading: ", level 2",
@@ -29,7 +33,7 @@ describe("Move To Role", () => {
2933
describe("moveToNextRole", () => {
3034
describe("when there are matching roles in the container", () => {
3135
beforeEach(async () => {
32-
document.body.innerHTML = quickNavigationRolesWithHeading
36+
document.body.innerHTML = quickNavigationRolesWithHeadingAndLink
3337
.map(
3438
(role) =>
3539
`<div role="${role}" aria-label="Accessible name">Node with role: ${role}</div>`
@@ -39,7 +43,7 @@ describe("Move To Role", () => {
3943
await virtual.start({ container: document.body });
4044
});
4145

42-
it.each(quickNavigationRolesWithHeading)(
46+
it.each(quickNavigationRolesWithHeadingAndLink)(
4347
"should let you navigate to the next %s",
4448
async (role) => {
4549
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -69,7 +73,7 @@ describe("Move To Role", () => {
6973
await virtual.start({ container: document.body });
7074
});
7175

72-
it.each(quickNavigationRolesWithHeading)(
76+
it.each(quickNavigationRolesWithHeadingAndLink)(
7377
"should gracefully handle being asked to move to the next %s",
7478
async (role) => {
7579
await virtual.perform(
@@ -90,7 +94,7 @@ describe("Move To Role", () => {
9094
describe("moveToPreviousRole", () => {
9195
describe("when there are matching roles in the container", () => {
9296
beforeEach(async () => {
93-
document.body.innerHTML = quickNavigationRolesWithHeading
97+
document.body.innerHTML = quickNavigationRolesWithHeadingAndLink
9498
.map(
9599
(role) =>
96100
`<div role="${role}" aria-label="Accessible name">Node with role: ${role}</div>`
@@ -100,7 +104,7 @@ describe("Move To Role", () => {
100104
await virtual.start({ container: document.body });
101105
});
102106

103-
it.each(quickNavigationRolesWithHeading)(
107+
it.each(quickNavigationRolesWithHeadingAndLink)(
104108
"should let you navigate to the previous %s",
105109
async (role) => {
106110
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -130,7 +134,7 @@ describe("Move To Role", () => {
130134
await virtual.start({ container: document.body });
131135
});
132136

133-
it.each(quickNavigationRolesWithHeading)(
137+
it.each(quickNavigationRolesWithHeadingAndLink)(
134138
"should gracefully handle being asked to move to the previous %s",
135139
async (role) => {
136140
await virtual.perform(

0 commit comments

Comments
 (0)