Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import React, { useRef } from 'react'; 2import { mount } from '@cypress/react'; 3 4import { useMenuFocus } from '../useMenuFocus'; 5 6it('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 57it('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}); 88 89it('supports being attached to an owner element', () => { 90 const Menu = () => { 91 const ownerRef = useRef<HTMLInputElement>(null); 92 const ref = useRef<HTMLUListElement>(null); 93 94 useMenuFocus(ref, { ownerRef }); 95 96 return ( 97 <main> 98 <input type="search" name="search" ref={ownerRef} /> 99 <ul ref={ref}> 100 <li tabIndex={0}>#1</li> 101 <li tabIndex={0}>#2</li> 102 <li tabIndex={0}>#3</li> 103 </ul> 104 </main> 105 ); 106 }; 107 108 mount(<Menu />); 109 110 // focus the input 111 cy.get('input').first().as('input').focus(); 112 cy.focused().should('have.property.name', 'search'); 113 114 // pressing escape on input shouldn't change focus 115 cy.realPress('Escape'); 116 cy.get('@input').should('have.focus'); 117 118 // pressing arrow down should start focusing the menu 119 cy.get('@input').focus(); 120 cy.realPress('ArrowDown'); 121 cy.focused().contains('#1'); 122 123 // pressing arrow up should start focusing the last item 124 cy.get('@input').focus(); 125 cy.realPress('ArrowUp'); 126 cy.focused().contains('#3'); 127 128 // pressing enter should start focusing the first item 129 cy.get('@input').focus(); 130 cy.realPress('Enter'); 131 cy.focused().contains('#1'); 132 133 // typing regular values should refocus the owner input 134 cy.get('li').first().focus(); 135 cy.realType('test'); 136 cy.get('@input') 137 .should('have.focus') 138 .should('have.value', 'test'); 139 140 // pressing escape should refocus input 141 cy.get('li').first().focus(); 142 cy.realPress('Escape'); 143 cy.get('@input').should('have.focus'); 144}); 145 146it('behaves nicely for nested menus', () => { 147 const InnerMenu = () => { 148 const ref = useRef<HTMLUListElement>(null); 149 useMenuFocus(ref); 150 151 return ( 152 <ul ref={ref}> 153 <li tabIndex={0}>Inner #1</li> 154 <li tabIndex={0}>Inner #2</li> 155 </ul> 156 ); 157 }; 158 159 const OuterMenu = () => { 160 const ref = useRef<HTMLUListElement>(null); 161 useMenuFocus(ref); 162 163 return ( 164 <ul ref={ref}> 165 <li tabIndex={0}>Outer #1</li> 166 <InnerMenu /> 167 </ul> 168 ); 169 }; 170 171 mount( 172 <main> 173 <button tabIndex={-1}>Start</button> 174 <OuterMenu /> 175 </main> 176 ); 177 178 // Moves into the inner menu as needed 179 cy.get('button').first().focus(); 180 cy.focused().contains('Start'); 181 cy.realPress('Tab'); 182 cy.focused().contains('Outer #1'); 183 cy.realPress('Tab'); 184 cy.focused().contains('Inner #1'); 185 cy.realPress('Tab'); 186 cy.focused().contains('Inner #2'); 187 cy.realPress('ArrowDown'); 188 cy.focused().contains('Inner #1'); 189 cy.realPress('Escape'); 190 cy.focused().contains('Start'); 191 192 // Can move from outer to inner seamlessly 193 cy.get('button').first().focus(); 194 cy.focused().contains('Start'); 195 cy.realPress('Tab'); 196 cy.focused().contains('Outer #1'); 197 cy.realPress('ArrowDown'); 198 cy.focused().contains('Inner #1'); 199 cy.realPress('ArrowDown'); 200 cy.focused().contains('Inner #2'); 201 cy.realPress('ArrowDown'); 202 cy.focused().contains('Inner #1'); 203});