Skip to content

Instantly share code, notes, and snippets.

@e-dong
Last active August 1, 2021 13:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save e-dong/4d1cc14838e6ae5265f9aed303f2256c to your computer and use it in GitHub Desktop.
Save e-dong/4d1cc14838e6ae5265f9aed303f2256c to your computer and use it in GitHub Desktop.
React hook to manage open state of context menu
/**
* 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];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment