Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.

Add test for nested or duplicated useDialogFocus

+192
+192
src/__tests__/useDialogFocus.test.tsx
··· 164 164 cy.realPress('Escape'); 165 165 cy.get('@input').should('have.focus'); 166 166 }); 167 + 168 + it('supports nested dialogs', () => { 169 + const InnerDialog = () => { 170 + const ref = useRef<HTMLUListElement>(null); 171 + useDialogFocus(ref); 172 + 173 + return ( 174 + <ul ref={ref} role="dialog"> 175 + <li tabIndex={0}>Inner #1</li> 176 + <li tabIndex={0}>Inner #2</li> 177 + </ul> 178 + ); 179 + }; 180 + 181 + const OuterDialog = () => { 182 + const [visible, setVisible] = useState(false); 183 + const [nested, setNested] = useState(false); 184 + const ref = useRef<HTMLUListElement>(null); 185 + 186 + useDialogFocus(ref, { disabled: !visible }); 187 + 188 + return ( 189 + <main> 190 + <input type="text" name="text" onFocus={() => setVisible(true)} /> 191 + {visible && ( 192 + <ul ref={ref} role="dialog"> 193 + <li tabIndex={0}>Outer #1</li> 194 + <li tabIndex={0} onFocus={() => setNested(true)}>Outer #2</li> 195 + {nested && <InnerDialog />} 196 + </ul> 197 + )} 198 + <button>after</button> 199 + </main> 200 + ); 201 + }; 202 + 203 + mount(<OuterDialog />); 204 + 205 + cy.get('input').first().as('input').focus(); 206 + cy.focused().should('have.property.name', 'text'); 207 + 208 + // select first dialog 209 + cy.realPress('ArrowDown'); 210 + cy.focused().contains('Outer #1'); 211 + cy.realPress('ArrowDown'); 212 + cy.focused().contains('Outer #2'); 213 + 214 + // select second dialog 215 + cy.realPress('ArrowDown'); 216 + cy.focused().contains('Inner #1'); 217 + cy.realPress('ArrowDown'); 218 + cy.focused().contains('Inner #2'); 219 + 220 + // remains in inner dialog 221 + cy.realPress('ArrowDown'); 222 + cy.focused().contains('Inner #1'); 223 + 224 + // tabs to last dialog 225 + cy.realPress(['Shift', 'Tab']); 226 + cy.focused().contains('Outer #2'); 227 + 228 + // arrows bring us back to the inner dialog 229 + cy.realPress('ArrowUp'); 230 + cy.focused().contains('Inner #2'); 231 + 232 + // tab out of dialogs 233 + cy.realPress('Tab'); 234 + cy.focused().contains('after'); 235 + // we can't reenter the dialogs 236 + cy.realPress(['Shift', 'Tab']); 237 + cy.get('@input').should('have.focus'); 238 + }); 239 + 240 + it('supports nested dialogs', () => { 241 + const InnerDialog = () => { 242 + const ref = useRef<HTMLUListElement>(null); 243 + useDialogFocus(ref); 244 + 245 + return ( 246 + <ul ref={ref} role="dialog"> 247 + <li tabIndex={0}>Inner #1</li> 248 + <li tabIndex={0}>Inner #2</li> 249 + </ul> 250 + ); 251 + }; 252 + 253 + const OuterDialog = () => { 254 + const [visible, setVisible] = useState(false); 255 + const [nested, setNested] = useState(false); 256 + const ref = useRef<HTMLUListElement>(null); 257 + 258 + useDialogFocus(ref, { disabled: !visible }); 259 + 260 + return ( 261 + <main> 262 + <input type="text" name="text" onFocus={() => setVisible(true)} /> 263 + {visible && ( 264 + <ul ref={ref} role="dialog"> 265 + <li tabIndex={0}>Outer #1</li> 266 + <li tabIndex={0} onFocus={() => setNested(true)}>Outer #2</li> 267 + {nested && <InnerDialog />} 268 + </ul> 269 + )} 270 + <button>after</button> 271 + </main> 272 + ); 273 + }; 274 + 275 + mount(<OuterDialog />); 276 + 277 + cy.get('input').first().as('input').focus(); 278 + cy.focused().should('have.property.name', 'text'); 279 + 280 + // select first dialog 281 + cy.realPress('ArrowDown'); 282 + cy.focused().contains('Outer #1'); 283 + cy.realPress('ArrowDown'); 284 + cy.focused().contains('Outer #2'); 285 + 286 + // select second dialog 287 + cy.realPress('ArrowDown'); 288 + cy.focused().contains('Inner #1'); 289 + cy.realPress('ArrowDown'); 290 + cy.focused().contains('Inner #2'); 291 + 292 + // remains in inner dialog 293 + cy.realPress('ArrowDown'); 294 + cy.focused().contains('Inner #1'); 295 + 296 + // tabs to last dialog 297 + cy.realPress(['Shift', 'Tab']); 298 + cy.focused().contains('Outer #2'); 299 + 300 + // arrows bring us back to the inner dialog 301 + cy.realPress('ArrowUp'); 302 + cy.focused().contains('Inner #2'); 303 + 304 + // tab out of dialogs 305 + cy.realPress('Tab'); 306 + cy.focused().contains('after'); 307 + // we can't reenter the dialogs 308 + cy.realPress(['Shift', 'Tab']); 309 + cy.get('@input').should('have.focus'); 310 + }); 311 + 312 + it('allows dialogs in semantic order', () => { 313 + const Dialog = ({ name }) => { 314 + const ownerRef = useRef<HTMLInputElement>(null); 315 + const ref = useRef<HTMLUListElement>(null); 316 + 317 + useDialogFocus(ref, { ownerRef }); 318 + 319 + return ( 320 + <div> 321 + <input type="text" className={name} ref={ownerRef} tabIndex={-1} /> 322 + <ul ref={ref} role="dialog"> 323 + <li tabIndex={0}>{name} #1</li> 324 + <li tabIndex={0}>{name} #2</li> 325 + </ul> 326 + </div> 327 + ); 328 + }; 329 + 330 + mount( 331 + <main> 332 + <Dialog name="First" /> 333 + <Dialog name="Second" /> 334 + <button>after</button> 335 + </main> 336 + ); 337 + 338 + cy.get('.First').first().as('first'); 339 + cy.get('.Second').first().as('second'); 340 + 341 + // focus first dialog 342 + cy.get('@first').focus(); 343 + cy.get('.First').first().as('first').focus(); 344 + 345 + // tabs over both subsequent dialogs 346 + cy.realPress('Tab'); 347 + cy.focused().contains('after'); 348 + 349 + // given a focused first input, doesn't allow the first dialog to be used 350 + cy.get('@first').focus(); 351 + cy.realPress('ArrowDown'); 352 + cy.get('@first').should('have.focus'); 353 + 354 + // given a focused second input, does allow the second dialog to be used 355 + cy.get('@second').focus(); 356 + cy.realPress('ArrowDown'); 357 + cy.focused().contains('Second #1'); 358 + });