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 useMenuFocus tests

+101 -5
+87
src/__tests__/useMenuFocus.test.tsx
··· 1 + import React, { useState, useRef } from 'react'; 2 + import { mount } from '@cypress/react'; 3 + 4 + import { useMenuFocus } from '../useMenuFocus'; 5 + 6 + it('allows menus to be navigated via dialog-like controls', () => { 7 + const Menu = () => { 8 + const ref = useRef<HTMLUListElement>(null); 9 + useMenuFocus(ref); 10 + 11 + return ( 12 + <ul ref={ref}> 13 + <li tabIndex={0}>#1</li> 14 + <li tabIndex={0}>#2</li> 15 + <li tabIndex={0}>#3</li> 16 + </ul> 17 + ); 18 + }; 19 + 20 + mount( 21 + <main> 22 + <button tabIndex={-1}>Start</button> 23 + <Menu /> 24 + </main> 25 + ); 26 + 27 + // Focus button first 28 + cy.get('button').first().focus(); 29 + cy.focused().contains('Start'); 30 + 31 + // permits regular tab order 32 + cy.realPress('Tab'); 33 + cy.realPress('Tab'); 34 + cy.focused().contains('#2'); 35 + 36 + // permits arrow-key tabbing 37 + cy.realPress('ArrowDown'); 38 + cy.focused().contains('#3'); 39 + cy.realPress('ArrowUp'); 40 + cy.focused().contains('#2'); 41 + cy.realPress('ArrowRight'); 42 + cy.focused().contains('#3'); 43 + cy.realPress('ArrowLeft'); 44 + cy.focused().contains('#2'); 45 + 46 + // permits special key navigation 47 + cy.realPress('Home'); 48 + cy.focused().contains('#1'); 49 + cy.realPress('End'); 50 + cy.focused().contains('#3'); 51 + 52 + // releases focus to original element on escape 53 + cy.realPress('Escape'); 54 + cy.focused().contains('Start'); 55 + }); 56 + 57 + it('prevents Left/Right arrow keys from overriding input actions', () => { 58 + const Menu = () => { 59 + const ref = useRef<HTMLDivElement>(null); 60 + useMenuFocus(ref); 61 + 62 + return ( 63 + <div ref={ref}> 64 + <input type="text" name="text" /> 65 + <button>Focus</button> 66 + </div> 67 + ); 68 + }; 69 + 70 + mount(<Menu />); 71 + 72 + // focus the input 73 + cy.get('input').first().as('input').focus(); 74 + cy.focused().should('have.property.name', 'text'); 75 + 76 + // arrow Left/Right should not change focus 77 + cy.realPress('ArrowRight'); 78 + cy.get('@input').should('be.focused'); 79 + cy.realPress('ArrowLeft'); 80 + cy.get('@input').should('be.focused'); 81 + 82 + // arrow Down/Up should change focus 83 + cy.realPress('ArrowDown'); 84 + cy.get('@input').should('not.be.focused'); 85 + cy.realPress('ArrowUp'); 86 + cy.get('@input').should('be.focused'); 87 + });
+6 -1
src/useDialogFocus.ts
··· 120 120 focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1; 121 121 willReceiveFocus = true; 122 122 focusTargets[nextIndex].focus(); 123 - } else if (selection && event.code === 'Escape') { 123 + } else if ( 124 + owner && 125 + !contains(ref.current, owner) && 126 + event.code === 'Escape' 127 + ) { 124 128 // Restore selection if escape is pressed 125 129 event.preventDefault(); 126 130 willReceiveFocus = false; ··· 128 132 } else if ( 129 133 owner && 130 134 isInputElement(owner) && 135 + !contains(ref.current, owner) && 131 136 contains(owner, active) && 132 137 event.code === 'Enter' 133 138 ) {
+8 -4
src/useMenuFocus.ts
··· 58 58 const focusTargets = getFocusTargets(ref.current); 59 59 if ( 60 60 !focusTargets.length || 61 - !contains(ref.current, active) || 62 - !contains(owner, active) 61 + (!contains(ref.current, active) && !contains(owner, active)) 63 62 ) { 64 63 // Do nothing if container doesn't contain focus or not targets are available 65 64 return; ··· 106 105 selection = snapshotSelection(owner); 107 106 newTarget.focus(); 108 107 } 109 - } else if (owner && !contains(owner, active) && event.code === 'Escape') { 108 + } else if ( 109 + owner && 110 + !contains(ref.current, owner) && 111 + !contains(owner, active) && 112 + event.code === 'Escape' 113 + ) { 110 114 // Restore selection if escape is pressed 111 115 event.preventDefault(); 112 116 restoreSelection(selection); 113 117 } else if ( 114 118 owner && 115 - active !== owner && 116 119 isInputElement(owner) && 120 + !contains(owner, active) && 117 121 /^(?:Key|Digit)/.test(event.code) 118 122 ) { 119 123 // Restore selection if a key is pressed on input