Uh oh!
There was an error while loading. Please reload this page.
fix(web): stop project-scope menu from highlighting first row without hover - #7950
fix(web): stop project-scope menu from highlighting first row without hover#7950IzonIcy wants to merge 6 commits into
Conversation
… hover Base UI focuses the first tabbable element when a menu opens with the mouse. Inside the sidebar project-scope menu every row is tabIndex=-1 until highlighted, so the first tabbable element was the settings gear inside the first project row; focus bubbling into that row made Base UI render it as highlighted although the pointer was elsewhere. Keyboard opens were unaffected because the checked row is pre-highlighted and receives focus itself. When the trigger is activated by pointer, focus the popup element during commit instead: Base UI's queued open-time autofocus sees focus is already inside the popup and skips. Keyboard opens keep the default behavior; arrow keys, Tab-to-gear, and mouse clicks on the gear still work as before.
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
One finding: the open-focus redirect is a component-local workaround that depends on Base UI's internal focus timing, while the actual cause (a tabbable Button nested inside a menu radio item) is left in place. Details inline.
Posted via Macroscope — UI Consistency
Uh oh!
There was an error while loading. Please reload this page.
The Sidebar-level layout effect raced Base UI's FloatingFocusManager: it only helps if the popup mounts in the same commit, and the manager's autofocus decision can still land on the settings gear otherwise. Move the redirect into the popup subtree as a sentinel component. React runs child layout effects before the popup's own effect, so the manager snapshots activeElement already inside the popup and skips its queued autofocus deterministically, regardless of mount timing. Behavior is unchanged for keyboard opens (sentinel not mounted) and for arrow keys, Tab-to-gear, and gear clicks.
IzonIcy
commented
Aug 23, 2026
Fixed in 437a995 — the timing critique was fair, so the redirect moved from a Sidebar-level layout effect into the popup's own subtree: a hidden sentinel child focuses |
There was a problem hiding this comment.
Reviewed the project-scope menu focus fix against the shared Menu primitive contract and Base UI 1.4.1 behavior. The layout-effect ordering the sentinel relies on does hold in 1.4.1 (FloatingFocusManager snapshots activeElement synchronously in its own layout effect and bails when focus is already inside the popup), so the mechanism itself is sound. Three smaller issues remain: a render-time ref read that the React Compiler rejects, a popup focus() call that does not mirror Base UI's preventScroll, and ownership of the behavior sitting at one call site while an identical menu composition exists elsewhere.
Posted via Macroscope — UI Consistency
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
IzonIcy
commented
Aug 23, 2026
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 437a995. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
ApprovabilityVerdict: Would Approve Macroscope's review found this PR approvable — This is a focused, opt-in UI focus correction limited to the project-scope menu, with keyboard behavior and other menus preserved. An unresolved Medium finding identifies a popup-ref merging risk, which remains a separate correctness gate under the repository’s configured threshold. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
Addresses the three Macroscope UI-consistency findings on this PR: - The sentinel no longer reaches through MenuPopup's internal DOM via closest(). MenuPopup now owns a ref to its own popup element and exposes an opt-in focusOnMountRef prop; the FocusPopupOnMount helper lives beside it so the Base UI ordering workaround is documented in one place. - The mount focus now passes preventScroll:true, matching Base UI's own open-focus behavior while the popup is still unpositioned. - projectScopeOpenedWithPointerRef.current is no longer read during render. The ref object is passed down and read inside the layout effect, keeping the component React Compiler-safe. Verified: pnpm exec tsgo --noEmit, vp lint on changed files, menu + sidebar unit tests. Model: ox-alpha (opencode/x-preview-f-free), opencode
| className, | ||
| )} | ||
| data-slot="menu-popup" | ||
| ref={popupRef} |
There was a problem hiding this comment.
🟡 Mediumui/menu.tsx:103
When a caller supplies ref, {...props} overwrites popupRef, so FocusPopupOnMount sees popupRef.current === null and the opt-in popup is not focused; the caller's ref is also the only ref attached. Merge the internal and caller refs instead of allowing the spread to replace the internal ref.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ui/menu.tsx around line 103:
When a caller supplies `ref`, `{...props}` overwrites `popupRef`, so `FocusPopupOnMount` sees `popupRef.current === null` and the opt-in popup is not focused; the caller's ref is also the only ref attached. Merge the internal and caller refs instead of allowing the spread to replace the internal ref.
Evidence trail:
3fe9398
apps/web/src/components/ui/menu.tsx:25-40
apps/web/src/components/ui/menu.tsx:54-108
apps/web/package.json:15,42
IzonIcy
commented
Aug 25, 2026
Attaching the before/after captures promised in the description (hosted on the fork's Before — pointer open, no hover: After — pointer open: focus lands on the popup, project row neutral: |
There was a problem hiding this comment.
Two findings in the shared MenuPopup primitive. The behavior goal (no pre-highlighted row on pointer opens) looks right; the concern is where the behavior lives and how it is wired.
Posted via Macroscope — UI Consistency
| function FocusPopupOnMount({ | ||
| enabled, | ||
| popupRef, | ||
| }: { | ||
| enabled: { current: boolean }; | ||
| popupRef: { current: HTMLElement | null }; | ||
| }) { | ||
| useLayoutEffect(() => { | ||
| if (!enabled.current) { | ||
| return; | ||
| } | ||
| // preventScroll matches Base UI's own open-focus behavior: this runs | ||
| // before the positioner has positioned the popup, so a scrolling focus | ||
| // could jump ancestor scrollers. | ||
| popupRef.current?.focus({ preventScroll: true }); |
There was a problem hiding this comment.
This puts popup focus ownership in a hidden child whose correctness depends on React running its layout effect before Base UI's focus manager, and it pushes interaction-type detection out to every consumer (Sidebar.tsx now tracks onPointerDown/onKeyDown on the trigger by hand). Base UI's popups already own both halves of this: Menu.Popup accepts initialFocus — the sibling of the finalFocus prop this repo already uses on a Base UI popup in CommandPalette.tsx:535 — as a ref or a callback that receives the open interaction type, and the focus manager consults it instead of being raced by effect ordering.
Suggest replacing FocusPopupOnMount + focusOnMountRef with initialFocus on MenuPrimitive.Popup, returning popupRef.current for non-keyboard opens and falling through to the default for keyboard opens (verify the exact callback return contract with a typecheck against @base-ui/react@1.5.0). That keeps the fix declarative, survives Base UI upgrades that change when the focus manager runs, and lets MenuPopup apply it by default — the same nested-control-in-first-row pattern exists at ProjectScriptsControl.tsx:214, which the current opt-in ref prop does not cover.
No diff: the fix spans the prop signature and the Sidebar trigger handlers.
Posted via Macroscope — UI Consistency
Uh oh!
There was an error while loading. Please reload this page.
t3dotgg
commented
Aug 27, 2026
Note 🤖 GPT-5.6 Sol responding on behalf of Theo We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together. Closing this alternate menu focus policy. The same project-menu highlight is covered by #7916. This version adds a shared layout-effect focus API and relies on open-time scheduling details. Keep the pointer-versus-keyboard reproduction in the selected fix and use the menu primitive's supported initial-focus behavior. If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look. |





What changed
apps/web/src/components/Sidebar.tsx: when the sidebar project-scope menu ("All projects" dropdown) is opened with a pointer, focus is redirected to the popup element itself during commit. Base UI's open-time autofocus — which queues a microtask and skips when focus already sits inside the popup — then no-ops instead of landing on the first tabbable element, the settings gear inside the first project row.Keyboard opens keep Base UI's default behavior (the checked row is pre-highlighted and receives focus itself). Arrow-key navigation, Tab-to-gear, and mouse clicks on the gear are unchanged; only the misdirected open focus moved.
Why
Fixes#7915: opening this menu with the mouse rendered the first project row as highlighted although the pointer was elsewhere, because every row is
tabIndex=-1until highlighted and focus bubbling into the gear marked its row active.Evidence
Menu opened with the pointer, nothing hovered.
Before — the
t3-demo-projectrow renders highlighted because Base UI's open autofocus landed on its settings gear:After — focus lands on the popup itself; the project row stays neutral (the checked "All projects" row keeps its selection background, as intended):
Images live on the fork's
pr-evidence/7950branch so no PR-only assets land in the repo.Verification
tsgo --noEmit, 0 errors); lint cleanFloatingFocusManagerskips its queued autofocus when focus is already inside the floating element)--
Worked by ox-alpha via opencode (x-preview-f-free).
Note
Fix project-scope menu highlighting first row when opened by pointer
projectScopeOpenedWithPointerRefin Sidebar.tsx set viaonPointerDown/onKeyDownon the menu trigger, distinguishing pointer opens from keyboard opensFocusPopupOnMountcomponent and optionalfocusOnMountRefprop toMenuPopupin menu.tsx; when enabled, the popup element itself receives focus on mount instead of the first tabbable descendantMacroscope summarized 3a0f2aa.
Note
Low Risk
Localized focus-management workaround for one menu; keyboard behavior and other menus are unchanged unless they opt into
focusOnMountRef.Overview
Fixes incorrect row highlight when the sidebar All projects menu opens via click: Base UI autofocus was landing on the first row’s nested settings gear instead of leaving rows neutral.
MenuPopupgains an optionalfocusOnMountRefand aFocusPopupOnMountlayout effect that focuses the popup container (withpreventScroll) when the ref is true at mount, so Base UI’s focus manager skips autofocus to the first tabbable descendant.Sidebar records pointer vs keyboard on the project-scope trigger (
onPointerDown/onKeyDown) and passes that ref intoMenuPopupso pointer opens redirect focus to the popup; keyboard opens keep the default pre-highlighted checked row.Reviewed by Cursor Bugbot for commit 3a0f2aa. Bugbot is set up for automated code reviews on this repo. Configure here.