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:
committed by
Facebook Github Bot
parent
07e5d7faf7
commit
97f9b2494d
@@ -7,10 +7,9 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import TooltipProvider, {TooltipOptions} from './TooltipProvider';
|
||||
import {TooltipOptions, TooltipContext} from './TooltipProvider';
|
||||
import styled from '@emotion/styled';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {useContext, useCallback, useRef, useEffect} from 'react';
|
||||
|
||||
const TooltipContainer = styled.div({
|
||||
display: 'contents',
|
||||
@@ -25,59 +24,40 @@ type TooltipProps = {
|
||||
options?: TooltipOptions;
|
||||
};
|
||||
|
||||
type TooltipState = {
|
||||
open: boolean;
|
||||
};
|
||||
export default function Tooltip(props: TooltipProps) {
|
||||
const tooltipManager = useContext(TooltipContext);
|
||||
const ref = useRef<HTMLDivElement | null>();
|
||||
const isOpen = useRef<boolean>(false);
|
||||
|
||||
export default class Tooltip extends Component<TooltipProps, TooltipState> {
|
||||
static contextTypes = {
|
||||
TOOLTIP_PROVIDER: PropTypes.object,
|
||||
};
|
||||
|
||||
context!: {
|
||||
TOOLTIP_PROVIDER: TooltipProvider;
|
||||
};
|
||||
|
||||
ref: HTMLDivElement | undefined | null;
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.open === true) {
|
||||
this.context.TOOLTIP_PROVIDER.close();
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (isOpen.current) {
|
||||
tooltipManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (this.ref != null) {
|
||||
this.context.TOOLTIP_PROVIDER.open(
|
||||
this.ref,
|
||||
this.props.title,
|
||||
this.props.options || {},
|
||||
},
|
||||
[],
|
||||
);
|
||||
this.setState({open: true});
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
if (ref.current && props.title) {
|
||||
tooltipManager.open(ref.current, props.title, props.options || {});
|
||||
isOpen.current = true;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.context.TOOLTIP_PROVIDER.close();
|
||||
this.setState({open: false});
|
||||
};
|
||||
const onMouseLeave = useCallback(() => {
|
||||
if (isOpen.current) {
|
||||
tooltipManager.close();
|
||||
isOpen.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
setRef = (ref: HTMLDivElement | null) => {
|
||||
this.ref = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TooltipContainer
|
||||
ref={this.setRef}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}>
|
||||
{this.props.children}
|
||||
ref={ref as any}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
{props.children}
|
||||
</TooltipContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import {colors} from './colors';
|
||||
import {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {memo, createContext, useMemo, useState, useRef} from 'react';
|
||||
import {
|
||||
TopProperty,
|
||||
LeftProperty,
|
||||
@@ -134,63 +133,84 @@ type TooltipState = {
|
||||
timeoutID: ReturnType<typeof setTimeout> | null | undefined;
|
||||
};
|
||||
|
||||
export default class TooltipProvider extends Component<
|
||||
TooltipProps,
|
||||
TooltipState
|
||||
> {
|
||||
static childContextTypes = {
|
||||
TOOLTIP_PROVIDER: PropTypes.object,
|
||||
};
|
||||
interface TooltipManager {
|
||||
open(
|
||||
container: HTMLDivElement,
|
||||
title: React.ReactNode,
|
||||
options: TooltipOptions,
|
||||
): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
state: TooltipState = {
|
||||
tooltip: null,
|
||||
timeoutID: undefined,
|
||||
};
|
||||
|
||||
getChildContext() {
|
||||
return {TOOLTIP_PROVIDER: this};
|
||||
}
|
||||
export const TooltipContext = createContext<TooltipManager>(undefined as any);
|
||||
|
||||
const TooltipProvider: React.FC<{}> = memo(function TooltipProvider({
|
||||
children,
|
||||
}) {
|
||||
const timeoutID = useRef<NodeJS.Timeout>();
|
||||
const [tooltip, setTooltip] = useState<TooltipObject | undefined>(undefined);
|
||||
const tooltipManager = useMemo(
|
||||
() => ({
|
||||
open(
|
||||
container: HTMLDivElement,
|
||||
title: React.ReactNode,
|
||||
options: TooltipOptions,
|
||||
) {
|
||||
if (timeoutID.current) {
|
||||
clearTimeout(timeoutID.current);
|
||||
}
|
||||
const node = container.childNodes[0];
|
||||
if (node == null || !(node instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.delay) {
|
||||
this.state.timeoutID = setTimeout(() => {
|
||||
this.setState({
|
||||
tooltip: {
|
||||
timeoutID.current = setTimeout(() => {
|
||||
setTooltip({
|
||||
rect: node.getBoundingClientRect(),
|
||||
title,
|
||||
options: options,
|
||||
},
|
||||
});
|
||||
}, options.delay);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tooltip: {
|
||||
setTooltip({
|
||||
rect: node.getBoundingClientRect(),
|
||||
title,
|
||||
options: options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
close() {
|
||||
if (this.state.timeoutID) {
|
||||
clearTimeout(this.state.timeoutID);
|
||||
}
|
||||
this.setState({tooltip: null});
|
||||
if (timeoutID.current) {
|
||||
clearTimeout(timeoutID.current);
|
||||
}
|
||||
setTooltip(undefined);
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
getTooltipTail(tooltip: TooltipObject) {
|
||||
return (
|
||||
<>
|
||||
{tooltip && tooltip.title ? <Tooltip tooltip={tooltip} /> : null}
|
||||
<TooltipContext.Provider value={tooltipManager}>
|
||||
{children}
|
||||
</TooltipContext.Provider>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function Tooltip({tooltip}: {tooltip: TooltipObject}) {
|
||||
return (
|
||||
<>
|
||||
{getTooltipTail(tooltip)}
|
||||
{getTooltipBubble(tooltip)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TooltipProvider;
|
||||
|
||||
function getTooltipTail(tooltip: TooltipObject) {
|
||||
const opts = Object.assign(defaultOptions, tooltip.options);
|
||||
if (!opts.showTail) {
|
||||
return null;
|
||||
@@ -228,9 +248,9 @@ export default class TooltipProvider extends Component<
|
||||
options={opts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getTooltipBubble(tooltip: TooltipObject) {
|
||||
function getTooltipBubble(tooltip: TooltipObject) {
|
||||
const opts = Object.assign(defaultOptions, tooltip.options);
|
||||
let left: LeftProperty<number> = 'auto';
|
||||
let top: TopProperty<number> = 'auto';
|
||||
@@ -277,20 +297,4 @@ export default class TooltipProvider extends Component<
|
||||
{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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user