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 530eb21e3..76fbb5f10 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 @@ -482,6 +482,27 @@ export class NavigationMenuTriggerProvider { readonly isOpen = $derived.by(() => this.item.isOpen); + /** + * Timestamp of the last `isOpen → true` transition for this trigger. + * Used to suppress the "click immediately closes" race that hits users + * who hover briefly then click: hover schedules an open (after + * `openDelay`), the open fires, then a stray click toggles it shut. + * If a click arrives within ~250ms of the open we treat it as the + * same intent (commit the open) and ignore the close-toggle. + * + * `$effect` watches `isOpen` and stamps the time whenever it flips + * from false → true, regardless of whether the open came from hover + * scheduling, click, or keyboard. Tracked per-trigger because each + * NavigationMenuTriggerProvider is an independent instance. + */ + private lastOpenedAt = 0; + + openedAtEffect = $effect.root(() => { + $effect(() => { + if (this.isOpen) this.lastOpenedAt = Date.now(); + }); + }); + readonly onpointerenter = () => { if (this.opts.disabled.current) return; if (!this.provider.opts.hoverEnabled.current) return; @@ -495,8 +516,15 @@ export class NavigationMenuTriggerProvider { readonly onclick = (_e: SomaMouseEvent) => { if (this.opts.disabled.current) return; - if (this.isOpen) this.provider.closeNow(); - else this.provider.openNow(this.item.opts.value.current); + if (this.isOpen) { + // Suppress the close if the open is fresh — the user likely + // clicked right after hover-open, intending to commit the + // open rather than toggle it shut. + if (Date.now() - this.lastOpenedAt < 250) return; + this.provider.closeNow(); + } else { + this.provider.openNow(this.item.opts.value.current); + } }; readonly onkeydown = (e: SomaKeyboardEvent) => {