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(: );
}