@@ -16,23 +16,28 @@ describe('Dropdown Component', () => {
1616
1717 it ( 'toggles dropdown content on button click' , ( ) => {
1818 const { container : { children : [ div ] } , getByRole } = render (
19- < Dropdown label = 'go' > < div > Child 1</ div > < div > Child 2</ div > </ Dropdown >
19+ < Dropdown label = 'go' >
20+ < div > Child 1</ div >
21+ < div > Child 2</ div >
22+ </ Dropdown >
2023 )
21-
2224 const dropdownButton = getByRole ( 'button' )
23- fireEvent . click ( dropdownButton )
2425
25- // Check if dropdown content appears
26+ // open menu with click
27+ fireEvent . click ( dropdownButton )
2628 expect ( div ?. children [ 0 ] ?. getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
2729
28- // Click again to close
30+ // click again to close
2931 fireEvent . click ( dropdownButton )
3032 expect ( div ?. children [ 0 ] ?. getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
3133 } )
3234
3335 it ( 'closes dropdown when clicking outside' , ( ) => {
3436 const { container : { children : [ div ] } , getByRole } = render (
35- < Dropdown > < div > Child 1</ div > < div > Child 2</ div > </ Dropdown >
37+ < Dropdown >
38+ < div > Child 1</ div >
39+ < div > Child 2</ div >
40+ </ Dropdown >
3641 )
3742
3843 const dropdownButton = getByRole ( 'button' )
@@ -46,7 +51,10 @@ describe('Dropdown Component', () => {
4651
4752 it ( 'does not close dropdown when clicking inside' , ( ) => {
4853 const { container : { children : [ div ] } , getByRole, getByText } = render (
49- < Dropdown > < div > Child 1</ div > < div > Child 2</ div > </ Dropdown >
54+ < Dropdown >
55+ < div > Child 1</ div >
56+ < div > Child 2</ div >
57+ </ Dropdown >
5058 )
5159
5260 const dropdownButton = getByRole ( 'button' )
@@ -61,7 +69,10 @@ describe('Dropdown Component', () => {
6169
6270 it ( 'closes dropdown on escape key press' , ( ) => {
6371 const { container : { children : [ div ] } , getByRole } = render (
64- < Dropdown > < div > Child 1</ div > < div > Child 2</ div > </ Dropdown >
72+ < Dropdown >
73+ < div > Child 1</ div >
74+ < div > Child 2</ div >
75+ </ Dropdown >
6576 )
6677
6778 const dropdownButton = getByRole ( 'button' )
@@ -94,4 +105,135 @@ describe('Dropdown Component', () => {
94105 expect ( mockRemoveEventListener ) . toHaveBeenCalledWith ( 'keydown' , expect . any ( Function ) )
95106 expect ( mockRemoveEventListener ) . toHaveBeenCalledWith ( 'mousedown' , expect . any ( Function ) )
96107 } )
108+
109+ // Keyboard navigation tests
110+ it ( 'opens dropdown and focuses first item on ArrowDown when closed' , ( ) => {
111+ const { getByRole, getAllByRole } = render (
112+ < Dropdown label = "Menu" >
113+ < button role = "menuitem" > Item 1</ button >
114+ < button role = "menuitem" > Item 2</ button >
115+ </ Dropdown >
116+ )
117+ const menuItems = getAllByRole ( 'menuitem' )
118+ const dropdownButton = getByRole ( 'button' )
119+
120+ // initially closed
121+ expect ( dropdownButton . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
122+
123+ // down arrow to open menu
124+ fireEvent . keyDown ( dropdownButton , { key : 'ArrowDown' , code : 'ArrowDown' } )
125+ expect ( dropdownButton . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
126+
127+ // first menu item should be focused
128+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
129+ } )
130+
131+ it ( 'focuses the next item on ArrowDown and wraps to first item if at the end' , ( ) => {
132+ const { getByRole, getAllByRole } = render (
133+ < Dropdown label = "Menu" >
134+ < button role = "menuitem" > Item 1</ button >
135+ < button role = "menuitem" > Item 2</ button >
136+ </ Dropdown >
137+ )
138+ const menuItems = getAllByRole ( 'menuitem' ) as [ HTMLElement , HTMLElement ]
139+ const dropdownButton = getByRole ( 'button' )
140+
141+ // open menu, first item has focus
142+ fireEvent . click ( dropdownButton )
143+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
144+
145+ // second item should be focused
146+ fireEvent . keyDown ( menuItems [ 0 ] , { key : 'ArrowDown' , code : 'ArrowDown' } )
147+ expect ( document . activeElement ) . toBe ( menuItems [ 1 ] )
148+
149+ // wrap back to first item
150+ fireEvent . keyDown ( menuItems [ 1 ] , { key : 'ArrowDown' , code : 'ArrowDown' } )
151+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
152+ } )
153+
154+ it ( 'focuses the previous item on ArrowUp and wraps to the last item if at the top' , ( ) => {
155+ const { getByRole, getAllByRole } = render (
156+ < Dropdown label = "Menu" >
157+ < button role = "menuitem" > Item 1</ button >
158+ < button role = "menuitem" > Item 2</ button >
159+ </ Dropdown >
160+ )
161+ const menuItems = getAllByRole ( 'menuitem' ) as [ HTMLElement , HTMLElement ]
162+ const dropdownButton = getByRole ( 'button' )
163+
164+ // open menu, first item has focus
165+ fireEvent . click ( dropdownButton )
166+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
167+
168+ // ArrowUp -> should wrap to last item
169+ fireEvent . keyDown ( menuItems [ 0 ] , { key : 'ArrowUp' , code : 'ArrowUp' } )
170+ expect ( document . activeElement ) . toBe ( menuItems [ 1 ] )
171+ } )
172+
173+ it ( 'focuses first item on Home key press' , ( ) => {
174+ const { getByRole, getAllByRole } = render (
175+ < Dropdown label = "Menu" >
176+ < button role = "menuitem" > Item 1</ button >
177+ < button role = "menuitem" > Item 2</ button >
178+ < button role = "menuitem" > Item 3</ button >
179+ </ Dropdown >
180+ )
181+ const menuItems = getAllByRole ( 'menuitem' ) as [ HTMLElement , HTMLElement , HTMLElement ]
182+ const dropdownButton = getByRole ( 'button' )
183+
184+ // open menu, first item has focus
185+ fireEvent . click ( dropdownButton )
186+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
187+
188+ // move to the second item
189+ fireEvent . keyDown ( menuItems [ 0 ] , { key : 'ArrowDown' , code : 'ArrowDown' } )
190+ expect ( document . activeElement ) . toBe ( menuItems [ 1 ] )
191+
192+ // Home key should focus first item
193+ fireEvent . keyDown ( menuItems [ 1 ] , { key : 'Home' , code : 'Home' } )
194+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
195+ } )
196+
197+ it ( 'focuses last item on End key press' , ( ) => {
198+ const { getByRole, getAllByRole } = render (
199+ < Dropdown label = "Menu" >
200+ < button role = "menuitem" > Item 1</ button >
201+ < button role = "menuitem" > Item 2</ button >
202+ < button role = "menuitem" > Item 3</ button >
203+ </ Dropdown >
204+ )
205+ const menuItems = getAllByRole ( 'menuitem' ) as [ HTMLElement , HTMLElement , HTMLElement ]
206+ const dropdownButton = getByRole ( 'button' )
207+
208+ // open menu, first item has focus
209+ fireEvent . click ( dropdownButton )
210+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
211+
212+ // End key should focus the last item
213+ fireEvent . keyDown ( menuItems [ 0 ] , { key : 'End' , code : 'End' } )
214+ expect ( document . activeElement ) . toBe ( menuItems [ 2 ] )
215+ } )
216+
217+ it ( 'closes the menu and puts focus back on the button on Escape' , ( ) => {
218+ const { getByRole, getAllByRole } = render (
219+ < Dropdown label = "Menu" >
220+ < button role = "menuitem" > Item 1</ button >
221+ < button role = "menuitem" > Item 2</ button >
222+ </ Dropdown >
223+ )
224+ const menuItems = getAllByRole ( 'menuitem' ) as [ HTMLElement ]
225+ const dropdownButton = getByRole ( 'button' )
226+
227+ // open menu, first item has focus
228+ fireEvent . click ( dropdownButton )
229+ expect ( document . activeElement ) . toBe ( menuItems [ 0 ] )
230+ expect ( dropdownButton . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
231+
232+ // escape closes menu
233+ fireEvent . keyDown ( menuItems [ 0 ] , { key : 'Escape' , code : 'Escape' } )
234+ expect ( dropdownButton . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
235+
236+ // focus returns to the button
237+ expect ( document . activeElement ) . toBe ( dropdownButton )
238+ } )
97239} )
0 commit comments