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
|
* @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!: {
|
|
||||||
TOOLTIP_PROVIDER: TooltipProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
ref: HTMLDivElement | undefined | null;
|
|
||||||
|
|
||||||
state = {
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.state.open === true) {
|
|
||||||
this.context.TOOLTIP_PROVIDER.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 = () => {
|
const onMouseLeave = useCallback(() => {
|
||||||
this.context.TOOLTIP_PROVIDER.close();
|
if (isOpen.current) {
|
||||||
this.setState({open: false});
|
tooltipManager.close();
|
||||||
};
|
isOpen.current = false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
setRef = (ref: HTMLDivElement | null) => {
|
|
||||||
this.ref = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<TooltipContainer
|
<TooltipContainer
|
||||||
ref={this.setRef}
|
ref={ref as any}
|
||||||
onMouseEnter={this.onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={this.onMouseLeave}>
|
onMouseLeave={onMouseLeave}>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</TooltipContainer>
|
</TooltipContainer>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,63 +133,84 @@ type TooltipState = {
|
|||||||
timeoutID: ReturnType<typeof setTimeout> | null | undefined;
|
timeoutID: ReturnType<typeof setTimeout> | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class TooltipProvider extends Component<
|
interface TooltipManager {
|
||||||
TooltipProps,
|
open(
|
||||||
TooltipState
|
container: HTMLDivElement,
|
||||||
> {
|
title: React.ReactNode,
|
||||||
static childContextTypes = {
|
options: TooltipOptions,
|
||||||
TOOLTIP_PROVIDER: PropTypes.object,
|
): void;
|
||||||
};
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
state: TooltipState = {
|
export const TooltipContext = createContext<TooltipManager>(undefined as any);
|
||||||
tooltip: null,
|
|
||||||
timeoutID: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {TOOLTIP_PROVIDER: this};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const TooltipProvider: React.FC<{}> = memo(function TooltipProvider({
|
||||||
|
children,
|
||||||
|
}) {
|
||||||
|
const timeoutID = useRef<NodeJS.Timeout>();
|
||||||
|
const [tooltip, setTooltip] = useState<TooltipObject | undefined>(undefined);
|
||||||
|
const tooltipManager = useMemo(
|
||||||
|
() => ({
|
||||||
open(
|
open(
|
||||||
container: HTMLDivElement,
|
container: HTMLDivElement,
|
||||||
title: React.ReactNode,
|
title: React.ReactNode,
|
||||||
options: TooltipOptions,
|
options: TooltipOptions,
|
||||||
) {
|
) {
|
||||||
|
if (timeoutID.current) {
|
||||||
|
clearTimeout(timeoutID.current);
|
||||||
|
}
|
||||||
const node = container.childNodes[0];
|
const node = container.childNodes[0];
|
||||||
if (node == null || !(node instanceof HTMLElement)) {
|
if (node == null || !(node instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.delay) {
|
if (options.delay) {
|
||||||
this.state.timeoutID = setTimeout(() => {
|
timeoutID.current = setTimeout(() => {
|
||||||
this.setState({
|
setTooltip({
|
||||||
tooltip: {
|
|
||||||
rect: node.getBoundingClientRect(),
|
rect: node.getBoundingClientRect(),
|
||||||
title,
|
title,
|
||||||
options: options,
|
options: options,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}, options.delay);
|
}, options.delay);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setTooltip({
|
||||||
this.setState({
|
|
||||||
tooltip: {
|
|
||||||
rect: node.getBoundingClientRect(),
|
rect: node.getBoundingClientRect(),
|
||||||
title,
|
title,
|
||||||
options: options,
|
options: options,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
if (this.state.timeoutID) {
|
if (timeoutID.current) {
|
||||||
clearTimeout(this.state.timeoutID);
|
clearTimeout(timeoutID.current);
|
||||||
}
|
|
||||||
this.setState({tooltip: null});
|
|
||||||
}
|
}
|
||||||
|
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);
|
const opts = Object.assign(defaultOptions, tooltip.options);
|
||||||
if (!opts.showTail) {
|
if (!opts.showTail) {
|
||||||
return null;
|
return null;
|
||||||
@@ -228,9 +248,9 @@ export default class TooltipProvider extends Component<
|
|||||||
options={opts}
|
options={opts}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTooltipBubble(tooltip: TooltipObject) {
|
function getTooltipBubble(tooltip: TooltipObject) {
|
||||||
const opts = Object.assign(defaultOptions, tooltip.options);
|
const opts = Object.assign(defaultOptions, tooltip.options);
|
||||||
let left: LeftProperty<number> = 'auto';
|
let left: LeftProperty<number> = 'auto';
|
||||||
let top: TopProperty<number> = 'auto';
|
let top: TopProperty<number> = 'auto';
|
||||||
@@ -277,20 +297,4 @@ export default class TooltipProvider extends Component<
|
|||||||
{tooltip.title}
|
{tooltip.title}
|
||||||
</TooltipBubble>
|
</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