|
/** |
|
* Handle the open/close state of a context menu and the element that triggers it. |
|
* The following cases are taken cared of: |
|
* - Clicking the trigger element will toggle the menu open or close |
|
* - If the menu is open, clicking outside the menu will close it |
|
* - If the menu is open, pressing the esc key will close it |
|
* - The menu may be opened/closed externally since this hook returns the setState function |
|
* |
|
* Example Usage: |
|
* import { useHandleMenuOpenState } from 'utils/userInputHandlers'; |
|
* . . . |
|
* const dropdownRef = React.useRef<HTMLDivElement>(null); |
|
* const menuRef = React.useRef<HTMLDivElement>(null); |
|
* . . . |
|
* const [isOpened, setIsOpened] = useHandleMenuOpenState(dropdownRef, menuRef); |
|
* . . . |
|
* <div ... ref={dropdownRef} ... /> |
|
* <div ... ref={menuRef} ... /> |
|
*/ |
|
|
|
import * as React from 'react'; |
|
|
|
/** |
|
* menuTriggerRef - The element that will trigger the opening of the menu |
|
* menuRef - The element that represents the opened menu |
|
* @returns [boolean, Function] - Two element array where the first element is the isOpen state and |
|
* the second element is the setState function, similarly to the React.useState hook. |
|
*/ |
|
export type HandleMenuOpenStateHook = ( |
|
menuTriggerRef: React.RefObject<HTMLElement>, |
|
menuRef: React.RefObject<HTMLElement> |
|
) => [boolean, Function]; |
|
|
|
export const useHandleMenuOpenState: HandleMenuOpenStateHook = ( |
|
menuTriggerRef, |
|
menuRef |
|
) => { |
|
const [isOpened, setisOpened] = React.useState(false); |
|
|
|
/** |
|
* Mousedown handler to toggle menu open state when menutrigger element is clicked |
|
*/ |
|
const toggleMenuHandler = React.useCallback(() => { |
|
setisOpened((prevVal) => !prevVal); |
|
}, []); |
|
|
|
/** |
|
* Mousedown handler for detecting clicks outside the context / dropdown menu |
|
* |
|
* @param e The mouse event when the mouse button is clicked down |
|
*/ |
|
const outsideMenuClickHandler = React.useCallback((e: MouseEvent) => { |
|
if (menuTriggerRef.current && menuRef.current) { |
|
// Clicking outside the context menu should close it |
|
// The menu trigger is ignored since it acts as a toggle, @see toggleMenuHandler() |
|
if ( |
|
!e.composedPath().includes(menuRef.current) && |
|
!e.composedPath().includes(menuTriggerRef.current) |
|
) { |
|
setisOpened(false); |
|
} |
|
} |
|
}, []); |
|
|
|
/** |
|
* Keydown handler that closes the menu when the escape key is pressed |
|
* |
|
* @param e The keyboard event when the key is pressed |
|
*/ |
|
const escKeyHandler = React.useCallback((e: KeyboardEvent) => { |
|
if (e.key === 'Escape') { |
|
setisOpened(false); |
|
} |
|
}, []); |
|
|
|
/** |
|
* Sets up toggle menu handler |
|
*/ |
|
React.useEffect(() => { |
|
if (menuTriggerRef.current) { |
|
menuTriggerRef.current.addEventListener('mousedown', toggleMenuHandler); |
|
} |
|
|
|
return () => { |
|
if (menuTriggerRef.current) { |
|
menuTriggerRef.current.removeEventListener( |
|
'mousedown', |
|
toggleMenuHandler |
|
); |
|
} |
|
}; |
|
}, [menuTriggerRef]); |
|
|
|
/** |
|
* Registers the appropriate listeners to close the opened menu, |
|
* otherwise the listeners are removed. |
|
*/ |
|
React.useEffect(() => { |
|
if (menuTriggerRef.current) { |
|
if (isOpened) { |
|
document.addEventListener('mousedown', outsideMenuClickHandler); |
|
document.addEventListener('keydown', escKeyHandler); |
|
} else { |
|
document.removeEventListener('mousedown', outsideMenuClickHandler); |
|
document.removeEventListener('keydown', escKeyHandler); |
|
} |
|
} |
|
}, [isOpened]); |
|
|
|
// Functional Component that uses this hook can get read/write access to the open state |
|
return [isOpened, setisOpened]; |
|
}; |