Summary:
Basic tooltips available. Use is:
<Tooltip
title="This is what will show up inside the tooltip"
options={{ // can include any or none of these (if not included default will be used)
position, // 'above', 'below', 'toRight', or 'toLeft'
showTail, // whether or not tooltip should have tail
delay, // how long to wait on hover before showing tooltip
// supported css properties
backgroundColor,
color,
maxWidth,
width,
borderRadius,
padding,
lineHeight,
}}>
<ElementTooltipWillShowUpFor/>
</Tooltip>
Reviewed By: danielbuechele
Differential Revision: D9596287
fbshipit-source-id: 233b1ad01b96264bbc1f62f3798e3d69d1ab4bae
254 lines
6.1 KiB
JavaScript
254 lines
6.1 KiB
JavaScript
/**
|
|
* Copyright 2018-present Facebook.
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
* @format
|
|
*/
|
|
|
|
import styled from '../styled/index.js';
|
|
import {colors} from './colors.js';
|
|
import {Component} from 'react';
|
|
|
|
const PropTypes = require('prop-types');
|
|
|
|
const defaultOptions = {
|
|
backgroundColor: colors.blueGrey,
|
|
position: 'below',
|
|
color: colors.white,
|
|
showTail: true,
|
|
maxWidth: '200px',
|
|
width: 'auto',
|
|
borderRadius: 4,
|
|
padding: '6px',
|
|
lineHeight: '20px',
|
|
delay: 0,
|
|
};
|
|
|
|
export type TooltipOptions = {
|
|
backgroundColor?: string,
|
|
position?: 'below' | 'above' | 'toRight' | 'toLeft',
|
|
color?: string,
|
|
showTail?: boolean,
|
|
maxWidth?: string,
|
|
width?: string,
|
|
borderRadius?: number,
|
|
padding?: string,
|
|
lineHeight?: string,
|
|
delay?: number, // in milliseconds
|
|
};
|
|
|
|
const TooltipBubble = styled('div')(props => ({
|
|
position: 'absolute',
|
|
zIndex: 99999999999,
|
|
backgroundColor: props.options.backgroundColor,
|
|
lineHeight: props.options.lineHeight,
|
|
padding: props.options.padding,
|
|
borderRadius: props.options.borderRadius,
|
|
width: props.options.width,
|
|
maxWidth: props.options.maxWidth,
|
|
top: props.top,
|
|
left: props.left,
|
|
bottom: props.bottom,
|
|
right: props.right,
|
|
color: props.options.color,
|
|
}));
|
|
|
|
// vertical offset on bubble when position is 'below'
|
|
const BUBBLE_BELOW_POSITION_VERTICAL_OFFSET = -10;
|
|
// horizontal offset on bubble when position is 'toLeft' or 'toRight'
|
|
const BUBBLE_LR_POSITION_HORIZONTAL_OFFSET = 5;
|
|
// offset on bubble when tail is showing
|
|
const BUBBLE_SHOWTAIL_OFFSET = 5;
|
|
// horizontal offset on tail when position is 'above' or 'below'
|
|
const TAIL_AB_POSITION_HORIZONTAL_OFFSET = 15;
|
|
// vertical offset on tail when position is 'toLeft' or 'toRight'
|
|
const TAIL_LR_POSITION_HORIZONTAL_OFFSET = 5;
|
|
|
|
const TooltipTail = styled('div')(props => ({
|
|
position: 'absolute',
|
|
display: 'block',
|
|
whiteSpace: 'pre',
|
|
height: '10px',
|
|
width: '10px',
|
|
lineHeight: '0',
|
|
zIndex: 99999999998,
|
|
transform: 'rotate(45deg)',
|
|
backgroundColor: props.options.backgroundColor,
|
|
top: props.top,
|
|
left: props.left,
|
|
bottom: props.bottom,
|
|
right: props.right,
|
|
}));
|
|
|
|
type TooltipProps = {
|
|
children: React$Node,
|
|
};
|
|
|
|
type TooltipObject = {
|
|
rect: ClientRect,
|
|
title: React$Node,
|
|
options: TooltipOptions,
|
|
};
|
|
|
|
type TooltipState = {
|
|
tooltip: ?TooltipObject,
|
|
timeoutID: ?TimeoutID,
|
|
};
|
|
|
|
export default class TooltipProvider extends Component<
|
|
TooltipProps,
|
|
TooltipState,
|
|
> {
|
|
static childContextTypes = {
|
|
TOOLTIP_PROVIDER: PropTypes.object,
|
|
};
|
|
|
|
state = {
|
|
tooltip: null,
|
|
timeoutID: undefined,
|
|
};
|
|
|
|
getChildContext() {
|
|
return {TOOLTIP_PROVIDER: this};
|
|
}
|
|
|
|
open(container: HTMLDivElement, title: React$Node, options: TooltipOptions) {
|
|
const node = container.childNodes[0];
|
|
if (node == null || !(node instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
if (options.delay) {
|
|
this.state.timeoutID = setTimeout(() => {
|
|
this.setState({
|
|
tooltip: {
|
|
rect: node.getBoundingClientRect(),
|
|
title,
|
|
options: options,
|
|
},
|
|
});
|
|
}, options.delay);
|
|
return;
|
|
}
|
|
|
|
this.setState({
|
|
tooltip: {
|
|
rect: node.getBoundingClientRect(),
|
|
title,
|
|
options: options,
|
|
},
|
|
});
|
|
}
|
|
|
|
close() {
|
|
if (this.state.timeoutID) {
|
|
clearTimeout(this.state.timeoutID);
|
|
}
|
|
this.setState({tooltip: null});
|
|
}
|
|
|
|
getTooltipTail(tooltip: TooltipObject) {
|
|
const opts = Object.assign(defaultOptions, tooltip.options);
|
|
if (!opts.showTail) {
|
|
return null;
|
|
}
|
|
|
|
let left = 'auto';
|
|
let top = 'auto';
|
|
let bottom = 'auto';
|
|
let right = 'auto';
|
|
|
|
if (opts.position === 'below') {
|
|
left = tooltip.rect.left + TAIL_AB_POSITION_HORIZONTAL_OFFSET;
|
|
top = tooltip.rect.bottom;
|
|
} else if (opts.position === 'above') {
|
|
left = tooltip.rect.left + TAIL_AB_POSITION_HORIZONTAL_OFFSET;
|
|
bottom = window.innerHeight - tooltip.rect.top;
|
|
} else if (opts.position === 'toRight') {
|
|
left = tooltip.rect.right + TAIL_LR_POSITION_HORIZONTAL_OFFSET;
|
|
top = tooltip.rect.top;
|
|
} else if (opts.position === 'toLeft') {
|
|
right =
|
|
window.innerWidth -
|
|
tooltip.rect.left +
|
|
TAIL_LR_POSITION_HORIZONTAL_OFFSET;
|
|
top = tooltip.rect.top;
|
|
}
|
|
|
|
return (
|
|
<TooltipTail
|
|
key="tail"
|
|
top={top}
|
|
left={left}
|
|
bottom={bottom}
|
|
right={right}
|
|
options={opts}
|
|
/>
|
|
);
|
|
}
|
|
|
|
getTooltipBubble(tooltip: TooltipObject) {
|
|
const opts = Object.assign(defaultOptions, tooltip.options);
|
|
let left = 'auto';
|
|
let top = 'auto';
|
|
let bottom = 'auto';
|
|
let right = 'auto';
|
|
|
|
if (opts.position === 'below') {
|
|
left = tooltip.rect.left;
|
|
top = tooltip.rect.bottom;
|
|
if (opts.showTail) {
|
|
top += BUBBLE_SHOWTAIL_OFFSET;
|
|
}
|
|
} else if (opts.position === 'above') {
|
|
bottom = window.innerHeight - tooltip.rect.top;
|
|
if (opts.showTail) {
|
|
bottom += BUBBLE_SHOWTAIL_OFFSET;
|
|
}
|
|
left = tooltip.rect.left;
|
|
} else if (opts.position === 'toRight') {
|
|
left = tooltip.rect.right + BUBBLE_LR_POSITION_HORIZONTAL_OFFSET;
|
|
if (opts.showTail) {
|
|
left += BUBBLE_SHOWTAIL_OFFSET;
|
|
}
|
|
top = tooltip.rect.top + BUBBLE_BELOW_POSITION_VERTICAL_OFFSET;
|
|
} else if (opts.position === 'toLeft') {
|
|
right =
|
|
window.innerWidth -
|
|
tooltip.rect.left +
|
|
BUBBLE_LR_POSITION_HORIZONTAL_OFFSET;
|
|
if (opts.showTail) {
|
|
right += BUBBLE_SHOWTAIL_OFFSET;
|
|
}
|
|
top = tooltip.rect.top + BUBBLE_BELOW_POSITION_VERTICAL_OFFSET;
|
|
}
|
|
|
|
return (
|
|
<TooltipBubble
|
|
key="bubble"
|
|
top={top}
|
|
left={left}
|
|
bottom={bottom}
|
|
right={right}
|
|
options={opts}>
|
|
{tooltip.title}
|
|
</TooltipBubble>
|
|
);
|
|
}
|
|
|
|
getTooltipElement() {
|
|
const {tooltip} = this.state;
|
|
return (
|
|
tooltip &&
|
|
tooltip.title && [
|
|
this.getTooltipTail(tooltip),
|
|
this.getTooltipBubble(tooltip),
|
|
]
|
|
);
|
|
}
|
|
|
|
render() {
|
|
return [this.getTooltipElement(), this.props.children];
|
|
}
|
|
}
|