UNPKG

@material-ui/core

Version:

React components that implement Google's Material Design.

476 lines (417 loc) 13.4 kB
import _extends from "@babel/runtime/helpers/builtin/extends"; import _objectSpread from "@babel/runtime/helpers/builtin/objectSpread"; import _objectWithoutProperties from "@babel/runtime/helpers/builtin/objectWithoutProperties"; /* 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, Reference } 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) { super(props); this.state = {}; this.enterTimer = null; this.leaveTimer = null; this.touchTimer = null; this.closeTimer = null; this.isControlled = null; this.scheduleUpdate = null; this.children = null; this.ignoreNonTouchEvents = false; this.handleResize = debounce(() => { if (this.scheduleUpdate) { this.scheduleUpdate(); } }, 166); this.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); } }; this.handleOpen = event => { if (!this.isControlled) { this.setState({ open: true }); } if (this.props.onOpen) { this.props.onOpen(event, true); } }; this.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); } }; this.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); }; this.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); }; this.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); }; this.isControlled = props.open != null; if (!this.isControlled) { // not controlled, use internal state this.state.open = false; } } componentDidMount() { process.env.NODE_ENV !== "production" ? 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')) : void 0; } componentWillUnmount() { clearTimeout(this.enterTimer); clearTimeout(this.leaveTimer); clearTimeout(this.touchTimer); clearTimeout(this.closeTimer); this.handleResize.clear(); } render() { const _props = this.props, { children, classes, className, disableFocusListener, disableHoverListener, disableTouchListener, enterDelay, enterTouchDelay, id, leaveDelay, leaveTouchDelay, onClose, onOpen, open: openProp, placement: placementProp, PopperProps: { className: PopperClassName } = {}, theme, title } = _props, PopperProps = _objectWithoutProperties(_props.PopperProps, ["className"]), other = _objectWithoutProperties(_props, ["children", "classes", "className", "disableFocusListener", "disableHoverListener", "disableTouchListener", "enterDelay", "enterTouchDelay", "id", "leaveDelay", "leaveTouchDelay", "onClose", "onOpen", "open", "placement", "PopperProps", "theme", "title"]); 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; } process.env.NODE_ENV !== "production" ? 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')) : void 0; return React.createElement(Manager, other, React.createElement(EventListener, { target: "window", onResize: this.handleResize }), React.createElement(Reference, null, ({ ref }) => React.createElement(RootRef, { rootRef: node => { this.children = node; ref(this.children); } }, React.cloneElement(children, childrenProps))), React.createElement(Portal, null, React.createElement(Popper, _extends({ placement: placement, eventsEnabled: open, className: classNames(classes.popper, { [classes.open]: open }, PopperClassName) }, PopperProps), popperProps => { this.scheduleUpdate = popperProps.scheduleUpdate; const actualPlacement = popperProps.placement ? popperProps.placement.split('-')[0] : null; return React.createElement("div", { ref: popperProps.ref, style: _objectSpread({}, popperProps.style, { top: popperProps.style.top || 0, left: popperProps.style.left || 0 }) }, React.createElement("div", { id: id, role: "tooltip", "aria-hidden": !open, className: classNames(classes.tooltip, { [classes.open]: open, [classes.touch]: this.ignoreNonTouchEvents }, popperProps.placement ? classes[`tooltipPlacement${capitalize(actualPlacement)}`] : null) }, title)); }))); } } Tooltip.propTypes = process.env.NODE_ENV !== "production" ? { /** * 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);