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 { useModalFocus } from '../useModalFocus';
5
6it('keeps the focus loop inside a given modal component', () => {
7 const Modal = () => {
8 const ref = useRef<HTMLDivElement>(null);
9 useModalFocus(ref);
10
11 return (
12 <div aria-modal="true" ref={ref}>
13 <button>Focus 1</button>
14 <button>Focus 2</button>
15 <button>Focus 3</button>
16 </div>
17 );
18 };
19
20 mount(
21 <main>
22 <button className="outside">No Focus</button>
23 <Modal />
24 <button className="outside">No Focus</button>
25 </main>
26 );
27
28 // starts out with first element available
29 cy.focused().contains('Focus 1');
30
31 // cycles through the modal's focusable targets only
32 cy.realPress('Tab');
33 cy.focused().contains('Focus 2');
34 cy.realPress('Tab');
35 cy.focused().contains('Focus 3');
36 cy.realPress('Tab');
37 cy.focused().contains('Focus 1');
38 cy.realPress(['Shift', 'Tab']);
39 cy.focused().contains('Focus 3');
40
41 // prevents focus of outside elements
42 cy.get('.outside').first().focus();
43 cy.focused().contains('Focus 1');
44});
45
46it('allows nested modals where the outer modal becomes inactive', () => {
47 const ModalOne = () => {
48 const ref = useRef<HTMLDivElement>(null);
49 useModalFocus(ref);
50
51 return (
52 <div aria-modal="true" ref={ref}>
53 <button className="inside">Never Focus</button>
54 <ModalTwo />
55 </div>
56 );
57 };
58
59 const ModalTwo = () => {
60 const ref = useRef<HTMLDivElement>(null);
61 useModalFocus(ref);
62
63 return (
64 <div aria-modal="true" ref={ref}>
65 <button className="inside">Focus 1</button>
66 </div>
67 );
68 };
69
70 mount(
71 <main>
72 <button className="outside">Never Focus</button>
73 <ModalOne />
74 </main>
75 );
76
77 // starts out with first element available
78 cy.focused().contains('Focus 1');
79 // keeps focus inside `ModalTwo`
80 cy.realPress('Tab');
81 cy.focused().contains('Focus 1');
82 // prevents focus of `ModalOne`
83 cy.get('.inside').first().focus();
84 cy.focused().contains('Focus 1');
85 // prevents focus of outside elements
86 cy.get('.outside').first().focus();
87 cy.focused().contains('Focus 1');
88});
89
90it('allows modals in semantic order where the preceding modal becomes inactive', () => {
91 const ModalOne = () => {
92 const ref = useRef<HTMLDivElement>(null);
93 useModalFocus(ref);
94
95 return (
96 <div aria-modal="true" ref={ref}>
97 <button className="inside">Never Focus</button>
98 </div>
99 );
100 };
101
102 const ModalTwo = () => {
103 const ref = useRef<HTMLDivElement>(null);
104 useModalFocus(ref);
105
106 return (
107 <div aria-modal="true" ref={ref}>
108 <button className="inside">Focus 1</button>
109 </div>
110 );
111 };
112
113 mount(
114 <main>
115 <button className="outside">Never Focus</button>
116 <ModalOne />
117 <ModalTwo />
118 </main>
119 );
120
121 // starts out with first element available
122 cy.focused().contains('Focus 1');
123 // keeps focus inside `ModalTwo`
124 cy.realPress('Tab');
125 cy.focused().contains('Focus 1');
126 // prevents focus of `ModalOne`
127 cy.get('.inside').first().focus();
128 cy.focused().contains('Focus 1');
129 // prevents focus of outside elements
130 cy.get('.outside').first().focus();
131 cy.focused().contains('Focus 1');
132});
133
134it('switches focus when nested modal closes', () => {
135 const ModalOne = () => {
136 const [nested, setNested] = useState(true);
137 const ref = useRef<HTMLDivElement>(null);
138
139 useModalFocus(ref);
140
141 const onClose = () => {
142 setNested(false);
143 };
144
145 return (
146 <div aria-modal="true" ref={ref}>
147 <button className="inside">Outer Focus</button>
148 {nested && <ModalTwo onClose={onClose} />}
149 </div>
150 );
151 };
152
153 const ModalTwo = ({ onClose }) => {
154 const ref = useRef<HTMLDivElement>(null);
155 useModalFocus(ref);
156
157 return (
158 <div aria-modal="true" ref={ref}>
159 <button className="inside" onClick={onClose}>Inner Focus</button>
160 </div>
161 );
162 };
163
164 mount(
165 <main>
166 <button className="outside">Never Focus</button>
167 <ModalOne />
168 </main>
169 );
170
171 // starts out with first element available
172 cy.focused().contains('Inner Focus');
173 // keeps `InnerModal` focused
174 cy.realPress('Tab');
175 cy.focused().contains('Inner Focus');
176 // switches focus when inner modal closes
177 cy.focused().realClick();
178 cy.focused().contains('Outer Focus');
179});