From d26779cd1692c901c32c9fd02f4b5428a867ba92 Mon Sep 17 00:00:00 2001 From: Sara Valderrama Date: Tue, 4 Sep 2018 10:32:01 -0700 Subject: [PATCH] Update Tooltip implementation for Flipper Summary: Basic tooltips available. Use is: Reviewed By: danielbuechele Differential Revision: D9596287 fbshipit-source-id: 233b1ad01b96264bbc1f62f3798e3d69d1ab4bae --- src/init.js | 13 +- src/plugins/layout/index.js | 4 +- src/ui/components/Tooltip.js | 8 +- src/ui/components/TooltipProvider.js | 218 +++++++++++++++--- .../data-inspector/DataInspector.js | 15 +- 5 files changed, 221 insertions(+), 37 deletions(-) diff --git a/src/init.js b/src/init.js index 01b5b96f7..96a47ff8c 100644 --- a/src/init.js +++ b/src/init.js @@ -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 = () => ( - - - - - + + + + + + + ); // $FlowFixMe: this element exists! diff --git a/src/plugins/layout/index.js b/src/plugins/layout/index.js index ac20f5f56..0226e4233 100644 --- a/src/plugins/layout/index.js +++ b/src/plugins/layout/index.js @@ -955,9 +955,9 @@ export default class Layout extends SonarPlugin { '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', diff --git a/src/ui/components/Tooltip.js b/src/ui/components/Tooltip.js index 2442dba51..cd3b4072c 100644 --- a/src/ui/components/Tooltip.js +++ b/src/ui/components/Tooltip.js @@ -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 { 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}); } }; diff --git a/src/ui/components/TooltipProvider.js b/src/ui/components/TooltipProvider.js index bd7fc37ca..c935b6de6 100644 --- a/src/ui/components/TooltipProvider.js +++ b/src/ui/components/TooltipProvider.js @@ -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 = ( - - {tooltip.title} - - ); + 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 ( + + ); + } + + 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 ( + + {tooltip.title} + + ); + } + + getTooltipElement() { + const {tooltip} = this.state; + return ( + tooltip && + tooltip.title && [ + this.getTooltipTail(tooltip), + this.getTooltipBubble(tooltip), + ] + ); + } + + render() { + return [this.getTooltipElement(), this.props.children]; } } diff --git a/src/ui/components/data-inspector/DataInspector.js b/src/ui/components/data-inspector/DataInspector.js index 811d9a8a7..8b8e00c5b 100644 --- a/src/ui/components/data-inspector/DataInspector.js +++ b/src/ui/components/data-inspector/DataInspector.js @@ -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 { const nameElems = []; if (typeof name !== 'undefined') { nameElems.push( - - {name} - , + + {name} + , ); nameElems.push(: ); }