UNPKG

monday-ui-react-core

Version:

Official monday.com UI resources for application development in React.js

335 lines (310 loc) • 8.81 kB
import React, { useCallback, useState, useMemo, useRef } from "react"; import PropTypes from "prop-types"; import cx from "classnames"; import NOOP from "lodash/noop"; import Dialog from "../Dialog/Dialog"; import Menu from "../Icon/Icons/components/Menu"; import DialogContentContainer from "../DialogContentContainer/DialogContentContainer"; import "./MenuButton.scss"; import Tooltip from "../Tooltip/Tooltip"; function BEMClass(className) { return `menu-button--wrapper--${className}`; } const TOOLTIP_SHOW_TRIGGER = [Dialog.hideShowTriggers.MOUSE_ENTER]; const TOOLTIP_HIDE_TRIGGER = [Dialog.hideShowTriggers.MOUSE_LEAVE]; const showTrigger = ["click", "enter"]; const EMPTY_ARRAY = []; const MOVE_BY = { main: 0, secondary: -6 }; const MenuButton = ({ id, componentClassName, openDialogComponentClassName, children, component, size, open, zIndex, ariaLabel, closeDialogOnContentClick, dialogOffset, dialogPosition, dialogClassName, dialogPaddingSize, onMenuHide, onMenuShow, disabled, text, disabledReason, startingEdge, removeTabCloseTrigger }) => { const buttonRef = useRef(null); const [isOpen, setIsOpen] = useState(open); // const onClick = useCallback( // event => { // if (disabled) { // return; // } // // if (isOpen) { // event.preventDefault(); // event.stopPropagation(); // return; // } // setIsOpen(true); // }, // [setIsOpen, isOpen, disabled] // ); const onMenuDidClose = useCallback( event => { if (event && event.key === "Escape") { setIsOpen(false); const button = buttonRef.current; window.requestAnimationFrame(() => { button.focus(); }); } }, [buttonRef, setIsOpen] ); const onDialogDidHide = useCallback( (event, hideEvent) => { setIsOpen(false); onMenuHide(); const button = buttonRef.current; window.requestAnimationFrame(() => { if (button && hideEvent === Dialog.hideShowTriggers.ESCAPE_KEY) { button.focus(); } }); }, [setIsOpen, onMenuHide, buttonRef] ); const onDialogDidShow = useCallback(() => { setIsOpen(true); onMenuShow(); }, [setIsOpen, onMenuShow]); const [clonedChildren, hideTrigger] = useMemo(() => { const triggers = new Set([ Dialog.hideShowTriggers.CLICK_OUTSIDE, Dialog.hideShowTriggers.TAB_KEY, Dialog.hideShowTriggers.ESCAPE_KEY ]); if (closeDialogOnContentClick) { triggers.add(Dialog.hideShowTriggers.CONTENT_CLICK); } if (removeTabCloseTrigger) { triggers.delete(Dialog.hideShowTriggers.TAB_KEY); } const childrenArr = React.Children.toArray(children); const cloned = childrenArr.map(child => { if (!React.isValidElement(child)) return null; const newProps = {}; if (child.type && child.type.supportFocusOnMount) { newProps.focusOnMount = true; triggers.delete(Dialog.hideShowTriggers.ESCAPE_KEY); } if (child.type && child.type.isMenu) { newProps.onClose = onMenuDidClose; } return React.cloneElement(child, newProps); }); return [cloned, Array.from(triggers)]; }, [children, onMenuDidClose, closeDialogOnContentClick, removeTabCloseTrigger]); const content = useMemo(() => { if (!clonedChildren.length === 0) return <div />; return ( <DialogContentContainer size={dialogPaddingSize} type={DialogContentContainer.types.POPOVER}> {clonedChildren} </DialogContentContainer> ); }, [clonedChildren, dialogPaddingSize]); const computedDialogOffset = useMemo( () => ({ ...MOVE_BY, ...dialogOffset }), [dialogOffset] ); const onMouseUp = event => { if (disabled) { event.currentTarget.blur(); } }; const Icon = component; const iconSize = size - 4; return ( <Tooltip content={disabledReason} position="right" showTrigger={TOOLTIP_SHOW_TRIGGER} hideTrigger={TOOLTIP_HIDE_TRIGGER} > <Dialog wrapperClassName={dialogClassName} position={dialogPosition} startingEdge={startingEdge} animationType="expand" content={content} moveBy={computedDialogOffset} showTrigger={disabled ? EMPTY_ARRAY : showTrigger} hideTrigger={hideTrigger} useDerivedStateFromProps={true} onDialogDidShow={onDialogDidShow} onDialogDidHide={onDialogDidHide} referenceWrapperClassName={BEMClass("reference-icon")} zIndex={zIndex} isOpen={isOpen} > <button id={id} ref={buttonRef} type="button" className={cx("menu-button--wrapper", componentClassName, BEMClass(`size-${size}`), { [BEMClass("open")]: isOpen, [openDialogComponentClassName]: isOpen && openDialogComponentClassName, [BEMClass("disabled")]: disabled, [BEMClass("text")]: text })} aria-haspopup="true" aria-expanded={isOpen} aria-label={!text && ariaLabel} onMouseUp={onMouseUp} aria-disabled={disabled} > <Icon size={Math.min(iconSize, 28).toString()} role="img" aria-hidden="true" /> {text && <span className={BEMClass("inner-text")}>{text}</span>} </button> </Dialog> </Tooltip> ); }; const MenuButtonSizes = { XXS: "16", XS: "24", SMALL: "32", MEDIUM: "40", LARGE: "48" }; const DialogPositions = { LEFT: "left", LEFT_START: "left-start", LEFT_END: "left-end", RIGHT: "right", RIGHT_START: "right-start", RIGHT_END: "right-end", TOP: "top", TOP_START: "top-start", TOP_END: "top-end", BOTTOM: "bottom", BOTTOM_START: "bottom-start", BOTTOM_END: "bottom-end" }; MenuButton.sizes = MenuButtonSizes; MenuButton.paddingSizes = DialogContentContainer.sizes; MenuButton.dialogPositions = DialogPositions; MenuButton.propTypes = { /* Id for the menu button */ id: PropTypes.string, componentClassName: PropTypes.string, /* Class name to add to the button when the dialog is open */ openDialogComponentClassName: PropTypes.string, /** * Receives React Component */ component: PropTypes.func, size: PropTypes.oneOf([ MenuButtonSizes.XXS, MenuButtonSizes.XS, MenuButtonSizes.SMALL, MenuButtonSizes.MEDIUM, MenuButtonSizes.LARGE ]), open: PropTypes.bool, zIndex: PropTypes.number, ariaLabel: PropTypes.string, closeDialogOnContentClick: PropTypes.bool, /* Class name to provide the element which wraps the popover/modal/dialog */ dialogClassName: PropTypes.string, /** * main - `dialogOffset.main` - main axis offset; `dialogOffset.secondary` secondary axis offset */ dialogOffset: PropTypes.shape({ main: PropTypes.number, secondary: PropTypes.number }), dialogPaddingSize: PropTypes.oneOf([ MenuButton.paddingSizes.NONE, MenuButton.paddingSizes.SMALL, MenuButton.paddingSizes.MEDIUM, MenuButton.paddingSizes.LARGE ]), dialogPosition: PropTypes.oneOf([ MenuButton.dialogPositions.BOTTOM_START, MenuButton.dialogPositions.BOTTOM, MenuButton.dialogPositions.BOTTOM_END, MenuButton.dialogPositions.LEFT, MenuButton.dialogPositions.LEFT_START, MenuButton.dialogPositions.LEFT_END, MenuButton.dialogPositions.RIGHT, MenuButton.dialogPositions.RIGHT_START, MenuButton.dialogPositions.RIGHT_END, MenuButton.dialogPositions.TOP, MenuButton.dialogPositions.TOP_END, MenuButton.dialogPositions.TOP_START ]), /** * Dialog Alignment */ startingEdge: PropTypes.string, /* Callback function to be called when the menu is shown */ onMenuShow: PropTypes.func, /* Callback function to be called when the menu is shown */ onMenuHide: PropTypes.func, /** * Text to be displayed after the icon */ text: PropTypes.string, disabled: PropTypes.bool, /** * Disabled tooltip text */ disabledReason: PropTypes.string, /* Remove "Tab" key from the hide trigger */ removeTabCloseTrigger: PropTypes.bool }; MenuButton.defaultProps = { id: undefined, componentClassName: "", component: Menu, size: MenuButtonSizes.SMALL, open: false, zIndex: null, ariaLabel: "Menu", startingEdge: "bottom", closeDialogOnContentClick: false, dialogClassName: "", openDialogComponentClassName: "", dialogOffset: MOVE_BY, dialogPaddingSize: DialogContentContainer.sizes.MEDIUM, dialogPosition: MenuButton.dialogPositions.BOTTOM_START, onMenuShow: NOOP, onMenuHide: NOOP, disabled: false, text: undefined, disabledReason: undefined, removeTabCloseTrigger: false }; export default MenuButton;