glass-app-manager
Version:
Informatica's Glass Framework CLI for bootstrapping
175 lines (144 loc) • 5.16 kB
JavaScript
// @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;