Attempt to fix Failed to execute 'removeChild' on 'Node'
(#9196)
* Switch tooltips to use React Portals * Remove redundant React key to simplify reconciliation * Fix cleanup and it.each test * Update snapshots due to style order difference
This commit is contained in:
parent
3461573df9
commit
27a7263965
4 changed files with 66 additions and 65 deletions
|
@ -52,8 +52,10 @@ export interface ITooltipProps {
|
|||
maxParentWidth?: number;
|
||||
}
|
||||
|
||||
export default class Tooltip extends React.Component<ITooltipProps> {
|
||||
private tooltipContainer: HTMLElement;
|
||||
type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transform" | "left">>;
|
||||
|
||||
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||
private static container: HTMLElement;
|
||||
private parent: Element;
|
||||
|
||||
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
|
||||
|
@ -65,37 +67,47 @@ export default class Tooltip extends React.Component<ITooltipProps> {
|
|||
alignment: Alignment.Natural,
|
||||
};
|
||||
|
||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
// Create a wrapper for the tooltips and attach it to the body element
|
||||
if (!Tooltip.container) {
|
||||
Tooltip.container = document.createElement("div");
|
||||
Tooltip.container.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(Tooltip.container);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this.renderTooltip, {
|
||||
window.addEventListener('scroll', this.updatePosition, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
|
||||
this.renderTooltip();
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.renderTooltip();
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
// Remove the wrapper element, as the tooltip has finished using it
|
||||
public componentWillUnmount() {
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this.renderTooltip, {
|
||||
window.removeEventListener('scroll', this.updatePosition, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
private updatePosition(style: CSSProperties) {
|
||||
private updatePosition = (): void => {
|
||||
// When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance)
|
||||
if (!this.props.visible) return;
|
||||
|
||||
const parentBox = this.parent.getBoundingClientRect();
|
||||
const width = UIStore.instance.windowWidth;
|
||||
const spacing = 6;
|
||||
|
@ -112,6 +124,7 @@ export default class Tooltip extends React.Component<ITooltipProps> {
|
|||
parentBox.left - window.scrollX + (parentWidth / 2)
|
||||
);
|
||||
|
||||
const style: State = {};
|
||||
switch (this.props.alignment) {
|
||||
case Alignment.Natural:
|
||||
if (parentBox.right > width / 2) {
|
||||
|
@ -153,25 +166,20 @@ export default class Tooltip extends React.Component<ITooltipProps> {
|
|||
break;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
private renderTooltip = () => {
|
||||
let style: CSSProperties = {};
|
||||
// When the tooltip is hidden, no need to thrash the DOM with `style`
|
||||
// attribute updates (performance)
|
||||
if (this.props.visible) {
|
||||
style = this.updatePosition({});
|
||||
}
|
||||
// Hide the entire container when not visible. This prevents flashing of the tooltip
|
||||
// if it is not meant to be visible on first mount.
|
||||
style.display = this.props.visible ? "block" : "none";
|
||||
this.setState(style);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
|
||||
"mx_Tooltip_visible": this.props.visible,
|
||||
"mx_Tooltip_invisible": !this.props.visible,
|
||||
});
|
||||
|
||||
const style = { ...this.state };
|
||||
// Hide the entire container when not visible.
|
||||
// This prevents flashing of the tooltip if it is not meant to be visible on first mount.
|
||||
style.display = this.props.visible ? "block" : "none";
|
||||
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_Tooltip_chevron" />
|
||||
|
@ -179,14 +187,10 @@ export default class Tooltip extends React.Component<ITooltipProps> {
|
|||
</div>
|
||||
);
|
||||
|
||||
// Render the tooltip manually, as we wish it not to be rendered within the parent
|
||||
ReactDOM.render<Element>(tooltip, this.tooltipContainer);
|
||||
};
|
||||
|
||||
public render() {
|
||||
// Render a placeholder
|
||||
return (
|
||||
<div className={this.props.className} />
|
||||
<div className={this.props.className}>
|
||||
{ ReactDOM.createPortal(tooltip, Tooltip.container) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue