Fix tooltips causing entire app to rerender

Summary:
Hovering a tooltip container would cause the full application to re-render. (Even if there isn't an actual tooltip!).

Fixed it by killing legacy context code, and using proper fragments rather than returning arrays.

Reviewed By: priteshrnandgaonkar

Differential Revision: D19969775

fbshipit-source-id: 59f6470d03b6c476305681fde7bbe3f0dca063aa
This commit is contained in:
Michel Weststrate
2020-02-19 22:15:25 -08:00
committed by Facebook Github Bot
parent 07e5d7faf7
commit 97f9b2494d
2 changed files with 193 additions and 209 deletions

View File

@@ -7,10 +7,9 @@
* @format * @format
*/ */
import TooltipProvider, {TooltipOptions} from './TooltipProvider'; import {TooltipOptions, TooltipContext} from './TooltipProvider';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React, {Component} from 'react'; import React, {useContext, useCallback, useRef, useEffect} from 'react';
import PropTypes from 'prop-types';
const TooltipContainer = styled.div({ const TooltipContainer = styled.div({
display: 'contents', display: 'contents',
@@ -25,59 +24,40 @@ type TooltipProps = {
options?: TooltipOptions; options?: TooltipOptions;
}; };
type TooltipState = { export default function Tooltip(props: TooltipProps) {
open: boolean; const tooltipManager = useContext(TooltipContext);
}; const ref = useRef<HTMLDivElement | null>();
const isOpen = useRef<boolean>(false);
export default class Tooltip extends Component<TooltipProps, TooltipState> { useEffect(
static contextTypes = { () => () => {
TOOLTIP_PROVIDER: PropTypes.object, if (isOpen.current) {
}; tooltipManager.close();
}
},
[],
);
context!: { const onMouseEnter = useCallback(() => {
TOOLTIP_PROVIDER: TooltipProvider; if (ref.current && props.title) {
}; tooltipManager.open(ref.current, props.title, props.options || {});
isOpen.current = true;
ref: HTMLDivElement | undefined | null;
state = {
open: false,
};
componentWillUnmount() {
if (this.state.open === true) {
this.context.TOOLTIP_PROVIDER.close();
} }
} }, []);
onMouseEnter = () => { const onMouseLeave = useCallback(() => {
if (this.ref != null) { if (isOpen.current) {
this.context.TOOLTIP_PROVIDER.open( tooltipManager.close();
this.ref, isOpen.current = false;
this.props.title,
this.props.options || {},
);
this.setState({open: true});
} }
}; }, []);
onMouseLeave = () => { return (
this.context.TOOLTIP_PROVIDER.close(); <TooltipContainer
this.setState({open: false}); ref={ref as any}
}; onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
setRef = (ref: HTMLDivElement | null) => { {props.children}
this.ref = ref; </TooltipContainer>
}; );
render() {
return (
<TooltipContainer
ref={this.setRef}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}>
{this.props.children}
</TooltipContainer>
);
}
} }

View File

@@ -9,8 +9,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {colors} from './colors'; import {colors} from './colors';
import {Component} from 'react'; import {memo, createContext, useMemo, useState, useRef} from 'react';
import PropTypes from 'prop-types';
import { import {
TopProperty, TopProperty,
LeftProperty, LeftProperty,
@@ -134,163 +133,168 @@ type TooltipState = {
timeoutID: ReturnType<typeof setTimeout> | null | undefined; timeoutID: ReturnType<typeof setTimeout> | null | undefined;
}; };
export default class TooltipProvider extends Component< interface TooltipManager {
TooltipProps,
TooltipState
> {
static childContextTypes = {
TOOLTIP_PROVIDER: PropTypes.object,
};
state: TooltipState = {
tooltip: null,
timeoutID: undefined,
};
getChildContext() {
return {TOOLTIP_PROVIDER: this};
}
open( open(
container: HTMLDivElement, container: HTMLDivElement,
title: React.ReactNode, title: React.ReactNode,
options: TooltipOptions, options: TooltipOptions,
) { ): void;
const node = container.childNodes[0]; close(): void;
if (node == null || !(node instanceof HTMLElement)) { }
return;
} export const TooltipContext = createContext<TooltipManager>(undefined as any);
if (options.delay) { const TooltipProvider: React.FC<{}> = memo(function TooltipProvider({
this.state.timeoutID = setTimeout(() => { children,
this.setState({ }) {
tooltip: { const timeoutID = useRef<NodeJS.Timeout>();
rect: node.getBoundingClientRect(), const [tooltip, setTooltip] = useState<TooltipObject | undefined>(undefined);
title, const tooltipManager = useMemo(
options: options, () => ({
}, open(
}); container: HTMLDivElement,
}, options.delay); title: React.ReactNode,
return; options: TooltipOptions,
} ) {
if (timeoutID.current) {
this.setState({ clearTimeout(timeoutID.current);
tooltip: { }
rect: node.getBoundingClientRect(), const node = container.childNodes[0];
title, if (node == null || !(node instanceof HTMLElement)) {
options: options, return;
}, }
}); if (options.delay) {
} timeoutID.current = setTimeout(() => {
setTooltip({
close() { rect: node.getBoundingClientRect(),
if (this.state.timeoutID) { title,
clearTimeout(this.state.timeoutID); options: options,
} });
this.setState({tooltip: null}); }, options.delay);
} return;
}
getTooltipTail(tooltip: TooltipObject) { setTooltip({
const opts = Object.assign(defaultOptions, tooltip.options); rect: node.getBoundingClientRect(),
if (!opts.showTail) { title,
return null; options: options,
} });
},
let left: LeftProperty<number> = 'auto'; close() {
let top: TopProperty<number> = 'auto'; if (timeoutID.current) {
let bottom: BottomProperty<number> = 'auto'; clearTimeout(timeoutID.current);
let right: RightProperty<number> = 'auto'; }
setTooltip(undefined);
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; return (
} else if (opts.position === 'toRight') { <>
left = tooltip.rect.right + TAIL_LR_POSITION_HORIZONTAL_OFFSET; {tooltip && tooltip.title ? <Tooltip tooltip={tooltip} /> : null}
top = tooltip.rect.top; <TooltipContext.Provider value={tooltipManager}>
} else if (opts.position === 'toLeft') { {children}
right = </TooltipContext.Provider>
window.innerWidth - </>
tooltip.rect.left + );
TAIL_LR_POSITION_HORIZONTAL_OFFSET; });
top = tooltip.rect.top;
} function Tooltip({tooltip}: {tooltip: TooltipObject}) {
return (
return ( <>
<TooltipTail {getTooltipTail(tooltip)}
key="tail" {getTooltipBubble(tooltip)}
top={top} </>
left={left} );
bottom={bottom} }
right={right}
options={opts} export default TooltipProvider;
/>
); function getTooltipTail(tooltip: TooltipObject) {
} const opts = Object.assign(defaultOptions, tooltip.options);
if (!opts.showTail) {
getTooltipBubble(tooltip: TooltipObject) { return null;
const opts = Object.assign(defaultOptions, tooltip.options); }
let left: LeftProperty<number> = 'auto';
let top: TopProperty<number> = 'auto'; let left: LeftProperty<number> = 'auto';
let bottom: BottomProperty<number> = 'auto'; let top: TopProperty<number> = 'auto';
let right: RightProperty<number> = 'auto'; let bottom: BottomProperty<number> = 'auto';
let right: RightProperty<number> = 'auto';
if (opts.position === 'below') {
left = tooltip.rect.left; if (opts.position === 'below') {
top = tooltip.rect.bottom; left = tooltip.rect.left + TAIL_AB_POSITION_HORIZONTAL_OFFSET;
if (opts.showTail) { top = tooltip.rect.bottom;
top += BUBBLE_SHOWTAIL_OFFSET; } else if (opts.position === 'above') {
} left = tooltip.rect.left + TAIL_AB_POSITION_HORIZONTAL_OFFSET;
} else if (opts.position === 'above') { bottom = window.innerHeight - tooltip.rect.top;
bottom = window.innerHeight - tooltip.rect.top; } else if (opts.position === 'toRight') {
if (opts.showTail) { left = tooltip.rect.right + TAIL_LR_POSITION_HORIZONTAL_OFFSET;
bottom += BUBBLE_SHOWTAIL_OFFSET; top = tooltip.rect.top;
} } else if (opts.position === 'toLeft') {
left = tooltip.rect.left; right =
} else if (opts.position === 'toRight') { window.innerWidth -
left = tooltip.rect.right + BUBBLE_LR_POSITION_HORIZONTAL_OFFSET; tooltip.rect.left +
if (opts.showTail) { TAIL_LR_POSITION_HORIZONTAL_OFFSET;
left += BUBBLE_SHOWTAIL_OFFSET; top = tooltip.rect.top;
} }
top = tooltip.rect.top + BUBBLE_BELOW_POSITION_VERTICAL_OFFSET;
} else if (opts.position === 'toLeft') { return (
right = <TooltipTail
window.innerWidth - key="tail"
tooltip.rect.left + top={top}
BUBBLE_LR_POSITION_HORIZONTAL_OFFSET; left={left}
if (opts.showTail) { bottom={bottom}
right += BUBBLE_SHOWTAIL_OFFSET; right={right}
} options={opts}
top = tooltip.rect.top + BUBBLE_BELOW_POSITION_VERTICAL_OFFSET; />
} );
}
return (
<TooltipBubble function getTooltipBubble(tooltip: TooltipObject) {
key="bubble" const opts = Object.assign(defaultOptions, tooltip.options);
top={top} let left: LeftProperty<number> = 'auto';
left={left} let top: TopProperty<number> = 'auto';
bottom={bottom} let bottom: BottomProperty<number> = 'auto';
right={right} let right: RightProperty<number> = 'auto';
options={opts}>
{tooltip.title} if (opts.position === 'below') {
</TooltipBubble> left = tooltip.rect.left;
); top = tooltip.rect.bottom;
} if (opts.showTail) {
top += BUBBLE_SHOWTAIL_OFFSET;
getTooltipElement() { }
const {tooltip} = this.state; } else if (opts.position === 'above') {
return ( bottom = window.innerHeight - tooltip.rect.top;
tooltip && if (opts.showTail) {
tooltip.title && [ bottom += BUBBLE_SHOWTAIL_OFFSET;
this.getTooltipTail(tooltip), }
this.getTooltipBubble(tooltip), 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;
render() { }
return [this.getTooltipElement(), this.props.children]; 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>
);
} }