fix(ui): close track menu on outside click and idle timeout

cx
This commit is contained in:
vicrodh committed 2026-01-22 12:24:44 -06:00
1 parent 9cf345921e
commit 99dfd4f0a3
1 file changed
+42 -2
+42 -2
View File
@@ -81,6 +81,7 @@
let menuStyle = $state(''); let menuStyle = $state('');
let submenuStyle = $state(''); let submenuStyle = $state('');
let downloadSubmenuStyle = $state(''); let downloadSubmenuStyle = $state('');
let isHoveringAnyMenu = $state(false);
// Portal action - moves element to body to escape stacking context // Portal action - moves element to body to escape stacking context
function portal(node: HTMLElement) { function portal(node: HTMLElement) {
@@ -250,7 +251,40 @@
// Ensure position + openSide classes are applied even if the initial openMenu() // Ensure position + openSide classes are applied even if the initial openMenu()
// call returns early due to timing. // call returns early due to timing.
setMenuPosition(); setMenuPosition();
document.addEventListener('mousedown', handleClickOutside);
// Use capture phase so stopPropagation in other UI doesn't prevent close.
const outsideListenerOptions: AddEventListenerOptions = { capture: true };
document.addEventListener('pointerdown', handleClickOutside as unknown as EventListener, outsideListenerOptions);
// Auto-close after inactivity when the cursor is not hovering any menu/submenu.
let idleTimer: ReturnType<typeof setTimeout> | null = null;
const idleMs = 20_000;
const scheduleIdleClose = () => {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (isOpen && !isHoveringAnyMenu) closeMenu();
}, idleMs);
};
const cancelIdleClose = () => {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
};
// Start the timer if the mouse isn't currently hovering the menu.
if (!isHoveringAnyMenu) scheduleIdleClose();
const onAnyActivity = () => {
if (!isHoveringAnyMenu) scheduleIdleClose();
};
window.addEventListener('pointermove', onAnyActivity, true);
window.addEventListener('wheel', onAnyActivity, true);
window.addEventListener('keydown', onAnyActivity, true);
const handleResize = () => setMenuPosition(); const handleResize = () => setMenuPosition();
const handleScroll = () => { const handleScroll = () => {
setMenuPosition(); setMenuPosition();
@@ -261,9 +295,13 @@
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll, true); window.addEventListener('scroll', handleScroll, true);
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('pointerdown', handleClickOutside as unknown as EventListener, outsideListenerOptions);
window.removeEventListener('pointermove', onAnyActivity, true);
window.removeEventListener('wheel', onAnyActivity, true);
window.removeEventListener('keydown', onAnyActivity, true);
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll, true); window.removeEventListener('scroll', handleScroll, true);
cancelIdleClose();
}; };
} }
}); });
@@ -300,6 +338,8 @@
bind:this={menuEl} bind:this={menuEl}
style={menuStyle} style={menuStyle}
use:portal use:portal
onmouseenter={() => { isHoveringAnyMenu = true; }}
onmouseleave={() => { isHoveringAnyMenu = false; }}
> >
<div class="menu-panel"> <div class="menu-panel">
{#if hasPlayback} {#if hasPlayback}