Skip to content

Commit bcb3c5c

Browse files
benmonroBen Monrokentcdodds
authored andcommitted
feat: focus trap on tab (#193)
feat: focus trap on tab Co-authored-by: Ben Monro <[email protected]> Co-authored-by: Kent C. Dodds <[email protected]>
1 parent 9ab5b2b commit bcb3c5c

File tree

3 files changed

+220
-105
lines changed

3 files changed

+220
-105
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,57 @@ expect(getByTestId("val3").selected).toBe(true);
167167
The `values` parameter can be either an array of values or a singular scalar
168168
value.
169169

170+
### `tab({shift, focusTrap})`
171+
172+
Fires a tab event changing the document.activeElement in the same way the
173+
browser does.
174+
175+
Options:
176+
177+
- `shift` (default `false`) can be true or false to invert tab direction.
178+
- `focusTrap` (default `document`) a container element to restrict the tabbing
179+
within.
180+
181+
> **A note about tab**: [jsdom does not support tabbing](https://github.com/jsdom/jsdom/issues/2102), so this feature is a way to enable tests to verify tabbing from the end user's perspective. However, this limitation in jsdom will mean that components like [focus-trap-react](https://github.com/davidtheclark/focus-trap-react) will not work with `userEvent.tab()` or jsdom. For that reason, the `focusTrap` option is available to let you ensure your user is restricted within a focus-trap.
182+
183+
```jsx
184+
import React from "react";
185+
import { render } from "@testing-library/react";
186+
import "@testing-library/jest-dom/extend-expect";
187+
import userEvent from "@testing-library/user-event";
188+
189+
it("should cycle elements in document tab order", () => {
190+
const { getAllByTestId } = render(
191+
<div>
192+
<input data-testid="element" type="checkbox" />
193+
<input data-testid="element" type="radio" />
194+
<input data-testid="element" type="number" />
195+
</div>
196+
);
197+
198+
const [checkbox, radio, number] = getAllByTestId("element");
199+
200+
expect(document.body).toHaveFocus();
201+
202+
userEvent.tab();
203+
204+
expect(checkbox).toHaveFocus();
205+
206+
userEvent.tab();
207+
208+
expect(radio).toHaveFocus();
209+
210+
userEvent.tab();
211+
212+
expect(number).toHaveFocus();
213+
214+
userEvent.tab();
215+
216+
// cycle goes back to first element
217+
expect(checkbox).toHaveFocus();
218+
});
219+
```
220+
170221
## Contributors
171222

172223
Thanks goes to these wonderful people
@@ -197,6 +248,7 @@ Thanks goes to these wonderful people
197248

198249
<!-- markdownlint-enable -->
199250
<!-- prettier-ignore-end -->
251+
200252
<!-- ALL-CONTRIBUTORS-LIST:END -->
201253

202254
This project follows the

__tests__/react/tab.js

Lines changed: 164 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -4,156 +4,219 @@ import "@testing-library/jest-dom/extend-expect";
44
import userEvent from "../../src";
55

66
describe("userEvent.tab", () => {
7-
it("should cycle elements in document tab order", () => {
8-
const { getAllByTestId } = render(
9-
<div>
10-
<input data-testid="element" type="checkbox" />
11-
<input data-testid="element" type="radio" />
12-
<input data-testid="element" type="number" />
13-
</div>
14-
);
7+
it("should cycle elements in document tab order", () => {
8+
const { getAllByTestId } = render(
9+
<div>
10+
<input data-testid="element" type="checkbox" />
11+
<input data-testid="element" type="radio" />
12+
<input data-testid="element" type="number" />
13+
</div>
14+
);
1515

16-
const [checkbox, radio, number] = getAllByTestId("element");
16+
const [checkbox, radio, number] = getAllByTestId("element");
1717

18-
expect(document.activeElement).toBe(document.body);
18+
expect(document.body).toHaveFocus();
1919

20-
userEvent.tab();
20+
userEvent.tab();
2121

22-
expect(document.activeElement).toBe(checkbox);
22+
expect(checkbox).toHaveFocus();
2323

24-
userEvent.tab();
24+
userEvent.tab();
2525

26-
expect(document.activeElement).toBe(radio);
26+
expect(radio).toHaveFocus();
2727

28-
userEvent.tab();
28+
userEvent.tab();
2929

30-
expect(document.activeElement).toBe(number);
30+
expect(number).toHaveFocus();
3131

32-
userEvent.tab();
32+
userEvent.tab();
3333

34-
// cycle goes back to first element
35-
expect(document.activeElement).toBe(checkbox);
36-
});
34+
// cycle goes back to first element
35+
expect(checkbox).toHaveFocus();
36+
});
3737

38-
it("should go backwards when shift = true", () => {
39-
const { getAllByTestId } = render(
40-
<div>
41-
<input data-testid="element" type="checkbox" />
42-
<input data-testid="element" type="radio" />
43-
<input data-testid="element" type="number" />
44-
</div>
45-
);
38+
it("should go backwards when shift = true", () => {
39+
const { getAllByTestId } = render(
40+
<div>
41+
<input data-testid="element" type="checkbox" />
42+
<input data-testid="element" type="radio" />
43+
<input data-testid="element" type="number" />
44+
</div>
45+
);
4646

47-
const [checkbox, radio, number] = getAllByTestId("element");
47+
const [checkbox, radio, number] = getAllByTestId("element");
4848

49-
radio.focus();
49+
radio.focus();
5050

51-
userEvent.tab({ shift: true });
51+
userEvent.tab({ shift: true });
5252

53-
expect(document.activeElement).toBe(checkbox);
53+
expect(checkbox).toHaveFocus();
5454

55-
userEvent.tab({ shift: true });
55+
userEvent.tab({ shift: true });
5656

57-
expect(document.activeElement).toBe(number);
58-
});
57+
expect(number).toHaveFocus();
58+
});
5959

60-
it("should respect tabindex, regardless of dom position", () => {
61-
const { getAllByTestId } = render(
62-
<div>
63-
<input data-testid="element" tabIndex={2} type="checkbox" />
64-
<input data-testid="element" tabIndex={1} type="radio" />
65-
<input data-testid="element" tabIndex={3} type="number" />
66-
</div>
67-
);
60+
it("should respect tabindex, regardless of dom position", () => {
61+
const { getAllByTestId } = render(
62+
<div>
63+
<input data-testid="element" tabIndex={2} type="checkbox" />
64+
<input data-testid="element" tabIndex={1} type="radio" />
65+
<input data-testid="element" tabIndex={3} type="number" />
66+
</div>
67+
);
6868

69-
const [checkbox, radio, number] = getAllByTestId("element");
69+
const [checkbox, radio, number] = getAllByTestId("element");
7070

71-
userEvent.tab();
71+
userEvent.tab();
7272

73-
expect(document.activeElement).toBe(radio);
73+
expect(radio).toHaveFocus();
7474

75-
userEvent.tab();
75+
userEvent.tab();
7676

77-
expect(document.activeElement).toBe(checkbox);
77+
expect(checkbox).toHaveFocus();
7878

79-
userEvent.tab();
79+
userEvent.tab();
8080

81-
expect(document.activeElement).toBe(number);
81+
expect(number).toHaveFocus();
8282

83-
userEvent.tab();
83+
userEvent.tab();
8484

85-
expect(document.activeElement).toBe(radio);
86-
});
85+
expect(radio).toHaveFocus();
86+
});
8787

88-
it('should respect dom order when tabindex are all the same', () => {
89-
const { getAllByTestId } = render(
90-
<div>
91-
<input data-testid="element" tabIndex={0} type="checkbox" />
92-
<input data-testid="element" tabIndex={1} type="radio" />
93-
<input data-testid="element" tabIndex={0} type="number" />
94-
</div>
95-
);
88+
it("should respect dom order when tabindex are all the same", () => {
89+
const { getAllByTestId } = render(
90+
<div>
91+
<input data-testid="element" tabIndex={0} type="checkbox" />
92+
<input data-testid="element" tabIndex={1} type="radio" />
93+
<input data-testid="element" tabIndex={0} type="number" />
94+
</div>
95+
);
9696

97-
const [checkbox, radio, number] = getAllByTestId("element");
97+
const [checkbox, radio, number] = getAllByTestId("element");
9898

99-
userEvent.tab();
99+
userEvent.tab();
100100

101-
expect(document.activeElement).toBe(checkbox);
101+
expect(checkbox).toHaveFocus();
102102

103-
userEvent.tab();
103+
userEvent.tab();
104104

105-
expect(document.activeElement).toBe(number);
105+
expect(number).toHaveFocus();
106106

107-
userEvent.tab();
107+
userEvent.tab();
108108

109-
expect(document.activeElement).toBe(radio);
109+
expect(radio).toHaveFocus();
110110

111-
userEvent.tab();
111+
userEvent.tab();
112112

113-
expect(document.activeElement).toBe(checkbox);
114-
});
113+
expect(checkbox).toHaveFocus();
114+
});
115115

116-
it('should suport a mix of elements with/without tab index', () => {
117-
const { getAllByTestId } = render(
118-
<div>
119-
<input data-testid="element" tabIndex={0} type="checkbox" />
120-
<input data-testid="element" tabIndex={1} type="radio" />
121-
<input data-testid="element" type="number" />
122-
</div>
123-
);
116+
it("should suport a mix of elements with/without tab index", () => {
117+
const { getAllByTestId } = render(
118+
<div>
119+
<input data-testid="element" tabIndex={0} type="checkbox" />
120+
<input data-testid="element" tabIndex={1} type="radio" />
121+
<input data-testid="element" type="number" />
122+
</div>
123+
);
124124

125-
const [checkbox, radio, number] = getAllByTestId("element");
125+
const [checkbox, radio, number] = getAllByTestId("element");
126126

127-
userEvent.tab();
127+
userEvent.tab();
128128

129-
expect(document.activeElement).toBe(checkbox);
130-
userEvent.tab();
129+
expect(checkbox).toHaveFocus();
130+
userEvent.tab();
131131

132-
expect(document.activeElement).toBe(number);
133-
userEvent.tab();
132+
expect(number).toHaveFocus();
133+
userEvent.tab();
134134

135-
expect(document.activeElement).toBe(radio);
135+
expect(radio).toHaveFocus();
136+
});
136137

137-
});
138+
it("should not tab to <a> with no href", () => {
139+
const { getAllByTestId } = render(
140+
<div>
141+
<input data-testid="element" tabIndex={0} type="checkbox" />
142+
<a>ignore this</a>
143+
<a data-testid="element" href="http://www.testingjavascript.com">
144+
a link
145+
</a>
146+
</div>
147+
);
138148

139-
it('should not tab to <a> with no href', () => {
140-
const { getAllByTestId } = render(
141-
<div>
142-
<input data-testid="element" tabIndex={0} type="checkbox" />
143-
<a>ignore this</a>
144-
<a data-testid="element" href="http://www.testingjavascript.com">a link</a>
145-
</div>
146-
);
149+
const [checkbox, link] = getAllByTestId("element");
147150

148-
const [checkbox, link] = getAllByTestId("element");
151+
userEvent.tab();
149152

150-
userEvent.tab();
153+
expect(checkbox).toHaveFocus();
151154

152-
expect(document.activeElement).toBe(checkbox);
155+
userEvent.tab();
153156

154-
userEvent.tab();
157+
expect(link).toHaveFocus();
158+
});
155159

156-
expect(document.activeElement).toBe(link);
157-
});
160+
it("should stay within a focus trab", () => {
161+
const { getAllByTestId, getByTestId } = render(
162+
<>
163+
<div data-testid="div1">
164+
<input data-testid="element" type="checkbox" />
165+
<input data-testid="element" type="radio" />
166+
<input data-testid="element" type="number" />
167+
</div>
168+
<div data-testid="div2">
169+
<input data-testid="element" foo="bar" type="checkbox" />
170+
<input data-testid="element" foo="bar" type="radio" />
171+
<input data-testid="element" foo="bar" type="number" />
172+
</div>
173+
</>
174+
);
158175

176+
const [div1, div2] = [getByTestId("div1"), getByTestId("div2")];
177+
const [
178+
checkbox1,
179+
radio1,
180+
number1,
181+
checkbox2,
182+
radio2,
183+
number2
184+
] = getAllByTestId("element");
185+
186+
expect(document.body).toHaveFocus();
187+
188+
userEvent.tab({ focusTrap: div1 });
189+
190+
expect(checkbox1).toHaveFocus();
191+
192+
userEvent.tab({ focusTrap: div1 });
193+
194+
expect(radio1).toHaveFocus();
195+
196+
userEvent.tab({ focusTrap: div1 });
197+
198+
expect(number1).toHaveFocus();
199+
200+
userEvent.tab({ focusTrap: div1 });
201+
202+
// cycle goes back to first element
203+
expect(checkbox1).toHaveFocus();
204+
205+
userEvent.tab({ focusTrap: div2 });
206+
207+
expect(checkbox2).toHaveFocus();
208+
209+
userEvent.tab({ focusTrap: div2 });
210+
211+
expect(radio2).toHaveFocus();
212+
213+
userEvent.tab({ focusTrap: div2 });
214+
215+
expect(number2).toHaveFocus();
216+
217+
userEvent.tab({ focusTrap: div2 });
218+
219+
// cycle goes back to first element
220+
expect(checkbox2).toHaveFocus();
221+
});
159222
});

0 commit comments

Comments
 (0)