Skip to content

Commit

Permalink
Backport of tgui tooltip fixes (TauCetiStation#13121)
Browse files Browse the repository at this point in the history
* Another fix for laggy tgui tooltips -- Turn off popper event listeners (tgstation/tgstation#61343)

Every single popper registered TWO event listeners for scrolling and resizing. This does not appear to be necessary for our cases.

* Make tooltips use one popper, fixing mount lag (tgstation/tgstation#61783)

* build

---------

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
  • Loading branch information
volas and Mothblocks authored May 31, 2024
1 parent e34883b commit e6e386e
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 49 deletions.
152 changes: 112 additions & 40 deletions tgui/packages/tgui/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@

import { Placement } from '@popperjs/core';
import { Component, findDOMfromVNode, InfernoNode } from 'inferno';
import { Popper } from "./Popper";

const DEFAULT_PLACEMENT = "top";
import { createPopper, Placement, VirtualElement } from '@popperjs/core';
import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';

type TooltipProps = {
children?: InfernoNode;
content: string;
content: InfernoNode;
position?: Placement,
};

type TooltipState = {
hovered: boolean;
};

export class Tooltip extends Component<TooltipProps, TooltipState> {
constructor() {
super();
const DEFAULT_OPTIONS = {
modifiers: [{
name: "eventListeners",
enabled: false,
}],
};

this.state = {
hovered: false,
};
}
const NULL_RECT = {
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
};

componentDidMount() {
export class Tooltip extends Component<TooltipProps, TooltipState> {
// Mounting poppers is really laggy because popper.js is very slow.
// Thus, instead of using the Popper component, Tooltip creates ONE popper
// and stores every tooltip inside that.
// This means you can never have two tooltips at once, for instance.
static renderedTooltip: HTMLDivElement | undefined;
static singletonPopper: ReturnType<typeof createPopper> | undefined;
static currentHoveredElement: Element | undefined;
static virtualElement: VirtualElement = {
getBoundingClientRect: () => (
Tooltip.currentHoveredElement?.getBoundingClientRect()
?? NULL_RECT
),
};

getDOMNode() {
// HACK: We don't want to create a wrapper, as it could break the layout
// of consumers, so we do the inferno equivalent of `findDOMNode(this)`.
// My attempt to avoid this was a render prop that passed in
Expand All @@ -33,41 +52,94 @@ export class Tooltip extends Component<TooltipProps, TooltipState> {
// This code is copied from `findDOMNode` in inferno-extras.
// Because this component is written in TypeScript, we will know
// immediately if this internal variable is removed.
const domNode = findDOMfromVNode(this.$LI, true);
return findDOMfromVNode(this.$LI, true);
}

componentDidMount() {
const domNode = this.getDOMNode();

if (!domNode) {
return;
}

domNode.addEventListener("mouseenter", () => {
this.setState({
hovered: true,
});
let renderedTooltip = Tooltip.renderedTooltip;
if (renderedTooltip === undefined) {
renderedTooltip = document.createElement("div");
renderedTooltip.className = "Tooltip";
document.body.appendChild(renderedTooltip);
Tooltip.renderedTooltip = renderedTooltip;
}

Tooltip.currentHoveredElement = domNode;

renderedTooltip.style.opacity = "1";

this.renderPopperContent();
});

domNode.addEventListener("mouseleave", () => {
this.setState({
hovered: false,
});
this.fadeOut();
});
}

render() {
return (
<Popper
options={{
placement: this.props.position || "auto",
}}
popperContent={
<div
className="Tooltip"
style={{
opacity: this.state.hovered ? 1 : 0,
}}>
{this.props.content}
</div>
fadeOut() {
if (Tooltip.currentHoveredElement !== this.getDOMNode()) {
return;
}

Tooltip.currentHoveredElement = undefined;
Tooltip.renderedTooltip!.style.opacity = "0";
}

renderPopperContent() {
const renderedTooltip = Tooltip.renderedTooltip;
if (!renderedTooltip) {
return;
}

render(
<span>{this.props.content}</span>,
renderedTooltip,
() => {
let singletonPopper = Tooltip.singletonPopper;
if (singletonPopper === undefined) {
singletonPopper = createPopper(
Tooltip.virtualElement,
renderedTooltip!,
{
...DEFAULT_OPTIONS,
placement: this.props.position || "auto",
}
);

Tooltip.singletonPopper = singletonPopper;
} else {
singletonPopper.setOptions({
...DEFAULT_OPTIONS,
placement: this.props.position || "auto",
});

singletonPopper.update();
}
additionalStyles={{
"pointer-events": "none",
}}>
{this.props.children}
</Popper>
},
this.context,
);
}

componentDidUpdate() {
if (Tooltip.currentHoveredElement !== this.getDOMNode()) {
return;
}

this.renderPopperContent();
}

componentWillUnmount() {
this.fadeOut();
}

render() {
return this.props.children;
}
}
2 changes: 1 addition & 1 deletion tgui/packages/tgui/styles/components/Tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ $border-radius: base.$border-radius !default;
padding: 0.5em 0.75em;
pointer-events: none;
text-align: left;
transition: all 150ms ease-out;
transition: opacity 150ms ease-out;
background-color: $background-color;
color: $color;
box-shadow: 0.1em 0.1em 1.25em -0.1em rgba(0, 0, 0, 0.5);
Expand Down
2 changes: 1 addition & 1 deletion tgui/public/tgui-panel.bundle.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tgui/public/tgui-panel.bundle.js

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions tgui/public/tgui.bundle.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tgui/public/tgui.bundle.js

Large diffs are not rendered by default.

0 comments on commit e6e386e

Please sign in to comment.