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