Skip to content

Commit 142c0cb

Browse files
authored
test: add roving focus group behavior tests (#1517)
1 parent cf380bf commit 142c0cb

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import * as axe from 'axe-core';
5+
6+
import RovingFocusGroup from '../index';
7+
import { ACCESSIBILITY_TEST_TAGS } from '~/setupTests';
8+
9+
describe('RovingFocusGroup behavior', () => {
10+
test('arrow keys move focus and Tab leaves group', async () => {
11+
const user = userEvent.setup();
12+
render(
13+
<>
14+
<RovingFocusGroup.Root orientation="horizontal">
15+
<RovingFocusGroup.Group>
16+
<RovingFocusGroup.Item><button>Item 1</button></RovingFocusGroup.Item>
17+
<RovingFocusGroup.Item><button>Item 2</button></RovingFocusGroup.Item>
18+
<RovingFocusGroup.Item><button>Item 3</button></RovingFocusGroup.Item>
19+
</RovingFocusGroup.Group>
20+
</RovingFocusGroup.Root>
21+
<button data-testid="outside">Outside</button>
22+
</>
23+
);
24+
const item1 = screen.getByText('Item 1');
25+
const item2 = screen.getByText('Item 2');
26+
const outside = screen.getByTestId('outside');
27+
28+
await user.tab();
29+
expect(item1).toHaveFocus();
30+
31+
await user.keyboard('{ArrowRight}');
32+
expect(item2).toHaveFocus();
33+
34+
await user.tab();
35+
expect(outside).toHaveFocus();
36+
});
37+
38+
test('wraps focus when reaching ends', async () => {
39+
const user = userEvent.setup();
40+
render(
41+
<RovingFocusGroup.Root orientation="horizontal" loop>
42+
<RovingFocusGroup.Group>
43+
<RovingFocusGroup.Item><button>Item 1</button></RovingFocusGroup.Item>
44+
<RovingFocusGroup.Item><button>Item 2</button></RovingFocusGroup.Item>
45+
<RovingFocusGroup.Item><button>Item 3</button></RovingFocusGroup.Item>
46+
</RovingFocusGroup.Group>
47+
</RovingFocusGroup.Root>
48+
);
49+
const item1 = screen.getByText('Item 1');
50+
const item3 = screen.getByText('Item 3');
51+
52+
await user.tab();
53+
expect(item1).toHaveFocus();
54+
55+
await user.keyboard('{ArrowLeft}');
56+
expect(item3).toHaveFocus();
57+
58+
await user.keyboard('{ArrowRight}');
59+
expect(item1).toHaveFocus();
60+
});
61+
62+
test('skips disabled items during navigation', async () => {
63+
const user = userEvent.setup();
64+
render(
65+
<RovingFocusGroup.Root orientation="horizontal" loop>
66+
<RovingFocusGroup.Group>
67+
<RovingFocusGroup.Item><button>Item 1</button></RovingFocusGroup.Item>
68+
<RovingFocusGroup.Item><button disabled>Item 2</button></RovingFocusGroup.Item>
69+
<RovingFocusGroup.Item><button>Item 3</button></RovingFocusGroup.Item>
70+
</RovingFocusGroup.Group>
71+
</RovingFocusGroup.Root>
72+
);
73+
const item1 = screen.getByText('Item 1');
74+
const item3 = screen.getByText('Item 3');
75+
76+
await user.tab();
77+
expect(item1).toHaveFocus();
78+
79+
await user.keyboard('{ArrowRight}');
80+
expect(item3).toHaveFocus();
81+
82+
await user.keyboard('{ArrowLeft}');
83+
expect(item1).toHaveFocus();
84+
});
85+
86+
test('respects RTL direction for horizontal navigation', async () => {
87+
const user = userEvent.setup();
88+
render(
89+
<RovingFocusGroup.Root orientation="horizontal" dir="rtl">
90+
<RovingFocusGroup.Group>
91+
<RovingFocusGroup.Item><button>Item 1</button></RovingFocusGroup.Item>
92+
<RovingFocusGroup.Item><button>Item 2</button></RovingFocusGroup.Item>
93+
<RovingFocusGroup.Item><button>Item 3</button></RovingFocusGroup.Item>
94+
</RovingFocusGroup.Group>
95+
</RovingFocusGroup.Root>
96+
);
97+
const item1 = screen.getByText('Item 1');
98+
const item2 = screen.getByText('Item 2');
99+
100+
await user.tab();
101+
expect(item1).toHaveFocus();
102+
103+
await user.keyboard('{ArrowLeft}');
104+
expect(item2).toHaveFocus();
105+
106+
await user.keyboard('{ArrowRight}');
107+
expect(item1).toHaveFocus();
108+
});
109+
110+
test('supports dynamic item add and remove', async () => {
111+
const user = userEvent.setup();
112+
const DynamicGroup = () => {
113+
const [items, setItems] = React.useState(['A', 'B']);
114+
return (
115+
<>
116+
<button onClick={() => setItems([...items, `Item${items.length + 1}`])} data-testid="add">Add</button>
117+
<button onClick={() => setItems(items.slice(0, -1))} data-testid="remove">Remove</button>
118+
<RovingFocusGroup.Root orientation="horizontal" loop mode="tree">
119+
<RovingFocusGroup.Group>
120+
{items.map((label) => (
121+
<RovingFocusGroup.Item key={label}>
122+
<button>{label}</button>
123+
</RovingFocusGroup.Item>
124+
))}
125+
</RovingFocusGroup.Group>
126+
</RovingFocusGroup.Root>
127+
</>
128+
);
129+
};
130+
render(<DynamicGroup />);
131+
132+
const itemA = screen.getByText('A');
133+
const addBtn = screen.getByTestId('add');
134+
const removeBtn = screen.getByTestId('remove');
135+
136+
await user.tab();
137+
await user.tab();
138+
await user.tab();
139+
expect(itemA).toHaveFocus();
140+
141+
await user.click(addBtn);
142+
const item3 = screen.getByText('Item3');
143+
itemA.focus();
144+
await user.keyboard('{ArrowRight}');
145+
await user.keyboard('{ArrowRight}');
146+
expect(item3).toHaveFocus();
147+
148+
await user.click(removeBtn);
149+
const itemB = screen.getByText('B');
150+
itemB.focus();
151+
await user.keyboard('{ArrowRight}');
152+
expect(itemA).toHaveFocus();
153+
});
154+
155+
test('handles multiple groups independently', async () => {
156+
const user = userEvent.setup();
157+
render(
158+
<RovingFocusGroup.Root orientation="horizontal">
159+
<RovingFocusGroup.Group>
160+
<RovingFocusGroup.Item><button>G1-1</button></RovingFocusGroup.Item>
161+
<RovingFocusGroup.Item><button>G1-2</button></RovingFocusGroup.Item>
162+
</RovingFocusGroup.Group>
163+
<RovingFocusGroup.Group>
164+
<RovingFocusGroup.Item><button>G2-1</button></RovingFocusGroup.Item>
165+
<RovingFocusGroup.Item><button>G2-2</button></RovingFocusGroup.Item>
166+
</RovingFocusGroup.Group>
167+
</RovingFocusGroup.Root>
168+
);
169+
const g1Item1 = screen.getByText('G1-1');
170+
const g1Item2 = screen.getByText('G1-2');
171+
const g2Item1 = screen.getByText('G2-1');
172+
173+
await user.tab();
174+
expect(g1Item1).toHaveFocus();
175+
176+
await user.keyboard('{ArrowRight}');
177+
expect(g1Item2).toHaveFocus();
178+
179+
await user.keyboard('{ArrowRight}');
180+
expect(g1Item1).toHaveFocus();
181+
182+
await user.tab();
183+
expect(g2Item1).toHaveFocus();
184+
});
185+
186+
test('has no accessibility violations', (done) => {
187+
const { container } = render(
188+
<RovingFocusGroup.Root orientation="horizontal">
189+
<RovingFocusGroup.Group>
190+
<RovingFocusGroup.Item><button>Item 1</button></RovingFocusGroup.Item>
191+
<RovingFocusGroup.Item><button>Item 2</button></RovingFocusGroup.Item>
192+
</RovingFocusGroup.Group>
193+
</RovingFocusGroup.Root>
194+
);
195+
196+
axe.run(container, { runOnly: { type: 'tag', values: ACCESSIBILITY_TEST_TAGS } }).then((results) => {
197+
expect(results.incomplete.length).toBe(0);
198+
expect(results.violations.length).toBe(0);
199+
done();
200+
});
201+
});
202+
});

0 commit comments

Comments
 (0)