Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
at main 5.7 kB view raw
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').should('have.focus').should('have.value', 'test'); 137 138 // pressing escape should refocus input 139 cy.get('li').first().focus(); 140 cy.realPress('Escape'); 141 cy.get('@input').should('have.focus'); 142}); 143 144it('behaves nicely for nested menus', () => { 145 const InnerMenu = () => { 146 const ref = useRef<HTMLUListElement>(null); 147 useMenuFocus(ref); 148 149 return ( 150 <ul ref={ref}> 151 <li tabIndex={0}>Inner #1</li> 152 <li tabIndex={0}>Inner #2</li> 153 </ul> 154 ); 155 }; 156 157 const OuterMenu = () => { 158 const ref = useRef<HTMLUListElement>(null); 159 useMenuFocus(ref); 160 161 return ( 162 <ul ref={ref}> 163 <li tabIndex={0}>Outer #1</li> 164 <InnerMenu /> 165 </ul> 166 ); 167 }; 168 169 mount( 170 <main> 171 <button tabIndex={-1}>Start</button> 172 <OuterMenu /> 173 </main> 174 ); 175 176 // Moves into the inner menu as needed 177 cy.get('button').first().focus(); 178 cy.focused().contains('Start'); 179 cy.realPress('Tab'); 180 cy.focused().contains('Outer #1'); 181 cy.realPress('Tab'); 182 cy.focused().contains('Inner #1'); 183 cy.realPress('Tab'); 184 cy.focused().contains('Inner #2'); 185 cy.realPress('ArrowDown'); 186 cy.focused().contains('Inner #1'); 187 cy.realPress('Escape'); 188 cy.focused().contains('Start'); 189 190 // Can move from outer to inner seamlessly 191 cy.get('button').first().focus(); 192 cy.focused().contains('Start'); 193 cy.realPress('Tab'); 194 cy.focused().contains('Outer #1'); 195 cy.realPress('ArrowDown'); 196 cy.focused().contains('Inner #1'); 197 cy.realPress('ArrowDown'); 198 cy.focused().contains('Inner #2'); 199 cy.realPress('ArrowDown'); 200 cy.focused().contains('Inner #1'); 201}); 202 203it('should not focus first menu item if input is not part of the menu', () => { 204 const Menu = () => { 205 const ref = useRef<HTMLUListElement>(null); 206 useMenuFocus(ref); 207 208 return ( 209 <main> 210 <input type="search" name="search" /> 211 <ul ref={ref}> 212 <li tabIndex={0}>#1</li> 213 <li tabIndex={0}>#2</li> 214 <li tabIndex={0}>#3</li> 215 </ul> 216 </main> 217 ); 218 }; 219 220 mount(<Menu />); 221 222 // focus the input 223 cy.get('input').first().as('input').focus(); 224 cy.focused().should('have.property.name', 'search'); 225 226 // pressing enter should not focus the first item 227 cy.get('@input').focus(); 228 cy.realPress('Enter'); 229 cy.get('@input').should('have.focus'); 230});