UNPKG

react-toolbox

Version:

A set of React components implementing Google's Material Design specification with the power of CSS Modules.

236 lines (211 loc) 7.63 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { themr } from 'react-css-themr'; import Portal from '../hoc/Portal'; import { getViewport } from '../utils/utils'; import { TOOLTIP } from '../identifiers'; import events from '../utils/events'; const POSITION = { BOTTOM: 'bottom', HORIZONTAL: 'horizontal', LEFT: 'left', RIGHT: 'right', TOP: 'top', VERTICAL: 'vertical', }; const defaults = { className: '', delay: 0, hideOnClick: true, passthrough: true, showOnClick: false, position: POSITION.VERTICAL, theme: {}, }; const tooltipFactory = (options = {}) => { const { className: defaultClassName, delay: defaultDelay, hideOnClick: defaultHideOnClick, showOnClick: defaultShowOnClick, passthrough: defaultPassthrough, position: defaultPosition, theme: defaultTheme, } = { ...defaults, ...options }; return (ComposedComponent) => { class TooltippedComponent extends Component { static propTypes = { children: PropTypes.node, className: PropTypes.string, onClick: PropTypes.func, onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, theme: PropTypes.shape({ tooltip: PropTypes.string, tooltipActive: PropTypes.string, tooltipWrapper: PropTypes.string, }), tooltip: PropTypes.oneOfType([ PropTypes.string, PropTypes.node, ]), tooltipDelay: PropTypes.number, tooltipHideOnClick: PropTypes.bool, tooltipPosition: PropTypes.oneOf(Object.keys(POSITION).map(key => POSITION[key])), tooltipShowOnClick: PropTypes.bool, }; static defaultProps = { className: defaultClassName, tooltipDelay: defaultDelay, tooltipHideOnClick: defaultHideOnClick, tooltipPosition: defaultPosition, tooltipShowOnClick: defaultShowOnClick, }; state = { active: false, position: this.props.tooltipPosition, visible: false, }; componentWillUnmount() { if (this.tooltipNode) { events.removeEventListenerOnTransitionEnded(this.tooltipNode, this.onTransformEnd); } if (this.timeout) clearTimeout(this.timeout); } onTransformEnd = (e) => { if (e.propertyName === 'transform') { events.removeEventListenerOnTransitionEnded(this.tooltipNode, this.onTransformEnd); this.setState({ visible: false }); } }; getPosition(element) { const { tooltipPosition } = this.props; if (tooltipPosition === POSITION.HORIZONTAL) { const origin = element.getBoundingClientRect(); const { width: ww } = getViewport(); const toRight = origin.left < ((ww / 2) - (origin.width / 2)); return toRight ? POSITION.RIGHT : POSITION.LEFT; } else if (tooltipPosition === POSITION.VERTICAL) { const origin = element.getBoundingClientRect(); const { height: wh } = getViewport(); const toBottom = origin.top < ((wh / 2) - (origin.height / 2)); return toBottom ? POSITION.BOTTOM : POSITION.TOP; } return tooltipPosition; } activate({ top, left, position }) { if (this.timeout) clearTimeout(this.timeout); this.setState({ visible: true, position }); this.timeout = setTimeout(() => { this.setState({ active: true, top, left }); }, this.props.tooltipDelay); } deactivate() { if (this.timeout) clearTimeout(this.timeout); if (this.state.active) { events.addEventListenerOnTransitionEnded(this.tooltipNode, this.onTransformEnd); this.setState({ active: false }); } else if (this.state.visible) { this.setState({ visible: false }); } } calculatePosition(element) { const position = this.getPosition(element); const { top, left, height, width } = element.getBoundingClientRect(); const xOffset = window.scrollX || window.pageXOffset; const yOffset = window.scrollY || window.pageYOffset; if (position === POSITION.BOTTOM) { return { top: top + height + yOffset, left: left + (width / 2) + xOffset, position, }; } else if (position === POSITION.TOP) { return { top: top + yOffset, left: left + (width / 2) + xOffset, position, }; } else if (position === POSITION.LEFT) { return { top: top + (height / 2) + yOffset, left: left + xOffset, position, }; } else if (position === POSITION.RIGHT) { return { top: top + (height / 2) + yOffset, left: left + width + xOffset, position, }; } return undefined; } handleMouseEnter = (event) => { this.activate(this.calculatePosition(event.currentTarget)); if (this.props.onMouseEnter) this.props.onMouseEnter(event); }; handleMouseLeave = (event) => { this.deactivate(); if (this.props.onMouseLeave) this.props.onMouseLeave(event); }; handleClick = (event) => { if (this.props.tooltipHideOnClick && this.state.active) { this.deactivate(); } if (this.props.tooltipShowOnClick && !this.state.active) { this.activate(this.calculatePosition(event.currentTarget)); } if (this.props.onClick) this.props.onClick(event); }; render() { const { active, left, top, position, visible } = this.state; const positionClass = `tooltip${position.charAt(0).toUpperCase() + position.slice(1)}`; const { children, className, theme, onClick, // eslint-disable-line no-unused-vars onMouseEnter, // eslint-disable-line no-unused-vars onMouseLeave, // eslint-disable-line no-unused-vars tooltip, tooltipDelay, // eslint-disable-line no-unused-vars tooltipHideOnClick, // eslint-disable-line no-unused-vars tooltipPosition, // eslint-disable-line no-unused-vars tooltipShowOnClick, // eslint-disable-line no-unused-vars ...other } = this.props; const _className = classnames(theme.tooltip, { [theme.tooltipActive]: active, [theme[positionClass]]: theme[positionClass], }); const childProps = { ...other, className, onClick: this.handleClick, onMouseEnter: this.handleMouseEnter, onMouseLeave: this.handleMouseLeave, }; const shouldPass = typeof ComposedComponent !== 'string' && defaultPassthrough; const finalProps = shouldPass ? { ...childProps, theme } : childProps; return React.createElement(ComposedComponent, finalProps, children, visible && ( <Portal> <span ref={(node) => { this.tooltipNode = node; }} className={_className} data-react-toolbox="tooltip" style={{ top, left }} > <span className={theme.tooltipInner}>{tooltip}</span> </span> </Portal> ), ); } } return themr(TOOLTIP, defaultTheme)(TooltippedComponent); }; }; export default tooltipFactory;