Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import React, { useState, useRef } from 'react';
2import { mount } from '@cypress/react';
3
4import { useDialogFocus } from '../useDialogFocus';
5
6it('allows dialogs to be navigated without an owner', () => {
7 const Dialog = () => {
8 const ref = useRef<HTMLUListElement>(null);
9 useDialogFocus(ref);
10 return (
11 <ul ref={ref} role="dialog">
12 <li tabIndex={0}>#1</li>
13 <li tabIndex={0}>#2</li>
14 <li tabIndex={0}>#3</li>
15 </ul>
16 );
17 };
18
19 const App = () => {
20 const [hasDialog, setDialog] = useState(false);
21 return (
22 <main>
23 <input type="text" name="text" onFocus={() => setDialog(true)} />
24 {hasDialog && <Dialog />}
25 </main>
26 );
27 };
28
29 mount(<App />);
30
31 cy.get('input').first().as('input').focus();
32 cy.focused().should('have.property.name', 'text');
33
34 // ArrowRight/ArrowLeft shouldn't affect the selection for inputs
35 cy.realPress('ArrowRight');
36 cy.realPress('ArrowLeft');
37 cy.get('@input').should('have.focus');
38
39 // Navigation with arrow keys is normal otherwise
40 cy.realPress('ArrowDown');
41 cy.realPress('ArrowDown');
42 cy.focused().contains('#2');
43 cy.realPress('ArrowRight');
44 cy.focused().contains('#3');
45 cy.realPress('ArrowLeft');
46 cy.focused().contains('#2');
47
48 // permits special key navigation
49 cy.realPress('Home');
50 cy.focused().contains('#1');
51 cy.realPress('End');
52 cy.focused().contains('#3');
53
54 // releases focus to original element on escape
55 cy.realPress('Escape');
56 cy.get('@input').should('have.focus');
57});
58
59it('should not allow the dialog to be tabbable', () => {
60 const Dialog = () => {
61 const ref = useRef<HTMLUListElement>(null);
62 useDialogFocus(ref);
63 return (
64 <ul ref={ref} role="dialog">
65 <li tabIndex={0}>#1</li>
66 <li tabIndex={0}>#2</li>
67 <li tabIndex={0}>#3</li>
68 </ul>
69 );
70 };
71
72 const App = () => {
73 const [hasDialog, setDialog] = useState(false);
74 return (
75 <main>
76 <button>before</button>
77 <input type="text" name="text" onFocus={() => setDialog(true)} />
78 {hasDialog && <Dialog />}
79 <button>after</button>
80 </main>
81 );
82 };
83
84 mount(<App />);
85
86 cy.get('input').first().as('input').focus();
87 cy.focused().should('have.property.name', 'text');
88
89 // Tabbing should skip over the dialog
90 cy.realPress('Tab');
91 cy.focused().contains('after');
92 // Tabbing back should skip over the dialog
93 cy.realPress(['Shift', 'Tab']);
94 cy.get('@input').should('have.focus');
95 // Tabbing back on the owner shouldn't affect the dialog
96 cy.realPress(['Shift', 'Tab']);
97 cy.focused().contains('before');
98 // It should still know which element the owner was
99 cy.realPress('Tab');
100 cy.realPress('ArrowDown');
101 cy.focused().contains('#1');
102 // From inside the dialog tabbing should skip out of the dialog
103 cy.realPress('Tab');
104 cy.focused().contains('after');
105});
106
107it('supports being attached to an owner element', () => {
108 const Dialog = () => {
109 const ownerRef = useRef<HTMLInputElement>(null);
110 const ref = useRef<HTMLUListElement>(null);
111
112 useDialogFocus(ref, { ownerRef });
113
114 return (
115 <main>
116 <input type="text" name="text" ref={ownerRef} />
117 <ul ref={ref} role="dialog">
118 <li tabIndex={0}>#1</li>
119 <li tabIndex={0}>#2</li>
120 <li tabIndex={0}>#3</li>
121 </ul>
122 </main>
123 );
124 };
125
126 mount(<Dialog />);
127
128 cy.get('input').first().as('input').focus();
129 cy.focused().should('have.property.name', 'text');
130
131 // pressing escape on input shouldn't change focus
132 cy.realPress('Escape');
133 cy.get('@input').should('have.focus');
134
135 // pressing arrow down should start focusing the menu
136 cy.get('@input').focus();
137 cy.realPress('ArrowDown');
138 cy.focused().contains('#1');
139 cy.realPress('ArrowDown');
140 cy.focused().contains('#2');
141
142 // tabbing should skip over the dialog items
143 cy.realPress(['Shift', 'Tab']);
144 cy.get('@input').should('have.focus');
145
146 // pressing arrow up should start focusing the last item
147 cy.get('@input').focus();
148 cy.realPress('ArrowUp');
149 cy.focused().contains('#3');
150
151 // pressing enter should start focusing the first item
152 cy.get('@input').focus();
153 cy.realPress('Enter');
154 cy.focused().contains('#1');
155
156 // typing regular values should refocus the owner input
157 cy.realType('test');
158 cy.get('@input')
159 .should('have.focus')
160 .should('have.value', 'test');
161
162 // pressing escape should refocus input
163 cy.get('li').first().focus();
164 cy.realPress('Escape');
165 cy.get('@input').should('have.focus');
166});