tooltip: kill stuck tips with watchdog + pointermove sanity
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) <noreply@anthropic.com>
This commit is contained in:
1 parent
64be9257f2
commit
27cc3f98ac
3 files changed
+29
-2
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('APP_NAME', 'hyprcrd');
|
define('APP_NAME', 'hyprcrd');
|
||||||
define('APP_VERSION', '1.9.12');
|
define('APP_VERSION', '1.9.13');
|
||||||
|
|
||||||
// Database path — writable directory, outside webroot
|
// Database path — writable directory, outside webroot
|
||||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyprcrd",
|
"name": "hyprcrd",
|
||||||
"version": "1.9.12",
|
"version": "1.9.13",
|
||||||
"description": "hyprcrd — self-hosted card manager",
|
"description": "hyprcrd — self-hosted card manager",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@
|
|||||||
|
|
||||||
const SHOW_DELAY = 400;
|
const SHOW_DELAY = 400;
|
||||||
const HIDE_DELAY = 100;
|
const HIDE_DELAY = 100;
|
||||||
|
const MAX_LIFE = 5000; // watchdog: tooltip never outlives this
|
||||||
const OFFSET = 8;
|
const OFFSET = 8;
|
||||||
|
|
||||||
let tipEl = null;
|
let tipEl = null;
|
||||||
let showTimer = null;
|
let showTimer = null;
|
||||||
let hideTimer = null;
|
let hideTimer = null;
|
||||||
|
let lifeTimer = null;
|
||||||
let currentTarget = null;
|
let currentTarget = null;
|
||||||
|
|
||||||
function create() {
|
function create() {
|
||||||
@@ -63,10 +65,18 @@ function show(target) {
|
|||||||
tipEl.style.top = top + 'px';
|
tipEl.style.top = top + 'px';
|
||||||
tipEl.style.left = left + 'px';
|
tipEl.style.left = left + 'px';
|
||||||
tipEl.style.visibility = '';
|
tipEl.style.visibility = '';
|
||||||
|
|
||||||
|
// Watchdog: if pointerleave never fires (fast flick off the
|
||||||
|
// element, target removed from the DOM mid-hover, focus taken by
|
||||||
|
// another window), the tooltip would sit forever. Force-hide
|
||||||
|
// after MAX_LIFE regardless.
|
||||||
|
clearTimeout(lifeTimer);
|
||||||
|
lifeTimer = setTimeout(hide, MAX_LIFE);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
currentTarget = null;
|
currentTarget = null;
|
||||||
|
clearTimeout(lifeTimer);
|
||||||
if (tipEl) tipEl.style.display = 'none';
|
if (tipEl) tipEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,3 +101,20 @@ document.addEventListener('pointerleave', onLeave, true);
|
|||||||
document.addEventListener('pointerdown', () => { clearTimeout(showTimer); hide(); }, true);
|
document.addEventListener('pointerdown', () => { clearTimeout(showTimer); hide(); }, true);
|
||||||
document.addEventListener('keydown', () => { clearTimeout(showTimer); hide(); }, true);
|
document.addEventListener('keydown', () => { clearTimeout(showTimer); hide(); }, true);
|
||||||
document.addEventListener('scroll', () => { 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(); } });
|
||||||
Reference in new issue
Block a user