UNPKG

@react-md/menu

Version:

Create menus that auto-position themselves within the viewport and adhere to the accessibility guidelines

149 lines (129 loc) 4.52 kB
import { HTMLAttributes, MutableRefObject, Ref } from "react"; import { useCloseOnOutsideClick, useEnsuredRef } from "@react-md/utils"; import { useMenuClick } from "./useMenuClick"; import { useMenuKeyDown } from "./useMenuKeyDown"; export interface MenuOptions { /** * An optional ref to be merged into the menu's required ref handler. */ ref?: Ref<HTMLDivElement | null>; /** * Boolean if the menu is currently visible. */ visible: boolean; /** * The id of the element that controls the menu's visibility. This is required * so that the menu won't be closed when the control element is clicked since * it'll have it's own toggle functionality built-in already. */ controlId: string; /** * Boolean if the menu is oriented horizontally instead of vertically. This * will update the keydown handlers to use the `ArrowLeft` and `ArrowRight` * keys instead of `ArrowUp` and `ArrrowDown` to navigate. */ horizontal?: boolean; /** * An optional click handler to call when the `Menu` (or any child item) is * clicked. This will be merged with the default implementation to close the * menu once clicked. */ onClick?: React.MouseEventHandler<HTMLDivElement>; /** * An optional keydown handler to call when the `Menu` (or any child item) * triggers a keydown event. This will be merged witht he default logic of * allowing items to be focused with the arrow keys or closing when the escape * or tab key is pressed. */ onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>; /** * The default focusable element within the menu. */ defaultFocus: "first" | "last" | string; /** * The function that should close the menu. */ onRequestClose: () => void; /** * Boolean if the close on outside click logic should consider the control * element within the menu and not call the `onRequestClose` function when it * is been clicked. This should be enabled when creating a context menu but * normally should remain `false` otherwise since the control element has it's * own toggle logic that conflicts with this close click. */ disableControlClickOkay?: boolean; /** * Boolean if the menu has been portalled so that the tab keypress behavior * can be fixed since tab order is destroyed when portalling. */ portalled?: boolean; } interface ReturnValue extends Pick<HTMLAttributes<HTMLDivElement>, "onClick" | "onKeyDown"> { ref: (instance: HTMLDivElement | null) => void; menuRef: MutableRefObject<HTMLDivElement | null>; } /** * This hook is used to provide all the menu functionality within the `Menu` * component. It'll ensure that: * * - the menu will be closed if an element outside of the menu is clicked * - the menu items within the menu are keyboard focusable after typing or using * the arrow keys * - the menu will close if the Escape key or Tab key is pressed (tab since * it'll lose focus) * - conditionally close the menu if the page is scrolled while visible. */ export function useMenu({ ref: propRef, visible, controlId, horizontal = false, onClick: propOnClick, onKeyDown: propOnKeyDown, portalled = false, defaultFocus, onRequestClose, disableControlClickOkay = false, }: MenuOptions): ReturnValue { const [ref, refHandler] = useEnsuredRef(propRef); useCloseOnOutsideClick({ element: ref, enabled: visible, onOutsideClick(element, target, contains) { if (!element || !target) { return; } const control = document.getElementById(controlId); // Need to also check if we have an `aria-expanded` visible anywhere since // the child menus need to be portalled out to fix the overflow issue in // Safari. If we didn't need to portal, this line could be removed as the // `menu.current` would contain the child menu and not close. const expanded = ref.current && ref.current.querySelector('[aria-expanded="true"]') && target.closest('[role="menu"]'); if ( (disableControlClickOkay || !contains(control, target)) && !expanded ) { onRequestClose(); } }, }); const onClick = useMenuClick({ onClick: propOnClick, onRequestClose }); const onKeyDown = useMenuKeyDown({ menu: ref.current, onKeyDown: propOnKeyDown, onRequestClose, portalled, horizontal, defaultFocus, }); return { ref: refHandler, menuRef: ref, onClick, onKeyDown, }; }