Skip to content

fix(app): keep desktop submenus open across parent items - #59625

Open
chemarma28 wants to merge 4 commits into
anomalyco:devfrom
chemarma28:submenu-close-grace
Open

chemarma28 wants to merge 4 commits into
anomalyco:devfrom
chemarma28:submenu-close-grace

Conversation

@chemarma28

Copy link
Copy Markdown

Issue for this PR

Closes #59613

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The desktop titlebar menu uses Kobalte dropdown submenus. Kobalte closes a submenu when focus moves to another parent item, and its pointer grace area only covers a direct path into the submenu. Moving down the parent menu to the last item therefore closed the submenu before the pointer reached it.

The submenu now stays open while the pointer is over the parent menu or the submenu, closes 300 ms after the pointer leaves both.

How did you verify your code works?

  • Added an e2e regression in packages/app/e2e/regression/tabs.spec.ts using the existing Windows menu fixture. It failed before the fix and passes after.
  • bun run check, tsc -b, and oxlint pass.
  • Manually checked in bun run dev:desktop on Windows in the actual Electron app: the submenu stays open while the pointer moves from its trigger to the last item, switches correctly when hovering another parent item.

Screenshots / recordings

submenu-close-grace-fix.mp4

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. It helps, but it doesn't fully fix #59613 yet.

I ran the issue's Playwright reproduction on the Windows menu test page in Chromium, on the base and with this branch. On the base, moving diagonally from File to its last item closes the File submenu, and your new regression test fails, which is good. With this branch, the same diagonal works when the pointer moves fast (about 70–160 ms). At a normal hand speed (about 400 ms, the issue's default), it still fails. The pointer rests on View for about 60 ms on its way, Kobalte opens View's submenu, it replaces File's, and Close Window is never highlighted. The change makes Kobalte's close requests wait, but it doesn't stop a sibling from opening while the pointer is heading into the open submenu. Details are inline.

In my runs a slow, deliberate hover onto a sibling still switched submenus, with only one submenu open. A quick keyboard check failed once in three runs: after ArrowDown then ArrowRight, Edit's submenu didn't open. I haven't confirmed whether that comes from this change or from my test, so it's worth a quick check, since closing and opening now also depend on the new lastInput flag.

Public API: unchanged.

The new e2e test moves the pointer with no pauses, so it passes even though a realistic move still fails (see inline).

The screenshots show a fast move on the base and with this branch, then a move at the issue's default speed (about 400 ms) with this branch, where View's submenu has replaced File's.

Before

After

Screenshot

<DesktopMenuSubmenu
label={language.t(menu.labelKey)}
open={activeSubmenu() === menu.id}
onOpenChange={(open) => (open ? openSubmenu(menu.id) : requestSubmenuClose(menu.id))}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the pointer stays on a sibling for Kobalte's open delay, Kobalte still calls onOpenChange(true) for it. openSubmenu then switches activeSubmenu to that sibling, even while the pointer is heading into the open submenu. Using the issue's diagonal path at about 400 ms (25 steps × 16 ms), aria-expanded moves from File to View after about 60 ms over View, and the last File item is never reached.

To fix #59613, the open request also needs a check: ignore it while the pointer is inside the triangle from the trigger to the open submenu (or moving toward it), and accept it once the pointer settles on the sibling. Then a deliberate hover onto a sibling still switches.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an intent-area check plus a short settle delay on the sibling.

const y = last.y + last.height / 2

await page.mouse.move(x, trigger.y + trigger.height / 2)
await page.mouse.move(x, y, { steps: 10 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mouse.move(..., { steps: 10 }) with no waits finishes in a few milliseconds, faster than Kobalte's sibling-open delay, so this test passes even though a move at normal speed still fails. Please make it follow the issue's path and speed: one straight diagonal from the trigger to the last item, about 16 ms per step. Then it catches the case that's still broken. Keep the closing check for when the pointer leaves the menu.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The e2e now uses ~400 ms diagonal path and also checks a deliberate sibling hover.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only merges dev into the branch. The changes to windows-menu.tsx and the e2e test are the same as before, so both earlier points are still open:

  • A sibling's submenu still opens when the pointer moves diagonally toward the open submenu at a normal speed.
  • The regression test moves the pointer with no pauses between steps, so it doesn't catch that case.

Public API: unchanged.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this now fixes #59613 in my runs, and both earlier points are addressed.

On the base, the issue's Playwright reproduction (a straight diagonal from File to its last item, 25 steps × 16 ms) closes the File submenu, and the new regression test fails. With this branch, the same reproduction passes at about 70, 400 and 600 ms moves. A deliberate hover onto a sibling still switches submenus, arrow keys and Escape still work, and the regression test passes 3/3. tsc -b passes. Public API: unchanged.

Not blocking: this safe-path check lives only in windows-menu.tsx. Other menus built on the shared Menu.Sub, such as the new branch and path submenus in session-identity-header.tsx, don't get it. If the same problem shows up there, the shared menu component in packages/ui may be a better place for it.

Before

After

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

desktop: submenu closes when the pointer moves to its last item

1 participant