@@ -12,7 +12,6 @@ import React, {
12
12
useEffect ,
13
13
useMemo ,
14
14
useReducer ,
15
- useRef ,
16
15
useState
17
16
} from 'react' ;
18
17
import { useSelection } from '@zendeskgarden/container-selection' ;
@@ -49,6 +48,7 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
49
48
onChange = ( ) => undefined ,
50
49
isExpanded,
51
50
defaultExpanded = false ,
51
+ restoreFocus = true ,
52
52
selectedItems,
53
53
focusedValue,
54
54
defaultFocusedValue
@@ -94,8 +94,6 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
94
94
*/
95
95
96
96
const [ menuVisible , setMenuVisible ] = useState < boolean > ( false ) ;
97
- const focusTriggerRef = useRef < boolean > ( false ) ;
98
-
99
97
const [ state , dispatch ] = useReducer ( stateReducer , {
100
98
focusedValue,
101
99
isExpanded : isExpanded || defaultExpanded ,
@@ -135,6 +133,15 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
135
133
136
134
// Internal
137
135
136
+ const returnFocusToTrigger = useCallback (
137
+ ( skip ?: boolean ) => {
138
+ if ( ! skip && restoreFocus && triggerRef . current ) {
139
+ triggerRef . current . focus ( ) ;
140
+ }
141
+ } ,
142
+ [ triggerRef , restoreFocus ]
143
+ ) ;
144
+
138
145
const closeMenu = useCallback (
139
146
( changeType : string ) => {
140
147
dispatch ( {
@@ -281,13 +288,22 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
281
288
}
282
289
} ) ;
283
290
291
+ // Skip focus return when isExpanded === true
292
+ returnFocusToTrigger ( ! controlledIsExpanded ) ;
293
+
284
294
onChange ( {
285
295
type : changeType ,
286
296
focusedValue : null ,
287
297
isExpanded : ! controlledIsExpanded
288
298
} ) ;
289
299
} ,
290
- [ controlledIsExpanded , isFocusedValueControlled , isExpandedControlled , onChange ]
300
+ [
301
+ isFocusedValueControlled ,
302
+ isExpandedControlled ,
303
+ controlledIsExpanded ,
304
+ returnFocusToTrigger ,
305
+ onChange
306
+ ]
291
307
) ;
292
308
293
309
const handleTriggerKeyDown = useCallback (
@@ -321,14 +337,23 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
321
337
}
322
338
} ) ;
323
339
340
+ returnFocusToTrigger ( ) ;
341
+
324
342
onChange ( {
325
343
type : changeType ,
326
344
focusedValue : defaultFocusedValue || nextFocusedValue ,
327
345
isExpanded : true
328
346
} ) ;
329
347
}
330
348
} ,
331
- [ isExpandedControlled , isFocusedValueControlled , defaultFocusedValue , onChange , values ]
349
+ [
350
+ values ,
351
+ isFocusedValueControlled ,
352
+ defaultFocusedValue ,
353
+ isExpandedControlled ,
354
+ returnFocusToTrigger ,
355
+ onChange
356
+ ]
332
357
) ;
333
358
334
359
const handleMenuKeyDown = useCallback (
@@ -341,25 +366,25 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
341
366
342
367
const type = StateChangeTypes [ key === KEYS . ESCAPE ? 'MenuKeyDownEscape' : 'MenuKeyDownTab' ] ;
343
368
344
- if ( KEYS . ESCAPE === key ) {
345
- focusTriggerRef . current = true ;
346
- }
369
+ // TODO: Investigate why focus goes to body instead of next interactive element on TAB keydown. Meanwhile, returning focus to trigger.
370
+ returnFocusToTrigger ( ) ;
347
371
348
372
closeMenu ( type ) ;
349
373
}
350
374
} ,
351
- [ closeMenu , focusTriggerRef ]
375
+ [ closeMenu , returnFocusToTrigger ]
352
376
) ;
353
377
354
378
const handleMenuBlur = useCallback (
355
379
( event : MouseEvent ) => {
356
380
const path = event . composedPath ( ) ;
357
381
358
382
if ( ! path . includes ( menuRef . current ! ) && ! path . includes ( triggerRef . current ! ) ) {
383
+ returnFocusToTrigger ( ) ;
359
384
closeMenu ( StateChangeTypes . MenuBlur ) ;
360
385
}
361
386
} ,
362
- [ closeMenu , menuRef , triggerRef ]
387
+ [ closeMenu , menuRef , returnFocusToTrigger , triggerRef ]
363
388
) ;
364
389
365
390
const handleMenuMouseLeave = useCallback ( ( ) => {
@@ -395,6 +420,8 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
395
420
}
396
421
} ) ;
397
422
423
+ returnFocusToTrigger ( isTransitionItem ) ;
424
+
398
425
onChange ( {
399
426
type : changeType ,
400
427
value : item . value ,
@@ -403,10 +430,11 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
403
430
} ) ;
404
431
} ,
405
432
[
433
+ getSelectedItems ,
406
434
state . nestedPathIds ,
407
435
isExpandedControlled ,
408
436
isSelectedItemsControlled ,
409
- getSelectedItems ,
437
+ returnFocusToTrigger ,
410
438
onChange
411
439
]
412
440
) ;
@@ -448,19 +476,13 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
448
476
...( nextSelection && { selectedItems : nextSelection } )
449
477
} ;
450
478
451
- if ( item . href ) {
452
- if ( key === KEYS . SPACE ) {
453
- event . preventDefault ( ) ;
479
+ event . preventDefault ( ) ;
454
480
455
- triggerLink ( event . target as HTMLAnchorElement , environment || window ) ;
456
- }
457
- } else {
458
- event . preventDefault ( ) ;
481
+ if ( item . href ) {
482
+ triggerLink ( event . target as HTMLAnchorElement , environment || window ) ;
459
483
}
460
484
461
- if ( ! isTransitionItem ) {
462
- focusTriggerRef . current = true ;
463
- }
485
+ returnFocusToTrigger ( isTransitionItem ) ;
464
486
} else if ( key === KEYS . RIGHT ) {
465
487
if ( rtl && isPrevious ) {
466
488
changeType = StateChangeTypes . MenuItemKeyDownPrevious ;
@@ -529,15 +551,15 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
529
551
}
530
552
} ,
531
553
[
532
- rtl ,
533
- state . nestedPathIds ,
554
+ getSelectedItems ,
534
555
isExpandedControlled ,
535
- isFocusedValueControlled ,
536
556
isSelectedItemsControlled ,
537
- focusTriggerRef ,
557
+ returnFocusToTrigger ,
538
558
environment ,
559
+ rtl ,
539
560
getNextFocusedValue ,
540
- getSelectedItems ,
561
+ isFocusedValueControlled ,
562
+ state . nestedPathIds ,
541
563
onChange
542
564
]
543
565
) ;
@@ -594,12 +616,8 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
594
616
} , [ controlledIsExpanded , handleMenuBlur , handleMenuKeyDown , environment ] ) ;
595
617
596
618
/**
597
- * Handles focus depending on menu state:
598
- * - When opened, focus the menu via `focusedValue`
599
- * - When closed, focus the trigger via `triggerRef`
600
- *
601
- * This effect is intended to prevent focusing the trigger or menu
602
- * unless the menu is in the right expansion state.
619
+ * When the menu is opened, this effect sets focus on the current menu item using `focusedValue`
620
+ * or on the first menu item.
603
621
*/
604
622
useEffect ( ( ) => {
605
623
if ( state . focusOnOpen && menuVisible && controlledFocusedValue && controlledIsExpanded ) {
@@ -614,13 +632,7 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
614
632
615
633
ref && ref . focus ( ) ;
616
634
}
617
-
618
- if ( ! menuVisible && ! controlledIsExpanded && focusTriggerRef . current ) {
619
- triggerRef ?. current ?. focus ( ) ;
620
- focusTriggerRef . current = false ;
621
- }
622
635
} , [
623
- focusTriggerRef ,
624
636
values ,
625
637
menuVisible ,
626
638
itemRefs ,
0 commit comments