2017-07-21 18:59:17 +00:00
|
|
|
// @flow
|
|
|
|
import React from 'react';
|
2017-03-28 22:45:23 +00:00
|
|
|
import autobind from 'autobind-decorator';
|
|
|
|
import classnames from 'classnames';
|
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
|
|
|
|
@autobind
|
2017-07-21 18:59:17 +00:00
|
|
|
class Tooltip extends React.PureComponent {
|
|
|
|
props: {
|
|
|
|
children: React.Children,
|
2017-08-11 21:44:34 +00:00
|
|
|
message: React.Children | string,
|
|
|
|
position: 'bottom' | 'top' | 'right' | 'left',
|
2017-07-21 18:59:17 +00:00
|
|
|
|
|
|
|
// Optional
|
|
|
|
className?: string,
|
|
|
|
delay?: number
|
|
|
|
};
|
|
|
|
|
|
|
|
state: {
|
|
|
|
visible: boolean
|
|
|
|
};
|
|
|
|
|
|
|
|
_showTimeout: number;
|
|
|
|
|
|
|
|
// TODO: Figure out what type these should be
|
|
|
|
_tooltip: any;
|
|
|
|
_bubble: any;
|
|
|
|
|
|
|
|
constructor (props: any) {
|
2017-03-28 22:45:23 +00:00
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
left: null,
|
|
|
|
top: null,
|
|
|
|
bottom: null,
|
|
|
|
right: null,
|
|
|
|
maxWidth: null,
|
|
|
|
maxHeight: null,
|
|
|
|
visible: false
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
_setTooltipRef (n: React.Element<*>): void {
|
2017-03-28 22:45:23 +00:00
|
|
|
this._tooltip = n;
|
|
|
|
}
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
_setBubbleRef (n: React.Element<*>): void {
|
2017-03-28 22:45:23 +00:00
|
|
|
this._bubble = n;
|
|
|
|
}
|
|
|
|
|
2017-07-27 22:59:07 +00:00
|
|
|
_handleClick (e: MouseEvent): void {
|
|
|
|
e.stopPropagation();
|
|
|
|
}
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
_handleMouseEnter (e: MouseEvent): void {
|
|
|
|
this._showTimeout = setTimeout((): void => {
|
2017-03-28 22:45:23 +00:00
|
|
|
const tooltip = ReactDOM.findDOMNode(this._tooltip);
|
|
|
|
const bubble = ReactDOM.findDOMNode(this._bubble);
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
if (!tooltip || !(tooltip instanceof HTMLDivElement)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!bubble || !(bubble instanceof HTMLDivElement)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-03-28 22:45:23 +00:00
|
|
|
const tooltipRect = tooltip.getBoundingClientRect();
|
|
|
|
const bubbleRect = bubble.getBoundingClientRect();
|
2017-07-21 20:11:01 +00:00
|
|
|
const margin = 3;
|
2017-03-28 22:45:23 +00:00
|
|
|
|
|
|
|
switch (this.props.position) {
|
|
|
|
case 'right':
|
2017-07-21 18:59:17 +00:00
|
|
|
bubble.style.top = `${tooltipRect.top - (bubbleRect.height / 2) + (tooltipRect.height / 2)}px`;
|
2017-07-21 20:11:01 +00:00
|
|
|
bubble.style.left = `${tooltipRect.left + tooltipRect.width + margin}px`;
|
2017-03-28 22:45:23 +00:00
|
|
|
break;
|
|
|
|
|
2017-03-29 23:09:28 +00:00
|
|
|
case 'bottom':
|
2017-07-21 20:11:01 +00:00
|
|
|
bubble.style.top = `${tooltipRect.top + tooltipRect.height + margin}px`;
|
2017-07-21 18:59:17 +00:00
|
|
|
bubble.style.left = `${tooltipRect.left - (bubbleRect.width / 2) + (tooltipRect.width / 2)}px`;
|
2017-03-29 23:09:28 +00:00
|
|
|
break;
|
|
|
|
|
2017-03-28 22:45:23 +00:00
|
|
|
case 'top':
|
|
|
|
default:
|
2017-07-21 20:11:01 +00:00
|
|
|
bubble.style.top = `${tooltipRect.top - bubbleRect.height - margin}px`;
|
2017-07-21 18:59:17 +00:00
|
|
|
bubble.style.left = `${tooltipRect.left - (bubbleRect.width / 2) + (tooltipRect.width / 2)}px`;
|
2017-03-28 22:45:23 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
this.setState({visible: true});
|
2017-03-28 22:45:23 +00:00
|
|
|
}, this.props.delay || 100);
|
|
|
|
}
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
_handleMouseLeave (): void {
|
2017-03-28 22:45:23 +00:00
|
|
|
clearTimeout(this._showTimeout);
|
2017-07-21 18:59:17 +00:00
|
|
|
this.setState({visible: false});
|
2017-07-21 20:11:01 +00:00
|
|
|
|
|
|
|
const bubble = ReactDOM.findDOMNode(this._bubble);
|
|
|
|
if (!bubble || !(bubble instanceof HTMLDivElement)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset positioning stuff
|
|
|
|
bubble.style.left = '';
|
|
|
|
bubble.style.top = '';
|
|
|
|
bubble.style.bottom = '';
|
|
|
|
bubble.style.right = '';
|
2017-03-28 22:45:23 +00:00
|
|
|
}
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
_getContainer (): HTMLElement {
|
|
|
|
let container = document.querySelector('#tooltips-container');
|
|
|
|
if (!container) {
|
|
|
|
container = document.createElement('div');
|
|
|
|
container.id = 'tooltips-container';
|
2017-03-28 22:45:23 +00:00
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
document.body && document.body.appendChild(container);
|
|
|
|
}
|
2017-03-28 22:45:23 +00:00
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
return container;
|
|
|
|
}
|
2017-03-28 22:45:23 +00:00
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
componentDidMount () {
|
|
|
|
// Move the element to the body so we can position absolutely
|
|
|
|
if (this._bubble) {
|
|
|
|
const el = ReactDOM.findDOMNode(this._bubble);
|
|
|
|
el && this._getContainer().appendChild(el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-27 22:59:07 +00:00
|
|
|
componentWillUnmount () {
|
|
|
|
// Remove the element from the body
|
|
|
|
if (this._bubble) {
|
|
|
|
const el = ReactDOM.findDOMNode(this._bubble);
|
|
|
|
el && this._getContainer().removeChild(el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-21 18:59:17 +00:00
|
|
|
render () {
|
|
|
|
const {children, message, className} = this.props;
|
|
|
|
const {visible} = this.state;
|
|
|
|
|
|
|
|
const tooltipClasses = classnames(className, 'tooltip');
|
|
|
|
const bubbleClasses = classnames('tooltip__bubble', {
|
|
|
|
'tooltip__bubble--visible': visible
|
|
|
|
});
|
2017-03-28 22:45:23 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={tooltipClasses}
|
|
|
|
ref={this._setTooltipRef}
|
2017-07-27 22:59:07 +00:00
|
|
|
onClick={this._handleClick}
|
2017-03-28 22:45:23 +00:00
|
|
|
onMouseEnter={this._handleMouseEnter}
|
|
|
|
onMouseLeave={this._handleMouseLeave}>
|
2017-07-21 18:59:17 +00:00
|
|
|
<div className={bubbleClasses} ref={this._setBubbleRef}>
|
2017-03-28 22:45:23 +00:00
|
|
|
{message}
|
|
|
|
</div>
|
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Tooltip;
|