UNPKG

react-cm-ui

Version:
363 lines (338 loc) 10.4 kB
import { get, isFunction, isNil, } from 'lodash'; import ClassNames from 'classnames'; import ClickAwayListener from '@material-ui/core/ClickAwayListener'; import PropTypes from 'prop-types'; import MUIPopper from '@material-ui/core/Popper'; import React, { useCallback, useEffect, useState, } from 'react'; import { UI_CLASS_NAME, BEM_POPOVER, } from '../../global/constants'; import Grow from '../../utils/grow'; import makeStyles from '../../styles/makeStyles'; const propTypes = { /** * Override or extend the styles applied to Popover. */ classes: PropTypes.shape({ arrow: PropTypes.string, content: PropTypes.string, popoverRoot: PropTypes.string, popper: PropTypes.string, }), /** * Additional classes. */ className: PropTypes.string, /** * The anchor element. */ children: PropTypes.node.isRequired, /** * The content in the Popover. */ content: PropTypes.node, /** * If `true`, Popover will not be fired. */ disable: PropTypes.bool, /** * The `id` of the Popover. */ id: PropTypes.string, /** * The max-width of the Popover */ maxWidth: PropTypes.number, /** * Popper.js is based on a "plugin-like" architecture; * most of its features are fully encapsulated "modifiers". * * A modifier is a function that is called each time Popper.js needs to * compute the position of the popper. * For this reason, modifiers should be very performant to avoid bottlenecks. * To learn how to create a modifier, [read the modifiers documentation](https://popper.js.org/docs/v1/#modifiers). */ modifiers: PropTypes.shape({}), /** * The mouse event to listen to. */ mouseEvent: PropTypes.oneOf(['onClick', 'onMouseEnter']), /** * Event handler for when cursor clicks off of target. */ onClickAway: PropTypes.func, /** * If `true`, the Popover is visible. */ open: PropTypes.bool, /** * Popper placement. */ placement: PropTypes.oneOf([ 'bottom-end', 'bottom-start', 'bottom', 'left-end', 'left-start', 'left', 'right-end', 'right-start', 'right', 'top-end', 'top-start', 'top', ]), /** * The width of the Popover */ width: PropTypes.number, }; const defaultProps = { classes: undefined, className: undefined, content: undefined, id: undefined, disable: false, maxWidth: undefined, modifiers: undefined, mouseEvent: 'onClick', onClickAway: undefined, open: undefined, placement: 'bottom', width: 250, }; const useStyles = makeStyles(({ palette, shadows, shape, spacing, typography, zIndex, }) => ({ arrow: { boxSizing: 'border-box', color: get(palette, 'background.primary'), height: '0.71em', overflow: 'hidden', position: 'absolute', width: '1em', '&::before': { backgroundColor: 'currentColor', boxShadow: shadows[1], content: '""', display: 'block', height: '100%', margin: 'auto', transform: 'rotate(45deg)', width: '100%', }, }, popoverRoot: { backgroundColor: get(palette, 'background.primary'), borderRadius: shape.borderRadiusSecondary, boxShadow: '0 7px 22px 0 rgba(0, 0, 0, 0.34)', color: get(palette, 'text.primary'), fontFamily: get(typography, 'fontFamily'), fontSize: get(typography, 'fontSize'), fontWeight: get(typography, 'fontWeightRegular'), maxWidth: ({ maxWidth }) => maxWidth || 'none', overflow: 'visible', padding: spacing(2), position: 'relative', width: ({ width }) => width, }, popper: { zIndex: zIndex.popover, '&[x-placement*="bottom"] $arrow': { top: 0, left: 0, marginTop: '-0.71em', marginLeft: 4, marginRight: 4, '&::before': { transformOrigin: '0 100%', }, }, '&[x-placement*="top"] $arrow': { bottom: 0, left: 0, marginBottom: '-0.71em', marginLeft: 4, marginRight: 4, '&::before': { transformOrigin: '100% 0', }, }, '&[x-placement*="right"] $arrow': { left: 0, marginLeft: '-0.71em', height: '1em', width: '0.71em', marginTop: 4, marginBottom: 4, '&::before': { transformOrigin: '100% 100%', }, }, '&[x-placement*="left"] $arrow': { right: 0, marginRight: '-0.71em', height: '1em', width: '0.71em', marginTop: 4, marginBottom: 4, '&::before': { transformOrigin: '0 0', }, }, }, content: {}, })); /** * The Popover is used to display some content on top of another. */ function Popover(props) { const { children, className, content, disable, id, modifiers, mouseEvent, onClickAway: onClickAwayProp, open: openProp, placement, } = props; const classes = useStyles(props); const [arrowRef, setArrowRef] = useState(null); const [childRef, setChildRef] = useState(null); const [isOpen, setIsOpen] = useState(false); useEffect(() => { if (openProp === true || openProp === false) { setIsOpen(!disable && openProp); } else { setIsOpen(!disable && Boolean(childRef)); } }, [childRef, disable, openProp]); const onClick = useCallback((event, childProps) => { const { onClick: onClickChildProp, } = childProps; if (isFunction(onClickChildProp)) { onClickChildProp(event); } if (event && event.currentTarget && mouseEvent === 'onClick') { setChildRef(event.currentTarget); } }, [mouseEvent]); const onMouseEnter = useCallback((event, childProps) => { const { onMouseEnter: onMouseEnterChildProp, } = childProps; if (isFunction(onMouseEnterChildProp)) { onMouseEnterChildProp(event); } if (event && event.currentTarget && mouseEvent === 'onMouseEnter') { setChildRef(event.currentTarget); } }, [mouseEvent]); const onMouseLeave = useCallback((event, childProps) => { const { onMouseLeave: onMouseLeaveChildProp, } = childProps; if (isFunction(onMouseLeaveChildProp)) { onMouseLeaveChildProp(); } setChildRef(null); }, []); const onClickAway = useCallback(() => { if (isFunction(onClickAwayProp)) { onClickAwayProp(); } setChildRef(null); }, [onClickAwayProp]); const rootClasses = ClassNames( UI_CLASS_NAME, BEM_POPOVER, className, ); return ( <div className={rootClasses} id={id} > {!disable || isNil(content) ? ( <React.Fragment> {React.cloneElement( children, { onClick: (event) => onClick(event, children.props), onMouseEnter: (event) => onMouseEnter(event, children.props), onMouseLeave: (event) => onMouseLeave(event, children.props), }, )} <MUIPopper anchorEl={childRef} className={ClassNames( `${BEM_POPOVER}--popper`, classes.popper, )} modifiers={{ arrow: { enabled: true, element: arrowRef, }, offset: { offset: '0, 11', }, ...modifiers, }} open={isOpen} placement={placement} transition > {({ TransitionProps }) => ( <Grow // eslint-disable-next-line react/jsx-props-no-spreading {...TransitionProps} > <div> <ClickAwayListener mouseEvent="onClick" onClickAway={onClickAway} > <div className={classes.popoverRoot} > <span className={classes.arrow} ref={setArrowRef} /> <div className={ClassNames( `${BEM_POPOVER}--content`, classes.content, )} > {content} </div> </div> </ClickAwayListener> </div> </Grow> )} </MUIPopper> </React.Fragment> ) : children} </div> ); } Popover.defaultProps = defaultProps; Popover.propTypes = propTypes; export default Popover;