Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
at main 4.5 kB view raw
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});