UNPKG

glass-app-manager

Version:

Informatica's Glass Framework CLI for bootstrapping

175 lines (144 loc) 5.16 kB
// @flow import * as React from "react"; import { isSubmenu, openMenu, closeMenu, toggleMenu, MenuOpener } from "./utils"; import MenuItem from "./MenuItem"; import Submenu from "./Submenu"; export type MenuProps = { /** * Children of the menu. */ children: React.Node, /** * JSX element that triggers the opening of the menu. */ trigger: React.Node, /** * Callback to execute just after the trigger is invoked and before the menu is shown */ onBeforeShow?: (e: SyntheticEvent<any>) => void, /** * True for a context menu. */ context?: boolean, /** * Callback to execute when the menu is opened. */ onOpen: () => void, /** * Callback to execute when the menu is closed. */ onClose: () => void, }; const MENU_FLIP_PADDING = 10; const MENU_TRIGGER_GAP = 5; function Menu({ children, trigger, context, onBeforeShow, onOpen, onClose }: MenuProps) { const [open, setOpen] = React.useState(false); const [mousePoint, setMousePoint] = React.useState(); const handleChange = React.useCallback( state => { if (state) { if (onOpen) { onOpen(); } } else { if (onClose) { onClose(); } } }, [onOpen, onClose] ); const handleClick = React.useCallback(() => { toggleMenu(open, setOpen, handleChange); }, [open, handleChange]); // Called when mouse click happens outside the menu. const handleClickOutside = React.useCallback( (e: SyntheticEvent<HTMLElement>) => { if (!isSubmenu(e)) { closeMenu(open, setOpen, handleChange); } }, [open, handleChange] ); const handleContextMenu = React.useCallback( e => { if (typeof onBeforeShow === "function") { onBeforeShow(e); } openMenu(open, setOpen, handleChange); setMousePoint({ pageX: e.pageX, pageY: e.pageY, clientX: e.clientX, clientY: e.clientY, }); e.preventDefault(); e.stopPropagation(); }, [open, handleChange, onBeforeShow] ); // Detect contextmenu events happened outside the menu. const handleContextMenuOutside = React.useCallback(e => closeMenu(open, setOpen, handleChange), [ open, handleChange, ]); // Specify the position of the context menu. const menuLocation = React.useCallback(({ targetRect, popoverRect, position, align, nudgedLeft, nudgedTop }) => { if (context) { let top = mousePoint.pageY; let left = mousePoint.pageX; const { innerWidth: windowWidth, innerHeight: windowHeight } = window; if (mousePoint.clientX + popoverRect.width > windowWidth) { left -= mousePoint.clientX + popoverRect.width - windowWidth + MENU_FLIP_PADDING; } if (mousePoint.clientY + popoverRect.height > windowHeight) { top -= mousePoint.clientY + popoverRect.height - windowHeight + MENU_FLIP_PADDING; } return { top: top, left: left }; } const { scrollX: windowX, scrollY: windowY } = window; let top = nudgedTop + windowY; if (position === "bottom") { top -= MENU_TRIGGER_GAP; } else if (position === "top") { top += MENU_TRIGGER_GAP; } return { top: top, left: nudgedLeft + windowX }; }); const wrappedTrigger = React.useMemo(() => { let triggerProps; if (context) { triggerProps = { onContextMenu: handleContextMenu }; return React.cloneElement(trigger, { ...triggerProps, "aria-haspopup": true, "aria-expanded": open }); } return ( <span className="menu__trigger" onClick={handleClick} aria-haspopup aria-expanded={open}> {trigger} </span> ); }, [open, context, trigger, handleChange, handleContextMenu, handleClick]); React.useEffect(() => { window.addEventListener("contextmenu", handleContextMenuOutside); return () => window.removeEventListener("contextmenu", handleContextMenuOutside); }); return ( <MenuOpener open={open} setOpen={setOpen} onChange={handleChange} position={["bottom", "top"]} align="end" onClickOutside={handleClickOutside} trigger={wrappedTrigger} contentLocation={menuLocation}> {children} </MenuOpener> ); } Menu.Item = MenuItem; Menu.Submenu = Submenu; Menu.Separator = () => { return <li className="menu__separator" />; }; Menu.defaultProps = { context: false }; export default Menu;