From d1500c6ac3813eedf4de4d53df9472c2b1978311 Mon Sep 17 00:00:00 2001 From: Falkan Date: Thu, 19 Mar 2026 00:24:20 -0400 Subject: [PATCH] fix: tooltip tracks current mouse position, opens at cursor --- src/lib/actions/tooltip.ts | 52 ++++++++++++++------------------------ 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/lib/actions/tooltip.ts b/src/lib/actions/tooltip.ts index 9344be4..717a342 100644 --- a/src/lib/actions/tooltip.ts +++ b/src/lib/actions/tooltip.ts @@ -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_VISIBLE_CLASS = 'hu-tooltip--visible'; -// Shared tooltip element — one per page, repositioned as needed. let tooltipEl: HTMLDivElement | null = null; function getTooltipEl(): HTMLDivElement { @@ -24,41 +15,36 @@ function getTooltipEl(): HTMLDivElement { export function tooltip(node: HTMLElement, text: string) { let timer: ReturnType | null = null; let currentText = text; + let mouseX = 0; + let mouseY = 0; - function show(e: MouseEvent) { + function show() { if (!currentText) return; const el = getTooltipEl(); el.textContent = currentText; - - // Position near cursor, clamped to viewport - const x = Math.min(e.clientX + 12, window.innerWidth - el.offsetWidth - 8); - const y = e.clientY + 20; - el.style.left = `${x}px`; - el.style.top = `${y}px`; - el.classList.add('hu-tooltip--visible'); + el.style.left = `${mouseX + 12}px`; + el.style.top = `${mouseY + 16}px`; + el.classList.add(TOOLTIP_VISIBLE_CLASS); } function enter(e: MouseEvent) { if (!currentText) return; - timer = setTimeout(() => show(e), 500); + mouseX = e.clientX; + mouseY = e.clientY; + timer = setTimeout(show, 500); } function move(e: MouseEvent) { - // Keep tooltip near cursor while hovering - if (!tooltipEl?.classList.contains('hu-tooltip--visible')) return; - const el = getTooltipEl(); - const x = Math.min(e.clientX + 12, window.innerWidth - el.offsetWidth - 8); - const y = e.clientY + 20; - el.style.left = `${x}px`; - el.style.top = `${y}px`; + mouseX = e.clientX; + mouseY = e.clientY; + if (!tooltipEl?.classList.contains(TOOLTIP_VISIBLE_CLASS)) return; + tooltipEl.style.left = `${mouseX + 12}px`; + tooltipEl.style.top = `${mouseY + 16}px`; } function leave() { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - tooltipEl?.classList.remove('hu-tooltip--visible'); + if (timer !== null) { clearTimeout(timer); timer = null; } + tooltipEl?.classList.remove(TOOLTIP_VISIBLE_CLASS); } node.addEventListener('mouseenter', enter); @@ -68,7 +54,7 @@ export function tooltip(node: HTMLElement, text: string) { return { update(newText: string) { currentText = newText; - if (tooltipEl?.classList.contains('hu-tooltip--visible')) { + if (tooltipEl?.classList.contains(TOOLTIP_VISIBLE_CLASS)) { tooltipEl.textContent = newText; } }, @@ -77,7 +63,7 @@ export function tooltip(node: HTMLElement, text: string) { node.removeEventListener('mouseenter', enter); node.removeEventListener('mousemove', move); node.removeEventListener('mouseleave', leave); - tooltipEl?.classList.remove('hu-tooltip--visible'); + tooltipEl?.classList.remove(TOOLTIP_VISIBLE_CLASS); } }; }