lemmy-ui/src/shared/tippy.ts
matc-pub fdeb9244db
Only conditionally render most of content action dropdown and workaround for tippy warning (#2422)
* Avoid destroyed tippy warning

Tippy doesn't remove its onDocumentPress listener when destroyed.
Instead the listener removes itself after calling hide for hideOnClick.

It doesn't look like there is a way to reliable work around this.

This skips the warning for the first hide call on a destroyed tippy
instance.

Cleanup is only performed after at least ten tippy instances have been
created.

* Hide tooltips for elements that are no longer connected to the document

* Only render action modals after first show

* Only render action dropdown after first show

* Modals fix for quick unmount

Modals use `await import("bootstrap/js/dist/modal")` when being mounted.
This means its possible that the component unmounts before the promise
resolves.

* bind() dropdown toggle click handler

* Modal mixin
2024-04-13 11:15:29 -04:00

70 lines
2 KiB
TypeScript

import { RefObject } from "inferno";
import {
DelegateInstance as TippyDelegateInstance,
Props as TippyProps,
Instance as TippyInstance,
delegate as tippyDelegate,
} from "tippy.js";
let instance: TippyDelegateInstance<TippyProps> | undefined;
const tippySelector = "[data-tippy-content]";
const shownInstances: Set<TippyInstance<TippyProps>> = new Set();
let instanceCounter = 0;
const tippyDelegateOptions: Partial<TippyProps> & { target: string } = {
delay: [500, 0],
// Display on "long press"
touch: ["hold", 500],
target: tippySelector,
onShow(i: TippyInstance<TippyProps>) {
shownInstances.add(i);
},
onHidden(i: TippyInstance<TippyProps>) {
shownInstances.delete(i);
},
onCreate() {
instanceCounter++;
},
onDestroy(i: TippyInstance<TippyProps>) {
// Tippy doesn't remove its onDocumentPress listener when destroyed.
// Instead the listener removes itself after calling hide for hideOnClick.
const origHide = i.hide;
// This silences the first warning when hiding a destroyed tippy instance.
// hide() is otherwise a noop for destroyed instances.
i.hide = () => {
i.hide = origHide;
};
},
};
export function setupTippy(root: RefObject<Element>) {
if (!instance && root.current) {
instance = tippyDelegate(root.current, tippyDelegateOptions);
}
}
export function cleanupTippy() {
// Hide tooltips for elements that are no longer connected to the document.
shownInstances.forEach(i => {
if (!i.reference.isConnected) {
console.assert(!i.state.isDestroyed, "hide called on destroyed tippy");
i.hide();
}
});
if (shownInstances.size || instanceCounter < 10) {
// Avoid randomly closing tooltips.
return;
}
instanceCounter = 0;
const current = instance?.reference ?? null;
// delegate from tippy.js creates tippy instances when needed, but only
// destroys them when the delegate instance is destroyed.
destroyTippy();
setupTippy({ current });
}
export function destroyTippy() {
instance?.destroy();
instance = undefined;
}