From 15b5871eb918011539676167bb11062d0158f776 Mon Sep 17 00:00:00 2001 From: Falkan Date: Thu, 19 Mar 2026 00:16:49 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20tooltip=20positioning=20without=20rAF=20?= =?UTF-8?q?=E2=80=94=20measure=20synchronously=20via=20ready=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/units.json | 2 +- src/app.css | 4 +++ src/lib/actions/tooltip.ts | 73 +++++++++++++++++++++++--------------- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/data/units.json b/data/units.json index faf7d98..f0155b2 100644 --- a/data/units.json +++ b/data/units.json @@ -272,7 +272,7 @@ "id": "lengths-and-distance", "label": "Lengths and Distance", "baseUnitId": "mm", - "toUniversal": 14.2332177486, + "toUniversal": 1, "alwaysShowLabel": false, "hidden": false } diff --git a/src/app.css b/src/app.css index 2c39110..4267f03 100644 --- a/src/app.css +++ b/src/app.css @@ -469,6 +469,10 @@ .hu-tooltip--visible { opacity: 1; } +.hu-tooltip--ready { + opacity: 1; + transition: none; +} [data-theme=dan] .hu-tooltip { background: #000; color: #ffff00; diff --git a/src/lib/actions/tooltip.ts b/src/lib/actions/tooltip.ts index 93c148f..36b1244 100644 --- a/src/lib/actions/tooltip.ts +++ b/src/lib/actions/tooltip.ts @@ -8,6 +8,8 @@ */ const TOOLTIP_CLASS = 'hu-tooltip'; +const TOOLTIP_VISIBLE_CLASS = 'hu-tooltip--visible'; +const TOOLTIP_READY_CLASS = 'hu-tooltip--ready'; // Shared tooltip element — one per page, repositioned as needed. let tooltipEl: HTMLDivElement | null = null; @@ -21,48 +23,55 @@ function getTooltipEl(): HTMLDivElement { return tooltipEl; } +/** + * Position the tooltip near (cx, cy), shifting up only as much as needed + * to keep it within the viewport. Requires the element to already be in + * the DOM with its content set so offsetWidth/offsetHeight are accurate. + */ +function position(el: HTMLDivElement, cx: number, cy: number) { + const margin = 8; + const w = el.offsetWidth; + const h = el.offsetHeight; + const x = Math.min(cx + 12, window.innerWidth - w - margin); + let y = cy + 20; + const overflow = y + h + margin - window.innerHeight; + if (overflow > 0) y -= overflow; + y = Math.max(margin, y); + el.style.left = `${x}px`; + el.style.top = `${y}px`; +} + export function tooltip(node: HTMLElement, text: string) { let timer: ReturnType | null = null; let currentText = text; - function show(e: MouseEvent) { + function show(cx: number, cy: number) { if (!currentText) return; const el = getTooltipEl(); el.textContent = currentText; - // Position off-screen first so we can measure dimensions after render - el.style.left = '-9999px'; - el.style.top = '-9999px'; - el.classList.add('hu-tooltip--visible'); + // Make visible but suppress the opacity transition while we measure+position. + // --ready class sets opacity:1 with no transition; --visible adds the fade-in. + el.classList.remove(TOOLTIP_VISIBLE_CLASS); + el.classList.add(TOOLTIP_READY_CLASS); - // Use rAF to let the browser lay out the element before measuring - requestAnimationFrame(() => { - position(el, e.clientX, e.clientY); - }); - } + // offsetWidth/offsetHeight are now accurate (element is in DOM, display not none) + position(el, cx, cy); - function position(el: HTMLDivElement, cx: number, cy: number) { - const margin = 8; - const w = el.offsetWidth; - const h = el.offsetHeight; - const x = Math.min(cx + 12, window.innerWidth - w - margin); - // Place below cursor; shift up only as much as needed to fit - let y = cy + 20; - const overflow = y + h + margin - window.innerHeight; - if (overflow > 0) y -= overflow; - y = Math.max(margin, y); - el.style.left = `${x}px`; - el.style.top = `${y}px`; + // Swap to the visible class to re-enable the fade transition + el.classList.remove(TOOLTIP_READY_CLASS); + el.classList.add(TOOLTIP_VISIBLE_CLASS); } function enter(e: MouseEvent) { if (!currentText) return; - timer = setTimeout(() => show(e), 500); + const cx = e.clientX; + const cy = e.clientY; + timer = setTimeout(() => show(cx, cy), 500); } function move(e: MouseEvent) { - // Keep tooltip near cursor while hovering - if (!tooltipEl?.classList.contains('hu-tooltip--visible')) return; + if (!tooltipEl?.classList.contains(TOOLTIP_VISIBLE_CLASS)) return; position(getTooltipEl(), e.clientX, e.clientY); } @@ -71,7 +80,11 @@ export function tooltip(node: HTMLElement, text: string) { clearTimeout(timer); timer = null; } - tooltipEl?.classList.remove('hu-tooltip--visible'); + const el = tooltipEl; + if (el) { + el.classList.remove(TOOLTIP_VISIBLE_CLASS); + el.classList.remove(TOOLTIP_READY_CLASS); + } } node.addEventListener('mouseenter', enter); @@ -81,7 +94,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; } }, @@ -90,7 +103,11 @@ 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'); + const el = tooltipEl; + if (el) { + el.classList.remove(TOOLTIP_VISIBLE_CLASS); + el.classList.remove(TOOLTIP_READY_CLASS); + } } }; }