fix: tooltip tracks current mouse position, opens at cursor

This commit is contained in:
Falkan
2026-03-19 00:24:20 -04:00
parent 7dec5272b1
commit d1500c6ac3

View File

@@ -1,15 +1,6 @@
/**
* Svelte action: show a custom tooltip after a 500ms hover delay.
* Uses a positioned div instead of the native `title` attribute —
* native tooltips only appear on mousemove, not on mouseenter, which
* causes a "hover then wiggle" requirement that feels broken.
*
* If text is empty, does nothing.
*/
const TOOLTIP_CLASS = 'hu-tooltip'; const TOOLTIP_CLASS = 'hu-tooltip';
const TOOLTIP_VISIBLE_CLASS = 'hu-tooltip--visible';
// Shared tooltip element — one per page, repositioned as needed.
let tooltipEl: HTMLDivElement | null = null; let tooltipEl: HTMLDivElement | null = null;
function getTooltipEl(): HTMLDivElement { function getTooltipEl(): HTMLDivElement {
@@ -24,41 +15,36 @@ function getTooltipEl(): HTMLDivElement {
export function tooltip(node: HTMLElement, text: string) { export function tooltip(node: HTMLElement, text: string) {
let timer: ReturnType<typeof setTimeout> | null = null; let timer: ReturnType<typeof setTimeout> | null = null;
let currentText = text; let currentText = text;
let mouseX = 0;
let mouseY = 0;
function show(e: MouseEvent) { function show() {
if (!currentText) return; if (!currentText) return;
const el = getTooltipEl(); const el = getTooltipEl();
el.textContent = currentText; el.textContent = currentText;
el.style.left = `${mouseX + 12}px`;
// Position near cursor, clamped to viewport el.style.top = `${mouseY + 16}px`;
const x = Math.min(e.clientX + 12, window.innerWidth - el.offsetWidth - 8); el.classList.add(TOOLTIP_VISIBLE_CLASS);
const y = e.clientY + 20;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
el.classList.add('hu-tooltip--visible');
} }
function enter(e: MouseEvent) { function enter(e: MouseEvent) {
if (!currentText) return; if (!currentText) return;
timer = setTimeout(() => show(e), 500); mouseX = e.clientX;
mouseY = e.clientY;
timer = setTimeout(show, 500);
} }
function move(e: MouseEvent) { function move(e: MouseEvent) {
// Keep tooltip near cursor while hovering mouseX = e.clientX;
if (!tooltipEl?.classList.contains('hu-tooltip--visible')) return; mouseY = e.clientY;
const el = getTooltipEl(); if (!tooltipEl?.classList.contains(TOOLTIP_VISIBLE_CLASS)) return;
const x = Math.min(e.clientX + 12, window.innerWidth - el.offsetWidth - 8); tooltipEl.style.left = `${mouseX + 12}px`;
const y = e.clientY + 20; tooltipEl.style.top = `${mouseY + 16}px`;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
} }
function leave() { function leave() {
if (timer !== null) { if (timer !== null) { clearTimeout(timer); timer = null; }
clearTimeout(timer); tooltipEl?.classList.remove(TOOLTIP_VISIBLE_CLASS);
timer = null;
}
tooltipEl?.classList.remove('hu-tooltip--visible');
} }
node.addEventListener('mouseenter', enter); node.addEventListener('mouseenter', enter);
@@ -68,7 +54,7 @@ export function tooltip(node: HTMLElement, text: string) {
return { return {
update(newText: string) { update(newText: string) {
currentText = newText; currentText = newText;
if (tooltipEl?.classList.contains('hu-tooltip--visible')) { if (tooltipEl?.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
tooltipEl.textContent = newText; tooltipEl.textContent = newText;
} }
}, },
@@ -77,7 +63,7 @@ export function tooltip(node: HTMLElement, text: string) {
node.removeEventListener('mouseenter', enter); node.removeEventListener('mouseenter', enter);
node.removeEventListener('mousemove', move); node.removeEventListener('mousemove', move);
node.removeEventListener('mouseleave', leave); node.removeEventListener('mouseleave', leave);
tooltipEl?.classList.remove('hu-tooltip--visible'); tooltipEl?.classList.remove(TOOLTIP_VISIBLE_CLASS);
} }
}; };
} }