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});