From 27cc3f98acbd4047d232a274f2d801e647bce345 Mon Sep 17 00:00:00 2001 From: th Date: Wed, 15 Apr 2026 21:43:12 +0200 Subject: [PATCH] tooltip: kill stuck tips with watchdog + pointermove sanity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tooltips could strand on screen when pointerleave didn't fire — fast flicks off the target, re-renders that yanked the element out mid- hover, or the tab losing focus. Three guards: - 5s max-life watchdog on every show, so the worst case is a late fade rather than a permanent overlay - pointermove check: if the pointer is no longer over the tip's owning target (or the target got detached from the DOM), hide - window blur + visibilitychange also hide immediately Co-Authored-By: Claude Opus 4.6 (1M context) --- config.php | 2 +- electron/package.json | 2 +- public/js/modules/tooltip.js | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/config.php b/config.php index 77c3626..d26cc3a 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ { clearTimeout(showTimer); hide(); }, true); document.addEventListener('keydown', () => { clearTimeout(showTimer); hide(); }, true); document.addEventListener('scroll', () => { clearTimeout(showTimer); hide(); }, true); + +// Belt-and-suspenders sanity check on every move: if the pointer is +// no longer over the tip-owning target (or the target has been +// detached from the DOM since show time), hide. Catches fast flicks +// that skip the expected pointerleave and DOM churn that yanks the +// element out from under us. +document.addEventListener('pointermove', (e) => { + if (!currentTarget) return; + if (!currentTarget.isConnected) { hide(); return; } + const el = document.elementFromPoint(e.clientX, e.clientY); + if (!el || !currentTarget.contains(el)) hide(); +}, true); + +// Window losing focus / tab hidden — nothing to hover over while +// we're not there, kill any lingering tip. +window.addEventListener('blur', () => { clearTimeout(showTimer); hide(); }); +document.addEventListener('visibilitychange', () => { if (document.hidden) { clearTimeout(showTimer); hide(); } });