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

Configure Feed

Select the types of activity you want to include in your feed.

Add initial useDialogFocus tests

+170 -11
+166
src/__tests__/useDialogFocus.test.tsx
··· 1 + import React, { useState, useRef } from 'react'; 2 + import { mount } from '@cypress/react'; 3 + 4 + import { useDialogFocus } from '../useDialogFocus'; 5 + 6 + it('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 + 59 + it('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 + 107 + it('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 + });
-7
src/__tests__/useDialogFocus.tsx
··· 1 - import React, { useRef } from 'react'; 2 - import { mount } from '@cypress/react'; 3 - 4 - import { useDialogFocus } from '../useDialogFocus'; 5 - 6 - it('allows dialogs to be navigated', () => { 7 - });
+1 -1
src/__tests__/useMenuFocus.test.tsx
··· 125 125 cy.realPress('ArrowUp'); 126 126 cy.focused().contains('#3'); 127 127 128 - // pressing arrow up should start focusing the last item 128 + // pressing enter should start focusing the first item 129 129 cy.get('@input').focus(); 130 130 cy.realPress('Enter'); 131 131 cy.focused().contains('#1');
+2 -2
src/useDialogFocus.ts
··· 50 50 51 51 if ( 52 52 willReceiveFocus || 53 - (hasPriority && owner && event.target === owner) 53 + (hasPriority && owner && contains(event.target, owner)) 54 54 ) { 55 55 if (!contains(ref.current, active)) 56 56 selection = snapshotSelection(owner); ··· 160 160 } 161 161 } else if ( 162 162 owner && 163 - contains(owner, active) && 164 163 isInputElement(owner) && 164 + !contains(owner, active) && 165 165 /^(?:Key|Digit)/.test(event.code) 166 166 ) { 167 167 // Restore selection if a key is pressed on input
+1 -1
src/utils/focus.ts
··· 76 76 while ( 77 77 (next = reverse ? next.previousElementSibling : next.nextElementSibling) 78 78 ) { 79 - if (isVisible(next) && !!node.matches(focusableSelectors)) { 79 + if (isVisible(next) && !!next.matches(focusableSelectors)) { 80 80 return next as HTMLElement; 81 81 } else if (hasFocusTargets(next)) { 82 82 const targets = getFocusTargets(next);