| 
									
										
										
										
											2021-11-18 04:09:02 +08:00
										 |  |  | import { RefObject, useEffect, useState } from 'react'; | 
					
						
							|  |  |  | import { useEffectOnce } from 'react-use'; | 
					
						
							| 
									
										
										
										
											2022-04-22 21:33:13 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-18 04:09:02 +08:00
										 |  |  | const modulo = (a: number, n: number) => ((a % n) + n) % n; | 
					
						
							|  |  |  | const UNFOCUSED = -1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @internal */ | 
					
						
							|  |  |  | export interface UseMenuFocusProps { | 
					
						
							|  |  |  |   localRef: RefObject<HTMLDivElement>; | 
					
						
							|  |  |  |   isMenuOpen?: boolean; | 
					
						
							|  |  |  |   openedWithArrow?: boolean; | 
					
						
							|  |  |  |   setOpenedWithArrow?: (openedWithArrow: boolean) => void; | 
					
						
							|  |  |  |   close?: () => void; | 
					
						
							|  |  |  |   onOpen?: (focusOnItem: (itemId: number) => void) => void; | 
					
						
							|  |  |  |   onClose?: () => void; | 
					
						
							|  |  |  |   onKeyDown?: React.KeyboardEventHandler; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @internal */ | 
					
						
							|  |  |  | export type UseMenuFocusReturn = [(event: React.KeyboardEvent) => void, () => void]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @internal */ | 
					
						
							|  |  |  | export const useMenuFocus = ({ | 
					
						
							|  |  |  |   localRef, | 
					
						
							|  |  |  |   isMenuOpen, | 
					
						
							|  |  |  |   openedWithArrow, | 
					
						
							|  |  |  |   setOpenedWithArrow, | 
					
						
							|  |  |  |   close, | 
					
						
							|  |  |  |   onOpen, | 
					
						
							|  |  |  |   onClose, | 
					
						
							|  |  |  |   onKeyDown, | 
					
						
							|  |  |  | }: UseMenuFocusProps): UseMenuFocusReturn => { | 
					
						
							|  |  |  |   const [focusedItem, setFocusedItem] = useState(UNFOCUSED); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     if (isMenuOpen && openedWithArrow) { | 
					
						
							|  |  |  |       setFocusedItem(0); | 
					
						
							|  |  |  |       setOpenedWithArrow?.(false); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [isMenuOpen, openedWithArrow, setOpenedWithArrow]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							| 
									
										
										
										
											2022-10-07 22:22:21 +08:00
										 |  |  |     const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>( | 
					
						
							|  |  |  |       `[data-role="menuitem"]` | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     menuItems?.[focusedItem]?.focus(); | 
					
						
							| 
									
										
										
										
											2021-11-18 04:09:02 +08:00
										 |  |  |     menuItems?.forEach((menuItem, i) => { | 
					
						
							| 
									
										
										
										
											2022-10-07 22:22:21 +08:00
										 |  |  |       menuItem.tabIndex = i === focusedItem ? 0 : -1; | 
					
						
							| 
									
										
										
										
											2021-11-18 04:09:02 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }, [localRef, focusedItem]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffectOnce(() => { | 
					
						
							| 
									
										
										
										
											2022-10-07 22:22:21 +08:00
										 |  |  |     const firstMenuItem = localRef?.current?.querySelector<HTMLElement | HTMLButtonElement | HTMLAnchorElement>( | 
					
						
							|  |  |  |       `[data-role="menuitem"]` | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-11-18 04:09:02 +08:00
										 |  |  |     if (firstMenuItem) { | 
					
						
							|  |  |  |       firstMenuItem.tabIndex = 0; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     onOpen?.(setFocusedItem); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const handleKeys = (event: React.KeyboardEvent) => { | 
					
						
							| 
									
										
										
										
											2022-10-07 22:22:21 +08:00
										 |  |  |     const menuItems = localRef?.current?.querySelectorAll<HTMLElement | HTMLButtonElement | HTMLAnchorElement>( | 
					
						
							|  |  |  |       `[data-role="menuitem"]` | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-11-18 04:09:02 +08:00
										 |  |  |     const menuItemsCount = menuItems?.length ?? 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     switch (event.key) { | 
					
						
							|  |  |  |       case 'ArrowUp': | 
					
						
							|  |  |  |         event.preventDefault(); | 
					
						
							|  |  |  |         event.stopPropagation(); | 
					
						
							|  |  |  |         setFocusedItem(modulo(focusedItem - 1, menuItemsCount)); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'ArrowDown': | 
					
						
							|  |  |  |         event.preventDefault(); | 
					
						
							|  |  |  |         event.stopPropagation(); | 
					
						
							|  |  |  |         setFocusedItem(modulo(focusedItem + 1, menuItemsCount)); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'ArrowLeft': | 
					
						
							|  |  |  |         event.preventDefault(); | 
					
						
							|  |  |  |         event.stopPropagation(); | 
					
						
							|  |  |  |         setFocusedItem(UNFOCUSED); | 
					
						
							|  |  |  |         close?.(); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Home': | 
					
						
							|  |  |  |         event.preventDefault(); | 
					
						
							|  |  |  |         event.stopPropagation(); | 
					
						
							|  |  |  |         setFocusedItem(0); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'End': | 
					
						
							|  |  |  |         event.preventDefault(); | 
					
						
							|  |  |  |         event.stopPropagation(); | 
					
						
							|  |  |  |         setFocusedItem(menuItemsCount - 1); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Enter': | 
					
						
							|  |  |  |         event.preventDefault(); | 
					
						
							|  |  |  |         event.stopPropagation(); | 
					
						
							| 
									
										
										
										
											2022-10-07 22:22:21 +08:00
										 |  |  |         menuItems?.[focusedItem]?.click(); | 
					
						
							| 
									
										
										
										
											2021-11-18 04:09:02 +08:00
										 |  |  |         break; | 
					
						
							|  |  |  |       case 'Escape': | 
					
						
							|  |  |  |         event.preventDefault(); | 
					
						
							|  |  |  |         event.stopPropagation(); | 
					
						
							|  |  |  |         onClose?.(); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       case 'Tab': | 
					
						
							|  |  |  |         onClose?.(); | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |       default: | 
					
						
							|  |  |  |         break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Forward event to parent
 | 
					
						
							|  |  |  |     onKeyDown?.(event); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const handleFocus = () => { | 
					
						
							|  |  |  |     if (focusedItem === UNFOCUSED) { | 
					
						
							|  |  |  |       setFocusedItem(0); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return [handleKeys, handleFocus]; | 
					
						
							|  |  |  | }; |