Packages
pui
1.0.0-alpha.29
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.1
1.0.0-alpha.32
1.0.0-alpha.31
1.0.0-alpha.30
1.0.0-alpha.29
1.0.0-alpha.28
1.0.0-alpha.27
1.0.0-alpha.26
1.0.0-alpha.25
1.0.0-alpha.24
1.0.0-alpha.23
1.0.0-alpha.22
1.0.0-alpha.21
1.0.0-alpha.19
1.0.0-alpha.18
1.0.0-alpha.17
1.0.0-alpha.16
1.0.0-alpha.15
1.0.0-alpha.14
1.0.0-alpha.13
1.0.0-alpha.12
1.0.0-alpha.11
A Phoenix LiveView UI toolkit
Current section
Files
Jump to
Current section
Files
assets/js/tooltip.js
import { ViewHook } from "phoenix_live_view";
import {
computePosition,
offset,
flip,
shift,
autoUpdate,
arrow,
} from "@floating-ui/dom";
export default class Tooltip extends ViewHook {
placement = "top";
delay = 300;
hovering = false;
#clear_floating;
#enterBind;
#leaveBind;
#focusInBind;
#focusOutBind;
leaveTimeout = null;
mounted() {
this.trigger = this.el.querySelector(':scope > :not([role="tooltip"])');
this.tooltip = this.el.querySelector(':scope > [role="tooltip"]');
this.arrow = this.el.querySelector(
':scope > [role="tooltip"] > [data-arrow]',
);
// this attribute controlled on client side, prevent liveview patching this attr
this.js().ignoreAttributes(this.tooltip, ["aria-*", "data-*", "style"]);
this.trigger?.setAttribute("aria-describedby", this.tooltip?.id || "");
this.placement = this.el.dataset.placement || this.placement;
this.delay = this.el.dataset.delay || this.delay;
this.#clear_floating = autoUpdate(this.trigger, this.tooltip, () => {
this._calculatePosition();
});
this.#enterBind = this._onMouseEnter.bind(this);
this.#leaveBind = this._onMouseLeave.bind(this);
this.#focusInBind = this._onFocusIn.bind(this);
this.#focusOutBind = this._onFocusOut.bind(this);
this.el?.addEventListener("mouseenter", this.#enterBind);
this.el?.addEventListener("mouseleave", this.#leaveBind);
this.el?.addEventListener("focusin", this.#focusInBind);
this.el?.addEventListener("focusout", this.#focusOutBind);
}
updated() {
this.setHidden(this.hovering ? false : true);
this._calculatePosition();
}
destroyed() {
if (this.#clear_floating) {
this.#clear_floating();
}
this.el?.removeEventListener("mouseenter", this.#enterBind);
this.el?.removeEventListener("mouseleave", this.#leaveBind);
this.el?.removeEventListener("focusin", this.#focusInBind);
this.el?.removeEventListener("focusout", this.#focusOutBind);
}
setHidden(hidden) {
requestAnimationFrame(() => {
this.tooltip?.setAttribute("aria-hidden", hidden ? "true" : "false");
});
}
_onMouseEnter(e) {
this.hovering = true;
if (this.leaveTimeout) clearTimeout(this.leaveTimeout);
this.setHidden(false);
}
_onMouseLeave(e) {
if (this.leaveTimeout) clearTimeout(this.leaveTimeout);
this.hovering = false;
this.leaveTimeout = setTimeout(() => {
this.setHidden(true);
}, this.delay);
}
_onFocusIn() {
this.hovering = true;
if (this.leaveTimeout) clearTimeout(this.leaveTimeout);
this.setHidden(false);
}
_onFocusOut(event) {
const nextFocusedElement = event.relatedTarget;
if (nextFocusedElement && this.el?.contains(nextFocusedElement)) {
return;
}
if (this.leaveTimeout) clearTimeout(this.leaveTimeout);
this.hovering = false;
this.leaveTimeout = setTimeout(() => {
this.setHidden(true);
}, this.delay);
}
_calculatePosition(data) {
computePosition(this.trigger, this.tooltip, {
placement: this.placement,
strategy: "fixed",
middleware: [offset(8), flip(), shift(), arrow({ element: this.arrow })],
}).then(({ x, y, strategy, middlewareData, placement }) => {
Object.assign(this.tooltip.style, {
left: `${x}px`,
top: `${y}px`,
position: strategy,
});
if (middlewareData.arrow) {
const { x, y } = middlewareData.arrow;
const p = placement.split("-").at(0) ?? null;
const arrowPlacement = {
top: "bottom",
left: "right",
right: "left",
bottom: "top",
};
const style = {
left: x != null ? `${x}px` : "",
top: y != null ? `${y}px` : "",
};
style[arrowPlacement[p]] = "-4px";
Object.assign(this.arrow.style, style);
}
});
}
}