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')
137 .should('have.focus')
138 .should('have.value', 'test');
139
140 // pressing escape should refocus input
141 cy.get('li').first().focus();
142 cy.realPress('Escape');
143 cy.get('@input').should('have.focus');
144});
145
146it('behaves nicely for nested menus', () => {
147 const InnerMenu = () => {
148 const ref = useRef<HTMLUListElement>(null);
149 useMenuFocus(ref);
150
151 return (
152 <ul ref={ref}>
153 <li tabIndex={0}>Inner #1</li>
154 <li tabIndex={0}>Inner #2</li>
155 </ul>
156 );
157 };
158
159 const OuterMenu = () => {
160 const ref = useRef<HTMLUListElement>(null);
161 useMenuFocus(ref);
162
163 return (
164 <ul ref={ref}>
165 <li tabIndex={0}>Outer #1</li>
166 <InnerMenu />
167 </ul>
168 );
169 };
170
171 mount(
172 <main>
173 <button tabIndex={-1}>Start</button>
174 <OuterMenu />
175 </main>
176 );
177
178 // Moves into the inner menu as needed
179 cy.get('button').first().focus();
180 cy.focused().contains('Start');
181 cy.realPress('Tab');
182 cy.focused().contains('Outer #1');
183 cy.realPress('Tab');
184 cy.focused().contains('Inner #1');
185 cy.realPress('Tab');
186 cy.focused().contains('Inner #2');
187 cy.realPress('ArrowDown');
188 cy.focused().contains('Inner #1');
189 cy.realPress('Escape');
190 cy.focused().contains('Start');
191
192 // Can move from outer to inner seamlessly
193 cy.get('button').first().focus();
194 cy.focused().contains('Start');
195 cy.realPress('Tab');
196 cy.focused().contains('Outer #1');
197 cy.realPress('ArrowDown');
198 cy.focused().contains('Inner #1');
199 cy.realPress('ArrowDown');
200 cy.focused().contains('Inner #2');
201 cy.realPress('ArrowDown');
202 cy.focused().contains('Inner #1');
203});