fix(sidebar): stop the workspace switcher stranding a phantom hover - #6096
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryLow Risk Overview
Adds Reviewed by Cursor Bugbot for commit b9b2efd. Configure here. |
Greptile SummaryThis PR fixes phantom workspace-switcher highlights by separating keyboard cursor painting from pointer navigation and adds regression coverage for initial, pointer, typing, and keyboard interactions. Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx | Adds input-modality state so persistent cursor highlighting appears only during keyboard navigation while pointer movement retains Enter targeting. |
| apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.test.tsx | Adds regression tests that now verify keyboard cursor painting on a non-selected workspace, resolving the prior assertion gap. |
Reviews (2): Last reviewed commit: "fix(sidebar): do not arm Enter without a..." | Re-trigger Greptile
The switcher's keyboard cursor is set from `onMouseMove` and only cleared when the menu closes, and it paints in `--surface-active` — the same token hover uses. So the last row the pointer crossed keeps a background indistinguishable from hover long after the pointer has gone, sitting alongside the equally-`--surface-active` current workspace as a second phantom-hovered row. It shows without any hovering too: the cursor is seeded to row 0 on open, so any user whose current workspace is not first sees two marked rows immediately. Only bites above the 3-workspace search threshold, which is why it went unnoticed. Paint the cursor only while the user is actually navigating by keyboard. Arrow keys enter that mode, any pointer motion leaves it, so in pointer mode the sole mark is real CSS :hover — which follows the pointer and leaves with it. `highlightedId` still tracks the pointer, so Enter keeps targeting the row last touched; only whether it is drawn changes. This is the pattern emcn's own popover already uses (`isKeyboardNav`, commented "prevent dual highlights") and that `tag-dropdown` consumes, and it matches how Headless UI models a combobox: one modality-driven focus marker, separate from the selected value. The list carries no `aria-activedescendant`/`aria-selected`, so the highlight is purely visual and nothing in the a11y contract changes.
Review round 1: the keyboard-positive assertions landed on the current workspace, which carries its own `isActive` fill, so they held whether or not the cursor was painted — the tests could not have caught deleting keyboard-cursor rendering. Navigate with ArrowUp instead, wrapping from the seeded first row to the last, and assert on that row. Verified both directions now: removing the modality gate reddens three tests, removing keyboard painting reddens two.
Review round 1: gating the cursor's paint on keyboard mode left Enter still acting on the seeded first row while that row was unmarked. The search field takes focus on open, so Enter could switch workspace with nothing shown as the target — emcn's popover instead holds its selection at -1 and ignores Enter until keyboard nav begins. Enter now acts only once a cursor is on screen, and typing counts as keyboard intent so the common "filter, then Enter" flow lands on a visible top result. Every path to Enter therefore has a marked target.
c35953b to
b9b2efd
Compare
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b9b2efd. Configure here.
The report
In the workspace switcher, a row keeps a hover-looking highlight after the pointer has moved away — so two rows appear hovered at once (the selected workspace, plus a stale one).
Cause
Not a hover at all. #5893 added keyboard navigation to the switcher, tracked by
highlightedId. Three things combine:highlightedIdis set fromonMouseMove(workspace-header.tsx:578) — deliberately, sincemouseenterwould hijack the keyboard selection whenscrollIntoViewslides rows under a stationary cursor.onMouseLeavein the file; it only resets when the whole menu closes.chipVariants({ active }), and inchip.tsxactive: true→bg-[var(--surface-active)]whileactive: false→hover-hover:bg-[var(--surface-active)]. Identical token (#ececec/#2c2c2c).So the last row the pointer crossed keeps a background indistinguishable from hover, forever — next to the equally-
--surface-activecurrent workspace.It also happens with no hovering at all: the seeding effect (
:214) puts the cursor on row 0 at open, so any user whose current workspace isn't first sees two marked rows immediately. Only reachable aboveWORKSPACE_SEARCH_THRESHOLD(3 workspaces), which is why it went unnoticed.Fix
Paint the cursor only while the user is actually navigating by keyboard. Arrow keys enter that mode; any pointer motion leaves it. In pointer mode the only mark is real CSS
:hover, which follows the pointer and leaves with it.highlightedIdstill tracks the pointer, so Enter keeps targeting the row last touched — only whether it's drawn changes.Why this approach
packages/emcn/src/components/popover/popover.tsxtracksisKeyboardNavunder the literal comment "Suppress hover when in keyboard mode to prevent dual highlights", andtag-dropdown.tsxconsumes it.data-focus, "focused via mouse or keyboard") kept separate fromdata-selected(the current value).aria-activedescendant,aria-selected, or listbox/option roles, so the highlight is purely visual and no a11y contract changes.Why not
onMouseLeaveIt's a normal pattern (19 files use it) but the wrong tool here, and it wouldn't work: the seeding effect at
:214re-seedshighlightedIdtofilteredWorkspaces[0]whenever the current one is absent, so clearing tonullbounces the highlight to row 1 rather than removing it. It would also silently retarget Enter (activeIndexfalls back to0), do nothing for the on-open case, and fire spuriously when a row's...menu portals out.Testing
New
workspace-header.test.tsxwith 4 tests, deliberately rendering the real@sim/emcn— mockingchipVariantswould only assert the stub.All three highlight tests were verified to fail without the fix and pass with it. Two fixture details that matter:
isMarkedmatches an exact class token, never a substring —hover-hover:bg-[var(--surface-active)]contains the active class, so a substring check reports every row as marked.