UNPKG

@activelylearn/material-ui

Version:

Material-UI's workspace package

485 lines (443 loc) 12.9 kB
/* eslint-disable react/no-multi-comp, no-underscore-dangle */ import React from 'react'; import PropTypes from 'prop-types'; import EventListener from 'react-event-listener'; import debounce from 'debounce'; import warning from 'warning'; import classNames from 'classnames'; import { Manager, Popper, Target } from 'react-popper'; import { capitalize } from '../utils/helpers'; import RootRef from '../RootRef'; import Portal from '../Portal'; import common from '../colors/common'; import withStyles from '../styles/withStyles'; export const styles = theme => ({ popper: { zIndex: theme.zIndex.tooltip, pointerEvents: 'none', '&$open': { pointerEvents: 'auto', }, }, open: {}, tooltip: { backgroundColor: theme.palette.grey[700], borderRadius: 2, color: common.white, fontFamily: theme.typography.fontFamily, opacity: 0, transform: 'scale(0)', transition: theme.transitions.create(['opacity', 'transform'], { duration: theme.transitions.duration.shortest, easing: theme.transitions.easing.easeIn, }), minHeight: 0, padding: `${theme.spacing.unit / 2}px ${theme.spacing.unit}px`, fontSize: theme.typography.pxToRem(10), lineHeight: `${theme.typography.round(14 / 10)}em`, '&$open': { opacity: 0.9, transform: 'scale(1)', transition: theme.transitions.create(['opacity', 'transform'], { duration: theme.transitions.duration.shortest, easing: theme.transitions.easing.easeOut, }), }, }, touch: { padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, fontSize: theme.typography.pxToRem(14), lineHeight: `${theme.typography.round(16 / 14)}em`, }, tooltipPlacementLeft: { transformOrigin: 'right center', margin: `0 ${theme.spacing.unit * 3}px`, [theme.breakpoints.up('sm')]: { margin: '0 14px', }, }, tooltipPlacementRight: { transformOrigin: 'left center', margin: `0 ${theme.spacing.unit * 3}px`, [theme.breakpoints.up('sm')]: { margin: '0 14px', }, }, tooltipPlacementTop: { transformOrigin: 'center bottom', margin: `${theme.spacing.unit * 3}px 0`, [theme.breakpoints.up('sm')]: { margin: '14px 0', }, }, tooltipPlacementBottom: { transformOrigin: 'center top', margin: `${theme.spacing.unit * 3}px 0`, [theme.breakpoints.up('sm')]: { margin: '14px 0', }, }, }); function flipPlacement(placement) { switch (placement) { case 'bottom-end': return 'bottom-start'; case 'bottom-start': return 'bottom-end'; case 'top-end': return 'top-start'; case 'top-start': return 'top-end'; default: return placement; } } class Tooltip extends React.Component { constructor(props, context) { super(props, context); this.isControlled = props.open != null; if (!this.isControlled) { // not controlled, use internal state this.state.open = false; } } state = {}; componentDidMount() { warning( !this.children || !this.children.disabled || !this.children.tagName.toLowerCase() === 'button', [ 'Material-UI: you are providing a disabled `button` child to the Tooltip component.', 'A disabled element does not fire events.', "Tooltip needs to listen to the child element's events to display the title.", '', 'Place a `div` container on top of the element.', ].join('\n'), ); } componentWillUnmount() { clearTimeout(this.enterTimer); clearTimeout(this.leaveTimer); clearTimeout(this.touchTimer); clearTimeout(this.closeTimer); this.handleResize.clear(); } enterTimer = null; leaveTimer = null; touchTimer = null; closeTimer = null; isControlled = null; popper = null; children = null; ignoreNonTouchEvents = false; handleResize = debounce(() => { if (this.popper) { this.popper._popper.scheduleUpdate(); } }, 166); // Corresponds to 10 frames at 60 Hz. handleEnter = event => { const { children, enterDelay } = this.props; const childrenProps = children.props; if (event.type === 'focus' && childrenProps.onFocus) { childrenProps.onFocus(event); } if (event.type === 'mouseover' && childrenProps.onMouseOver) { childrenProps.onMouseOver(event); } if (this.ignoreNonTouchEvents && event.type !== 'touchstart') { return; } clearTimeout(this.enterTimer); clearTimeout(this.leaveTimer); if (enterDelay) { event.persist(); this.enterTimer = setTimeout(() => { this.handleOpen(event); }, enterDelay); } else { this.handleOpen(event); } }; handleOpen = event => { if (!this.isControlled) { this.setState({ open: true }); } if (this.props.onOpen) { this.props.onOpen(event, true); } }; handleLeave = event => { const { children, leaveDelay } = this.props; const childrenProps = children.props; if (event.type === 'blur' && childrenProps.onBlur) { childrenProps.onBlur(event); } if (event.type === 'mouseleave' && childrenProps.onMouseLeave) { childrenProps.onMouseLeave(event); } clearTimeout(this.enterTimer); clearTimeout(this.leaveTimer); if (leaveDelay) { event.persist(); this.leaveTimer = setTimeout(() => { this.handleClose(event); }, leaveDelay); } else { this.handleClose(event); } }; handleClose = event => { if (!this.isControlled) { this.setState({ open: false }); } if (this.props.onClose) { this.props.onClose(event, false); } clearTimeout(this.closeTimer); this.closeTimer = setTimeout(() => { this.ignoreNonTouchEvents = false; }, this.props.theme.transitions.duration.shortest); }; handleTouchStart = event => { this.ignoreNonTouchEvents = true; const { children, enterTouchDelay } = this.props; const childrenProps = children.props; if (childrenProps.onTouchStart) { childrenProps.onTouchStart(event); } clearTimeout(this.leaveTimer); clearTimeout(this.closeTimer); clearTimeout(this.touchTimer); event.persist(); this.touchTimer = setTimeout(() => { this.handleEnter(event); }, enterTouchDelay); }; handleTouchEnd = event => { const { children, leaveTouchDelay } = this.props; const childrenProps = children.props; if (childrenProps.onTouchEnd) { childrenProps.onTouchEnd(event); } clearTimeout(this.touchTimer); clearTimeout(this.leaveTimer); event.persist(); this.leaveTimer = setTimeout(() => { this.handleClose(event); }, leaveTouchDelay); }; render() { const { children, classes, className, disableFocusListener, disableHoverListener, disableTouchListener, enterDelay, enterTouchDelay, id, leaveDelay, leaveTouchDelay, onClose, onOpen, open: openProp, placement: placementProp, PopperProps: { className: PopperClassName, ...PopperProps } = {}, theme, title, ...other } = this.props; const placement = theme.direction === 'rtl' ? flipPlacement(placementProp) : placementProp; let open = this.isControlled ? openProp : this.state.open; const childrenProps = { 'aria-describedby': id }; // There is no point at displaying an empty tooltip. if (title === '') { open = false; } if (!disableTouchListener) { childrenProps.onTouchStart = this.handleTouchStart; childrenProps.onTouchEnd = this.handleTouchEnd; } if (!disableHoverListener) { childrenProps.onMouseOver = this.handleEnter; childrenProps.onMouseLeave = this.handleLeave; } if (!disableFocusListener) { childrenProps.onFocus = this.handleEnter; childrenProps.onBlur = this.handleLeave; } warning( !children.props.title, [ 'Material-UI: you have been providing a `title` property to the child of <Tooltip />.', `Remove this title property \`${children.props.title}\` or the Tooltip component.`, ].join('\n'), ); return ( <Manager tag={false} {...other}> <EventListener target="window" onResize={this.handleResize} /> <Target> {({ targetProps }) => ( <RootRef rootRef={node => { this.children = node; targetProps.ref(this.children); }} > {React.cloneElement(children, childrenProps)} </RootRef> )} </Target> <Portal> <Popper placement={placement} eventsEnabled={open} className={classNames(classes.popper, { [classes.open]: open }, PopperClassName)} ref={node => { this.popper = node; }} {...PopperProps} > {({ popperProps, restProps }) => { const actualPlacement = (popperProps['data-placement'] || placement).split('-')[0]; return ( <div {...popperProps} {...restProps} style={{ ...popperProps.style, top: popperProps.style.top || 0, left: popperProps.style.left || 0, ...restProps.style, }} > <div id={id} role="tooltip" aria-hidden={!open} className={classNames( classes.tooltip, { [classes.open]: open }, { [classes.touch]: this.ignoreNonTouchEvents }, classes[`tooltipPlacement${capitalize(actualPlacement)}`], )} > {title} </div> </div> ); }} </Popper> </Portal> </Manager> ); } } Tooltip.propTypes = { /** * Tooltip reference element. */ children: PropTypes.element.isRequired, /** * Override or extend the styles applied to the component. * See [CSS API](#css-api) below for more details. */ classes: PropTypes.object.isRequired, /** * @ignore */ className: PropTypes.string, /** * Do not respond to focus events. */ disableFocusListener: PropTypes.bool, /** * Do not respond to hover events. */ disableHoverListener: PropTypes.bool, /** * Do not respond to long press touch events. */ disableTouchListener: PropTypes.bool, /** * The number of milliseconds to wait before showing the tooltip. * This property won't impact the enter touch delay (`enterTouchDelay`). */ enterDelay: PropTypes.number, /** * The number of milliseconds a user must touch the element before showing the tooltip. */ enterTouchDelay: PropTypes.number, /** * The relationship between the tooltip and the wrapper component is not clear from the DOM. * By providing this property, we can use aria-describedby to solve the accessibility issue. */ id: PropTypes.string, /** * The number of milliseconds to wait before hiding the tooltip. * This property won't impact the leave touch delay (`leaveTouchDelay`). */ leaveDelay: PropTypes.number, /** * The number of milliseconds after the user stops touching an element before hiding the tooltip. */ leaveTouchDelay: PropTypes.number, /** * Callback fired when the tooltip requests to be closed. * * @param {object} event The event source of the callback */ onClose: PropTypes.func, /** * Callback fired when the tooltip requests to be open. * * @param {object} event The event source of the callback */ onOpen: PropTypes.func, /** * If `true`, the tooltip is shown. */ open: PropTypes.bool, /** * Tooltip placement */ placement: PropTypes.oneOf([ 'bottom-end', 'bottom-start', 'bottom', 'left-end', 'left-start', 'left', 'right-end', 'right-start', 'right', 'top-end', 'top-start', 'top', ]), /** * Properties applied to the `Popper` element. */ PopperProps: PropTypes.object, /** * @ignore */ theme: PropTypes.object.isRequired, /** * Tooltip title. Zero-length titles string are never displayed. */ title: PropTypes.node.isRequired, }; Tooltip.defaultProps = { disableFocusListener: false, disableHoverListener: false, disableTouchListener: false, enterDelay: 0, enterTouchDelay: 1000, leaveDelay: 0, leaveTouchDelay: 1500, placement: 'bottom', }; export default withStyles(styles, { name: 'MuiTooltip', withTheme: true })(Tooltip);