import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Manager, Popper, Reference } from 'react-popper'; import classNames from 'classnames'; import { isMobile as isMobileUtil } from 'Utilities/mobile'; import { kinds, tooltipPositions } from 'Helpers/Props'; import Portal from 'Components/Portal'; import dimensions from 'Styles/Variables/dimensions'; import styles from './Tooltip.css'; let maxWidth = null; function getMaxWidth() { const windowWidth = window.innerWidth; if (windowWidth >= parseInt(dimensions.breakpointLarge)) { maxWidth = 800; } else if (windowWidth >= parseInt(dimensions.breakpointMedium)) { maxWidth = 650; } else if (windowWidth >= parseInt(dimensions.breakpointSmall)) { maxWidth = 500; } else { maxWidth = 450; } return maxWidth; } class Tooltip extends Component { // // Lifecycle constructor(props, context) { super(props, context); this._scheduleUpdate = null; this._closeTimeout = null; this._maxWidth = maxWidth || getMaxWidth(); this.state = { isOpen: false }; } componentDidUpdate() { if (this._scheduleUpdate && this.state.isOpen) { this._scheduleUpdate(); } } componentWillUnmount() { if (this._closeTimeout) { this._closeTimeout = clearTimeout(this._closeTimeout); } } // // Control computeMaxSize = (data) => { const { top, right, bottom, left } = data.offsets.reference; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; if ((/^top/).test(data.placement)) { data.styles.maxHeight = top - 20; } else if ((/^bottom/).test(data.placement)) { data.styles.maxHeight = windowHeight - bottom - 20; } else if ((/^right/).test(data.placement)) { data.styles.maxWidth = Math.min(this._maxWidth, windowWidth - right - 20); data.styles.maxHeight = top - 20; } else { data.styles.maxWidth = Math.min(this._maxWidth, left - 20); data.styles.maxHeight = top - 20; } return data; } // // Listeners onMeasure = ({ width }) => { this.setState({ width }); } onClick = () => { if (isMobileUtil()) { this.setState({ isOpen: !this.state.isOpen }); } } onMouseEnter = () => { if (this._closeTimeout) { this._closeTimeout = clearTimeout(this._closeTimeout); } this.setState({ isOpen: true }); } onMouseLeave = () => { this._closeTimeout = setTimeout(() => { this.setState({ isOpen: false }); }, 100); } // // Render render() { const { className, bodyClassName, anchor, tooltip, kind, position, canFlip } = this.props; return ( <Manager> <Reference> {({ ref }) => ( <span ref={ref} className={className} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} > {anchor} </span> )} </Reference> <Portal> <Popper placement={position} // Disable events to improve performance when many tooltips // are shown (Quality Definitions for example). eventsEnabled={false} modifiers={{ computeMaxHeight: { order: 851, enabled: true, fn: this.computeMaxSize }, preventOverflow: { // Fixes positioning for tooltips in the queue // and likely others. escapeWithReference: false }, flip: { enabled: canFlip } }} > {({ ref, style, placement, arrowProps, scheduleUpdate }) => { this._scheduleUpdate = scheduleUpdate; const popperPlacement = placement ? placement.split('-')[0] : position; const vertical = popperPlacement === 'top' || popperPlacement === 'bottom'; return ( <div ref={ref} className={classNames( styles.tooltipContainer, vertical ? styles.verticalContainer : styles.horizontalContainer )} style={style} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} > <div className={this.state.isOpen ? classNames( styles.arrow, styles[kind], styles[popperPlacement] ) : styles.arrowDisabled} ref={arrowProps.ref} style={arrowProps.style} /> { this.state.isOpen ? <div className={classNames( styles.tooltip, styles[kind] )} > <div className={bodyClassName} > {tooltip} </div> </div> : null } </div> ); }} </Popper> </Portal> </Manager> ); } } Tooltip.propTypes = { className: PropTypes.string, bodyClassName: PropTypes.string.isRequired, anchor: PropTypes.node.isRequired, tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]), position: PropTypes.oneOf(tooltipPositions.all), canFlip: PropTypes.bool.isRequired }; Tooltip.defaultProps = { bodyClassName: styles.body, kind: kinds.DEFAULT, position: tooltipPositions.TOP, canFlip: false }; export default Tooltip;