Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
at main 7.4 kB view raw
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 const ownerRef = useRef<HTMLInputElement>(null); 63 useDialogFocus(ref, { ownerRef }); 64 return ( 65 <div> 66 <input type="text" name="text" ref={ownerRef} /> 67 <ul ref={ref} role="dialog"> 68 <li tabIndex={0}>#1</li> 69 <li tabIndex={0}>#2</li> 70 <li tabIndex={0}>#3</li> 71 </ul> 72 </div> 73 ); 74 }; 75 76 const App = () => { 77 return ( 78 <main> 79 <button>before</button> 80 <Dialog /> 81 <button>after</button> 82 </main> 83 ); 84 }; 85 86 mount(<App />); 87 88 cy.get('input').first().as('input').focus(); 89 cy.focused().should('have.property.name', 'text'); 90 91 // Tabbing should skip over the dialog 92 cy.realPress('Tab'); 93 cy.focused().contains('after'); 94 // Tabbing back should skip over the dialog 95 cy.realPress(['Shift', 'Tab']); 96 cy.get('@input').should('have.focus'); 97 // Tabbing back on the owner shouldn't affect the dialog 98 cy.realPress(['Shift', 'Tab']); 99 cy.focused().contains('before'); 100 // It should still know which element the owner was 101 cy.realPress('Tab'); 102 cy.realPress('ArrowDown'); 103 cy.focused().contains('#1'); 104 // From inside the dialog tabbing should skip out of the dialog 105 cy.realPress('Tab'); 106 cy.focused().contains('after'); 107}); 108 109it('supports being attached to an owner element', () => { 110 const Dialog = () => { 111 const ownerRef = useRef<HTMLInputElement>(null); 112 const ref = useRef<HTMLUListElement>(null); 113 114 useDialogFocus(ref, { ownerRef }); 115 116 return ( 117 <main> 118 <input type="text" name="text" ref={ownerRef} /> 119 <ul ref={ref} role="dialog"> 120 <li tabIndex={0}>#1</li> 121 <li tabIndex={0}>#2</li> 122 <li tabIndex={0}>#3</li> 123 </ul> 124 </main> 125 ); 126 }; 127 128 mount(<Dialog />); 129 130 cy.get('input').first().as('input').focus(); 131 cy.focused().should('have.property.name', 'text'); 132 133 // pressing escape on input shouldn't change focus 134 cy.realPress('Escape'); 135 cy.get('@input').should('have.focus'); 136 137 // pressing arrow down should start focusing the menu 138 cy.get('@input').focus(); 139 cy.realPress('ArrowDown'); 140 cy.focused().contains('#1'); 141 cy.realPress('ArrowDown'); 142 cy.focused().contains('#2'); 143 144 // tabbing should skip over the dialog items 145 cy.realPress(['Shift', 'Tab']); 146 cy.get('@input').should('have.focus'); 147 148 // pressing arrow up should start focusing the last item 149 cy.get('@input').focus(); 150 cy.realPress('ArrowUp'); 151 cy.focused().contains('#3'); 152 153 // pressing enter should start focusing the first item 154 cy.get('@input').focus(); 155 cy.realPress('Enter'); 156 cy.focused().contains('#1'); 157 158 // typing regular values should refocus the owner input 159 cy.realType('test'); 160 cy.get('@input') 161 .should('have.focus') 162 .should('have.value', 'test'); 163}); 164 165it('supports nested dialogs', () => { 166 const InnerDialog = ({ ownerRef }) => { 167 const ref = useRef<HTMLUListElement>(null); 168 useDialogFocus(ref, { ownerRef }); 169 170 return ( 171 <ul ref={ref} role="dialog"> 172 <li tabIndex={0}>Inner #1</li> 173 <li tabIndex={0}>Inner #2</li> 174 </ul> 175 ); 176 }; 177 178 const OuterDialog = () => { 179 const [visible, setVisible] = useState(false); 180 const [nested, setNested] = useState(false); 181 const ref = useRef<HTMLUListElement>(null); 182 const ownerRef = useRef<HTMLInputElement>(null); 183 184 useDialogFocus(ref, { disabled: !visible, ownerRef }); 185 186 return ( 187 <main> 188 <input type="text" name="text" onFocus={() => setVisible(true)} ref={ownerRef} /> 189 {visible && ( 190 <ul ref={ref} role="dialog"> 191 <li tabIndex={0}>Outer #1</li> 192 <li tabIndex={0} onFocus={() => setNested(true)}>Outer #2</li> 193 {nested && <InnerDialog ownerRef={ref} />} 194 </ul> 195 )} 196 <button>after</button> 197 </main> 198 ); 199 }; 200 201 mount(<OuterDialog />); 202 203 cy.get('input').first().as('input').focus(); 204 cy.focused().should('have.property.name', 'text'); 205 206 // select first dialog 207 cy.realPress('ArrowDown'); 208 cy.focused().contains('Outer #1'); 209 cy.realPress('ArrowDown'); 210 cy.focused().contains('Outer #2'); 211 212 // select second dialog 213 cy.realPress('ArrowDown'); 214 cy.focused().contains('Inner #1'); 215 cy.realPress('ArrowDown'); 216 cy.focused().contains('Inner #2'); 217 218 // remains in inner dialog 219 cy.realPress('ArrowDown'); 220 cy.focused().contains('Inner #1'); 221 222 // tabs to last dialog 223 cy.realPress(['Shift', 'Tab']); 224 cy.get('@input').should('have.focus'); 225 226 // tab out of dialogs 227 cy.realPress('Tab'); 228 cy.focused().contains('after'); 229}); 230 231it('allows dialogs in semantic order', () => { 232 const Dialog = ({ name }) => { 233 const ownerRef = useRef<HTMLInputElement>(null); 234 const ref = useRef<HTMLUListElement>(null); 235 236 useDialogFocus(ref, { ownerRef }); 237 238 return ( 239 <div> 240 <input type="text" className={name} ref={ownerRef} tabIndex={-1} /> 241 <ul ref={ref} role="dialog"> 242 <li tabIndex={0}>{name} #1</li> 243 <li tabIndex={0}>{name} #2</li> 244 </ul> 245 </div> 246 ); 247 }; 248 249 mount( 250 <main> 251 <Dialog name="First" /> 252 <Dialog name="Second" /> 253 <button>after</button> 254 </main> 255 ); 256 257 cy.get('.First').first().as('first'); 258 cy.get('.Second').first().as('second'); 259 260 // focus first dialog 261 cy.get('@first').focus(); 262 cy.get('.First').first().as('first').focus(); 263 264 // tabs over both subsequent dialogs 265 cy.realPress('Tab'); 266 cy.focused().contains('after'); 267 268 // given a focused first input, doesn't allow the first dialog to be used 269 cy.get('@first').focus(); 270 cy.realPress('ArrowDown'); 271 cy.get('@first').should('have.focus'); 272 273 // given a focused second input, does allow the second dialog to be used 274 cy.get('@second').focus(); 275 cy.realPress('ArrowDown'); 276 cy.focused().contains('Second #1'); 277});