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 { 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 useDialogFocus(ref); 63 return ( 64 <ul ref={ref} role="dialog"> 65 <li tabIndex={0}>#1</li> 66 <li tabIndex={0}>#2</li> 67 <li tabIndex={0}>#3</li> 68 </ul> 69 ); 70 }; 71 72 const App = () => { 73 const [hasDialog, setDialog] = useState(false); 74 return ( 75 <main> 76 <button>before</button> 77 <input type="text" name="text" onFocus={() => setDialog(true)} /> 78 {hasDialog && <Dialog />} 79 <button>after</button> 80 </main> 81 ); 82 }; 83 84 mount(<App />); 85 86 cy.get('input').first().as('input').focus(); 87 cy.focused().should('have.property.name', 'text'); 88 89 // Tabbing should skip over the dialog 90 cy.realPress('Tab'); 91 cy.focused().contains('after'); 92 // Tabbing back should skip over the dialog 93 cy.realPress(['Shift', 'Tab']); 94 cy.get('@input').should('have.focus'); 95 // Tabbing back on the owner shouldn't affect the dialog 96 cy.realPress(['Shift', 'Tab']); 97 cy.focused().contains('before'); 98 // It should still know which element the owner was 99 cy.realPress('Tab'); 100 cy.realPress('ArrowDown'); 101 cy.focused().contains('#1'); 102 // From inside the dialog tabbing should skip out of the dialog 103 cy.realPress('Tab'); 104 cy.focused().contains('after'); 105}); 106 107it('supports being attached to an owner element', () => { 108 const Dialog = () => { 109 const ownerRef = useRef<HTMLInputElement>(null); 110 const ref = useRef<HTMLUListElement>(null); 111 112 useDialogFocus(ref, { ownerRef }); 113 114 return ( 115 <main> 116 <input type="text" name="text" ref={ownerRef} /> 117 <ul ref={ref} role="dialog"> 118 <li tabIndex={0}>#1</li> 119 <li tabIndex={0}>#2</li> 120 <li tabIndex={0}>#3</li> 121 </ul> 122 </main> 123 ); 124 }; 125 126 mount(<Dialog />); 127 128 cy.get('input').first().as('input').focus(); 129 cy.focused().should('have.property.name', 'text'); 130 131 // pressing escape on input shouldn't change focus 132 cy.realPress('Escape'); 133 cy.get('@input').should('have.focus'); 134 135 // pressing arrow down should start focusing the menu 136 cy.get('@input').focus(); 137 cy.realPress('ArrowDown'); 138 cy.focused().contains('#1'); 139 cy.realPress('ArrowDown'); 140 cy.focused().contains('#2'); 141 142 // tabbing should skip over the dialog items 143 cy.realPress(['Shift', 'Tab']); 144 cy.get('@input').should('have.focus'); 145 146 // pressing arrow up should start focusing the last item 147 cy.get('@input').focus(); 148 cy.realPress('ArrowUp'); 149 cy.focused().contains('#3'); 150 151 // pressing enter should start focusing the first item 152 cy.get('@input').focus(); 153 cy.realPress('Enter'); 154 cy.focused().contains('#1'); 155 156 // typing regular values should refocus the owner input 157 cy.realType('test'); 158 cy.get('@input') 159 .should('have.focus') 160 .should('have.value', 'test'); 161 162 // pressing escape should refocus input 163 cy.get('li').first().focus(); 164 cy.realPress('Escape'); 165 cy.get('@input').should('have.focus'); 166});