materialuiupgraded
Version:
Material-UI's workspace package
460 lines (416 loc) • 13.2 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import warning from 'warning';
import classNames from 'classnames';
import RootRef from '../RootRef';
import withStyles from '../styles/withStyles';
import { capitalize } from '../utils/helpers';
import Grow from '../Grow';
import Popper from '../Popper';
export const styles = theme => ({
/* Styles applied to the Popper component. */
popper: {
zIndex: theme.zIndex.tooltip,
opacity: 0.9,
},
/* Styles applied to the tooltip (label wrapper) element. */
tooltip: {
backgroundColor: theme.palette.grey[700],
borderRadius: theme.shape.borderRadius,
color: theme.palette.common.white,
fontFamily: theme.typography.fontFamily,
padding: '4px 8px',
fontSize: theme.typography.pxToRem(10),
lineHeight: `${theme.typography.round(14 / 10)}em`,
maxWidth: 300,
},
/* Styles applied to the tooltip (label wrapper) element if the tooltip is opened by touch. */
touch: {
padding: '8px 16px',
fontSize: theme.typography.pxToRem(14),
lineHeight: `${theme.typography.round(16 / 14)}em`,
},
/* Styles applied to the tooltip (label wrapper) element if `placement` contains "left". */
tooltipPlacementLeft: {
transformOrigin: 'right center',
margin: '0 24px ',
[theme.breakpoints.up('sm')]: {
margin: '0 14px',
},
},
/* Styles applied to the tooltip (label wrapper) element if `placement` contains "right". */
tooltipPlacementRight: {
transformOrigin: 'left center',
margin: '0 24px',
[theme.breakpoints.up('sm')]: {
margin: '0 14px',
},
},
/* Styles applied to the tooltip (label wrapper) element if `placement` contains "top". */
tooltipPlacementTop: {
transformOrigin: 'center bottom',
margin: '24px 0',
[theme.breakpoints.up('sm')]: {
margin: '14px 0',
},
},
/* Styles applied to the tooltip (label wrapper) element if `placement` contains "bottom". */
tooltipPlacementBottom: {
transformOrigin: 'center top',
margin: '24px 0',
[theme.breakpoints.up('sm')]: {
margin: '14px 0',
},
},
});
class Tooltip extends React.Component {
ignoreNonTouchEvents = false;
constructor(props) {
super();
this.isControlled = props.open != null;
this.state = {
open: null,
};
if (!this.isControlled) {
// not controlled, use internal state
this.state.open = false;
}
}
componentDidMount() {
warning(
!this.childrenRef.disabled || !this.childrenRef.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'),
);
// Fallback to this default id when possible.
// Use the random value for client side rendering only.
// We can't use it server side.
this.defaultId = `mui-tooltip-${Math.round(Math.random() * 1e5)}`;
// Rerender with this.defaultId and this.childrenRef.
if (this.props.open) {
this.forceUpdate();
}
}
componentWillUnmount() {
clearTimeout(this.closeTimer);
clearTimeout(this.enterTimer);
clearTimeout(this.focusTimer);
clearTimeout(this.leaveTimer);
clearTimeout(this.touchTimer);
}
onRootRef = ref => {
this.childrenRef = ref;
};
handleFocus = event => {
event.persist();
// The autoFocus of React might trigger the event before the componentDidMount.
// We need to account for this eventuality.
this.focusTimer = setTimeout(() => {
// We need to make sure the focus hasn't moved since the event was triggered.
if (this.childrenRef === document.activeElement) {
this.handleEnter(event);
}
}, 0);
const childrenProps = this.props.children.props;
if (childrenProps.onFocus) {
childrenProps.onFocus(event);
}
};
handleEnter = event => {
const { children, enterDelay } = this.props;
const childrenProps = children.props;
if (event.type === 'mouseover' && childrenProps.onMouseOver) {
childrenProps.onMouseOver(event);
}
if (this.ignoreNonTouchEvents && event.type !== 'touchstart') {
return;
}
// Remove the title ahead of time.
// We don't want to wait for the next render commit.
// We would risk displaying two tooltips at the same time (native + this one).
this.childrenRef.setAttribute('title', '');
clearTimeout(this.enterTimer);
clearTimeout(this.leaveTimer);
if (enterDelay) {
event.persist();
this.enterTimer = setTimeout(() => {
this.handleOpen(event);
}, enterDelay);
} else {
this.handleOpen(event);
}
};
handleOpen = event => {
// The mouseover event will trigger for every nested element in the tooltip.
// We can skip rerendering when the tooltip is already open.
// We are using the mouseover event instead of the mouseenter event to fix a hide/show issue.
if (!this.isControlled && !this.state.open) {
this.setState({ open: true });
}
if (this.props.onOpen) {
this.props.onOpen(event);
}
};
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);
}
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;
if (children.props.onTouchStart) {
children.props.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;
if (children.props.onTouchEnd) {
children.props.onTouchEnd(event);
}
clearTimeout(this.touchTimer);
clearTimeout(this.leaveTimer);
event.persist();
this.leaveTimer = setTimeout(() => {
this.handleClose(event);
}, leaveTouchDelay);
};
render() {
const {
children,
classes,
disableFocusListener,
disableHoverListener,
disableTouchListener,
enterDelay,
enterTouchDelay,
id,
leaveDelay,
leaveTouchDelay,
onClose,
onOpen,
open: openProp,
placement,
PopperProps,
theme,
title,
TransitionComponent,
TransitionProps,
...other
} = this.props;
let open = this.isControlled ? openProp : this.state.open;
// There is no point at displaying an empty tooltip.
if (title === '') {
open = false;
}
const childrenProps = {
'aria-describedby': open ? id || this.defaultId : null,
title: !open && typeof title === 'string' ? title : null,
...other,
};
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.handleFocus;
childrenProps.onBlur = this.handleLeave;
}
warning(
!children.props.title,
[
'Material-UI: you have provided a `title` property to the child of <Tooltip />.',
`Remove this title property \`${children.props.title}\` or the Tooltip component.`,
].join('\n'),
);
return (
<React.Fragment>
<RootRef rootRef={this.onRootRef}>{React.cloneElement(children, childrenProps)}</RootRef>
<Popper
className={classes.popper}
placement={placement}
anchorEl={this.childrenRef}
open={open}
id={childrenProps['aria-describedby']}
transition
{...PopperProps}
>
{({ placement: placementInner, TransitionProps: TransitionPropsInner }) => (
<TransitionComponent
timeout={theme.transitions.duration.shorter}
{...TransitionPropsInner}
{...TransitionProps}
>
<div
className={classNames(
classes.tooltip,
{
[classes.touch]: this.ignoreNonTouchEvents,
},
classes[`tooltipPlacement${capitalize(placementInner.split('-')[0])}`],
)}
>
{title}
</div>
</TransitionComponent>
)}
</Popper>
</React.Fragment>
);
}
}
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,
/**
* 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.
* This property is used with aria-describedby to solve the accessibility issue.
* If you don't provide this property. It fallback to a random generated id.
*/
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`](/api/popper/) element.
*/
PopperProps: PropTypes.object,
/**
* @ignore
*/
theme: PropTypes.object.isRequired,
/**
* Tooltip title. Zero-length titles string are never displayed.
*/
title: PropTypes.node.isRequired,
/**
* Transition component.
*/
TransitionComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
/**
* Properties applied to the `Transition` element.
*/
TransitionProps: PropTypes.object,
};
Tooltip.defaultProps = {
disableFocusListener: false,
disableHoverListener: false,
disableTouchListener: false,
enterDelay: 0,
enterTouchDelay: 1000,
leaveDelay: 0,
leaveTouchDelay: 1500,
placement: 'bottom',
TransitionComponent: Grow,
};
export default withStyles(styles, { name: 'MuiTooltip', withTheme: true })(Tooltip);