From 88be88f2106d42dfeb466b3cd61f1055369833df Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 13:35:54 +0200 Subject: [PATCH] Route Soma navigation menu timers --- .../navigation-menu-provider.svelte.ts | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts b/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts index f989a872c..95a9876fd 100644 --- a/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts +++ b/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts @@ -23,6 +23,7 @@ import { NAVIGATION_MENU_LANGS } from './langs'; import { navigationMenuMorfo } from '../../../morfo/components/navigation-menu'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; +import type { TimerHandle } from '$timer'; const attrs = createAttrs(navigationMenuMorfo); @@ -62,9 +63,9 @@ export class NavigationMenuProvider { return new NavigationMenuProvider(opts); } - private openTimer: ReturnType | null = null; - private closeTimer: ReturnType | null = null; - private skipDelayTimer: ReturnType | null = null; + private openTimer: TimerHandle | null = null; + private closeTimer: TimerHandle | null = null; + private skipDelayTimer: TimerHandle | null = null; /** True while the "skip delay" window is open — i.e. a submenu was just closed * and the user is likely hovering another trigger right after. During this * window, `scheduleOpen` uses the shorter `skipDelayDuration`. */ @@ -107,9 +108,8 @@ export class NavigationMenuProvider { $effect(() => { return () => { - if (this.openTimer) clearTimeout(this.openTimer); - if (this.closeTimer) clearTimeout(this.closeTimer); - if (this.skipDelayTimer) clearTimeout(this.skipDelayTimer); + this.clearTimers(); + this.clearSkipDelayTimer(); }; }); } @@ -129,11 +129,11 @@ export class NavigationMenuProvider { private clearTimers() { if (this.openTimer) { - clearTimeout(this.openTimer); + this.openTimer.cancel(); this.openTimer = null; } if (this.closeTimer) { - clearTimeout(this.closeTimer); + this.closeTimer.cancel(); this.closeTimer = null; } } @@ -158,12 +158,18 @@ export class NavigationMenuProvider { private armSkipDelay() { this.inSkipDelayWindow = true; - if (this.skipDelayTimer) clearTimeout(this.skipDelayTimer); + this.clearSkipDelayTimer(); // Skip window lasts the length of `openDelay` — a reasonable "grace // period" during which the user is presumed to still be navigating. - this.skipDelayTimer = setTimeout(() => { - this.inSkipDelayWindow = false; - }, this.opts.openDelay.current); + this.skipDelayTimer = this.soma.uix.timers.schedule( + this.skipDelayTimerKey, + this.opts.openDelay.current, + () => { + this.skipDelayTimer = null; + this.inSkipDelayWindow = false; + }, + { replace: true, meta: { component: 'navigation-menu', action: 'skip-delay' } } + ); } /** @@ -186,23 +192,58 @@ export class NavigationMenuProvider { this.openNow(value); return; } - this.openTimer = setTimeout(() => this.openNow(value), delay); + this.openTimer = this.soma.uix.timers.schedule( + this.openTimerKey, + delay, + () => { + this.openTimer = null; + this.openNow(value); + }, + { replace: true, meta: { component: 'navigation-menu', action: 'open-delay' } } + ); } /** Schedule a close (grace period). */ scheduleClose() { this.clearTimers(); - this.closeTimer = setTimeout(() => this.closeNow(), this.opts.closeDelay.current); + this.closeTimer = this.soma.uix.timers.schedule( + this.closeTimerKey, + this.opts.closeDelay.current, + () => { + this.closeTimer = null; + this.closeNow(); + }, + { replace: true, meta: { component: 'navigation-menu', action: 'close-delay' } } + ); } /** Cancel a pending close (e.g. pointer entered the content). */ cancelClose() { if (this.closeTimer) { - clearTimeout(this.closeTimer); + this.closeTimer.cancel(); this.closeTimer = null; } } + private clearSkipDelayTimer() { + if (this.skipDelayTimer) { + this.skipDelayTimer.cancel(); + this.skipDelayTimer = null; + } + } + + private get openTimerKey() { + return `soma:navigation-menu:${this.opts.id.current}:open`; + } + + private get closeTimerKey() { + return `soma:navigation-menu:${this.opts.id.current}:close`; + } + + private get skipDelayTimerKey() { + return `soma:navigation-menu:${this.opts.id.current}:skip-delay`; + } + registerTrigger(value: string, el: HTMLElement) { this.triggerRefs.set(value, el); return () => {