fix(soma navigation-menu): suppress click-close right after hover-open

Bug: hovering a NavigationMenu trigger opens its Content after the
configured \`openDelay\` (default 200ms). If the user then clicked
the trigger — naturally, after the open is visible — the click handler
saw \`isOpen=true\` and called \`closeNow()\`, shutting the menu the
user just opened. Net effect: the first click appears to open AND
close the menu in one go.

Repro (commit before fix):
- hover for ≥200ms (or any delay long enough to trigger
  \`scheduleOpen\`)
- menu opens
- click the trigger
- menu closes within milliseconds

Fix: track \`lastOpenedAt\` per NavigationMenuTriggerProvider via a
root \`\$effect\` that stamps the timestamp whenever \`isOpen\`
transitions to true (regardless of whether the open came from
hover, click, or keyboard). The click handler checks \`Date.now() -
lastOpenedAt < 250\` and silently ignores the close-toggle in that
window — treating the click as "commit the open" intent, not "toggle
shut".

Two-click toggle behavior is preserved: a click ≥250ms after the open
still closes the menu (verified: openBefore=true, stillOpenAfterClick=
true, closedAfter2ndClick=true). The 250ms threshold is slightly above
the default openDelay (200ms) so the protection covers the typical
hover-then-click sequence without making the toggle feel sluggish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent a010f8f255
commit 67726cb6f3

@ -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<HTMLButtonElement>) => {
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<HTMLButtonElement>) => {

Loading…
Cancel
Save

Powered by TurnKey Linux.