Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import React, { useRef } from 'react';
2import { mount } from '@cypress/react';
3
4import { useMenuFocus } from '../useMenuFocus';
5
6it('allows menus to be navigated via dialog-like controls', () => {
7 const Menu = () => {
8 const ref = useRef<HTMLUListElement>(null);
9 useMenuFocus(ref);
10
11 return (
12 <ul ref={ref}>
13 <li tabIndex={0}>#1</li>
14 <li tabIndex={0}>#2</li>
15 <li tabIndex={0}>#3</li>
16 </ul>
17 );
18 };
19
20 mount(
21 <main>
22 <button tabIndex={-1}>Start</button>
23 <Menu />
24 </main>
25 );
26
27 // Focus button first
28 cy.get('button').first().focus();
29 cy.focused().contains('Start');
30
31 // permits regular tab order
32 cy.realPress('Tab');
33 cy.realPress('Tab');
34 cy.focused().contains('#2');
35
36 // permits arrow-key tabbing
37 cy.realPress('ArrowDown');
38 cy.focused().contains('#3');
39 cy.realPress('ArrowUp');
40 cy.focused().contains('#2');
41 cy.realPress('ArrowRight');
42 cy.focused().contains('#3');
43 cy.realPress('ArrowLeft');
44 cy.focused().contains('#2');
45
46 // permits special key navigation
47 cy.realPress('Home');
48 cy.focused().contains('#1');
49 cy.realPress('End');
50 cy.focused().contains('#3');
51
52 // releases focus to original element on escape
53 cy.realPress('Escape');
54 cy.focused().contains('Start');
55});
56
57it('prevents Left/Right arrow keys from overriding input actions', () => {
58 const Menu = () => {
59 const ref = useRef<HTMLDivElement>(null);
60 useMenuFocus(ref);
61
62 return (
63 <div ref={ref}>
64 <input type="text" name="text" />
65 <button>Focus</button>
66 </div>
67 );
68 };
69
70 mount(<Menu />);
71
72 // focus the input
73 cy.get('input').first().as('input').focus();
74 cy.focused().should('have.property.name', 'text');
75
76 // arrow Left/Right should not change focus
77 cy.realPress('ArrowRight');
78 cy.get('@input').should('be.focused');
79 cy.realPress('ArrowLeft');
80 cy.get('@input').should('be.focused');
81
82 // arrow Down/Up should change focus
83 cy.realPress('ArrowDown');
84 cy.get('@input').should('not.be.focused');
85 cy.realPress('ArrowUp');
86 cy.get('@input').should('be.focused');
87});
88
89it('supports being attached to an owner element', () => {
90 const Menu = () => {
91 const ownerRef = useRef<HTMLInputElement>(null);
92 const ref = useRef<HTMLUListElement>(null);
93
94 useMenuFocus(ref, { ownerRef });
95
96 return (
97 <main>
98 <input type="search" name="search" ref={ownerRef} />
99 <ul ref={ref}>
100 <li tabIndex={0}>#1</li>
101 <li tabIndex={0}>#2</li>
102 <li tabIndex={0}>#3</li>
103 </ul>
104 </main>
105 );
106 };
107
108 mount(<Menu />);
109
110 // focus the input
111 cy.get('input').first().as('input').focus();
112 cy.focused().should('have.property.name', 'search');
113
114 // pressing escape on input shouldn't change focus
115 cy.realPress('Escape');
116 cy.get('@input').should('have.focus');
117
118 // pressing arrow down should start focusing the menu
119 cy.get('@input').focus();
120 cy.realPress('ArrowDown');
121 cy.focused().contains('#1');
122
123 // pressing arrow up should start focusing the last item
124 cy.get('@input').focus();
125 cy.realPress('ArrowUp');
126 cy.focused().contains('#3');
127
128 // pressing enter should start focusing the first item
129 cy.get('@input').focus();
130 cy.realPress('Enter');
131 cy.focused().contains('#1');
132
133 // typing regular values should refocus the owner input
134 cy.get('li').first().focus();
135 cy.realType('test');
136 cy.get('@input').should('have.focus').should('have.value', 'test');
137
138 // pressing escape should refocus input
139 cy.get('li').first().focus();
140 cy.realPress('Escape');
141 cy.get('@input').should('have.focus');
142});
143
144it('behaves nicely for nested menus', () => {
145 const InnerMenu = () => {
146 const ref = useRef<HTMLUListElement>(null);
147 useMenuFocus(ref);
148
149 return (
150 <ul ref={ref}>
151 <li tabIndex={0}>Inner #1</li>
152 <li tabIndex={0}>Inner #2</li>
153 </ul>
154 );
155 };
156
157 const OuterMenu = () => {
158 const ref = useRef<HTMLUListElement>(null);
159 useMenuFocus(ref);
160
161 return (
162 <ul ref={ref}>
163 <li tabIndex={0}>Outer #1</li>
164 <InnerMenu />
165 </ul>
166 );
167 };
168
169 mount(
170 <main>
171 <button tabIndex={-1}>Start</button>
172 <OuterMenu />
173 </main>
174 );
175
176 // Moves into the inner menu as needed
177 cy.get('button').first().focus();
178 cy.focused().contains('Start');
179 cy.realPress('Tab');
180 cy.focused().contains('Outer #1');
181 cy.realPress('Tab');
182 cy.focused().contains('Inner #1');
183 cy.realPress('Tab');
184 cy.focused().contains('Inner #2');
185 cy.realPress('ArrowDown');
186 cy.focused().contains('Inner #1');
187 cy.realPress('Escape');
188 cy.focused().contains('Start');
189
190 // Can move from outer to inner seamlessly
191 cy.get('button').first().focus();
192 cy.focused().contains('Start');
193 cy.realPress('Tab');
194 cy.focused().contains('Outer #1');
195 cy.realPress('ArrowDown');
196 cy.focused().contains('Inner #1');
197 cy.realPress('ArrowDown');
198 cy.focused().contains('Inner #2');
199 cy.realPress('ArrowDown');
200 cy.focused().contains('Inner #1');
201});
202
203it('should not focus first menu item if input is not part of the menu', () => {
204 const Menu = () => {
205 const ref = useRef<HTMLUListElement>(null);
206 useMenuFocus(ref);
207
208 return (
209 <main>
210 <input type="search" name="search" />
211 <ul ref={ref}>
212 <li tabIndex={0}>#1</li>
213 <li tabIndex={0}>#2</li>
214 <li tabIndex={0}>#3</li>
215 </ul>
216 </main>
217 );
218 };
219
220 mount(<Menu />);
221
222 // focus the input
223 cy.get('input').first().as('input').focus();
224 cy.focused().should('have.property.name', 'search');
225
226 // pressing enter should not focus the first item
227 cy.get('@input').focus();
228 cy.realPress('Enter');
229 cy.get('@input').should('have.focus');
230});