Update Tooltip implementation for Flipper

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
This commit is contained in:
Sara Valderrama
2018-09-04 10:32:01 -07:00
committed by Facebook Github Bot
parent cf3cb0d08f
commit d26779cd16
5 changed files with 221 additions and 37 deletions

View File

@@ -21,6 +21,7 @@ import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import reducers from './reducers/index.js';
import dispatcher from './dispatcher/index.js';
import {setupMenuBar} from './MenuBar.js';
import TooltipProvider from './ui/components/TooltipProvider.js';
const path = require('path');
const reducer: typeof reducers = persistReducer(
@@ -52,11 +53,13 @@ GK.init();
setupMenuBar();
const AppFrame = () => (
<ContextMenuProvider>
<Provider store={store}>
<App logger={logger} bugReporter={bugReporter} />
</Provider>
</ContextMenuProvider>
<TooltipProvider>
<ContextMenuProvider>
<Provider store={store}>
<App logger={logger} bugReporter={bugReporter} />
</Provider>
</ContextMenuProvider>
</TooltipProvider>
);
// $FlowFixMe: this element exists!

View File

@@ -955,9 +955,9 @@ export default class Layout extends SonarPlugin<InspectorState> {
'accessibility-focused':
'True if this element has the focus of an accessibility service',
'content-description':
'Text to label the content/functionality of this element ',
'Text to label the content or functionality of this element ',
'important-for-accessibility':
'Marks this element as important to accessibility services, one of AUTO, YES, NO, NO_HIDE_DESCENDANTS',
'Marks this element as important to accessibility services; one of AUTO, YES, NO, NO_HIDE_DESCENDANTS',
'talkback-focusable': 'True if Talkback can focus on this element',
'talkback-focusable-reasons': 'Why Talkback can focus on this element',
'talkback-ignored': 'True if Talkback cannot focus on this element',

View File

@@ -6,6 +6,7 @@
*/
import type TooltipProvider from './TooltipProvider.js';
import type {TooltipOptions} from './TooltipProvider.js';
import styled from '../styled/index.js';
import {Component} from 'react';
@@ -19,6 +20,7 @@ const TooltipContainer = styled('div')({
type TooltipProps = {
title: React$Node,
children: React$Node,
options?: TooltipOptions,
};
type TooltipState = {
@@ -48,7 +50,11 @@ export default class Tooltip extends Component<TooltipProps, TooltipState> {
onMouseEnter = () => {
if (this.ref != null) {
this.context.TOOLTIP_PROVIDER.open(this.ref, this.props.title);
this.context.TOOLTIP_PROVIDER.open(
this.ref,
this.props.title,
this.props.options || {},
);
this.setState({open: true});
}
};

View File

@@ -6,34 +6,93 @@
*/
import styled from '../styled/index.js';
import {colors} from './colors.js';
import {Component} from 'react';
const PropTypes = require('prop-types');
const TooltipBubble = styled('div')(props => ({
backgroundColor: '#000',
lineHeight: '25px',
padding: '0 6px',
borderRadius: 4,
position: 'absolute',
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,
zIndex: 99999999999,
pointerEvents: 'none',
color: '#fff',
marginTop: '-30px',
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: ?{
rect: ClientRect,
title: React$Node,
},
tooltip: ?TooltipObject,
timeoutID: ?TimeoutID,
};
export default class TooltipProvider extends Component<
@@ -46,42 +105,149 @@ export default class TooltipProvider extends Component<
state = {
tooltip: null,
timeoutID: undefined,
};
getChildContext() {
return {TOOLTIP_PROVIDER: this};
}
open(container: HTMLDivElement, title: React$Node) {
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});
}
render() {
const {tooltip} = this.state;
let tooltipElem = null;
if (tooltip != null) {
tooltipElem = (
<TooltipBubble top={tooltip.rect.top} left={tooltip.rect.left}>
{tooltip.title}
</TooltipBubble>
);
getTooltipTail(tooltip: TooltipObject) {
const opts = Object.assign(defaultOptions, tooltip.options);
if (!opts.showTail) {
return null;
}
return [tooltipElem, this.props.children];
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];
}
}

View File

@@ -8,6 +8,7 @@
import DataDescription from './DataDescription.js';
import {Component} from 'react';
import ContextMenu from '../ContextMenu.js';
import Tooltip from '../Tooltip.js';
import styled from '../../styled/index.js';
import DataPreview from './DataPreview.js';
import createPaste from '../../../utils/createPaste.js';
@@ -52,6 +53,11 @@ export const InspectorName = styled('span')({
color: colors.grapeDark1,
});
const nameTooltipOptions = {
position: 'toLeft',
showTail: true,
};
export type DataValueExtractor = (
value: any,
depth: number,
@@ -525,9 +531,12 @@ export default class DataInspector extends Component<DataInspectorProps> {
const nameElems = [];
if (typeof name !== 'undefined') {
nameElems.push(
<InspectorName key="name" title={(tooltips && tooltips[name]) || ''}>
{name}
</InspectorName>,
<Tooltip
title={tooltips && tooltips[name]}
key="name"
options={nameTooltipOptions}>
<InspectorName>{name}</InspectorName>
</Tooltip>,
);
nameElems.push(<span key="sep">: </span>);
}