Repository navigation
fix(web): anchor menus to the clicked trigger - #254
Conversation
WebKit does not focus a button on mouse click, so Menu read <body> from document.activeElement and opened the popup at the top-left corner. Anchor to the click's currentTarget, keeping focus as the fallback for bare open(), and re-place the open menu on resize and scroll. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Absolutely positioned descendants such as .sr-only labels escaped the scrolling frame and extended the document, so the shell could scroll up and expose the page background below it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed #254 against feat/web-rebuild (cadf0db6). The event-based anchor is the right WebKit fix, the scroll/resize re-placement is a real improvement, and the .workspace-frame-content { position: relative } change is the right containment fix for the .sr-only labels; the existing absolute tab-actions popover already has a positioned parent (.workspace-control-tab), so it keeps its anchor.
Verdict: approve with one follow-up. tsc --noEmit -p web/tsconfig.json, Prettier on the changed files and npm run build -w web pass locally. No CI checks are reported for this PR. I did not run real Safari; the PR's blur-then-click Chromium reproduction is a reasonable emulation, and the before/after measurements are good evidence.
The follow-up is on kit.tsx: the fallback is still reachable from one in-tree menu. web/src/screens/shared-task/TaskRow.tsx:84 calls bare open() from its kebab onClick (after stopPropagation()), so on WebKit that task-row menu still anchors to document.activeElement, often <body>. Pass the event there (open(event)), or explicitly state that the task-row menu is out of scope. I found no other bare open() inside a Menu trigger.
| }: { | ||
| label: string; | ||
| trigger: (open: () => void, expanded: boolean) => ReactNode; | ||
| trigger: ( |
There was a problem hiding this comment.
Keeping event optional preserves the focus fallback, but one in-tree menu still uses it: web/src/screens/shared-task/TaskRow.tsx:84 calls open() from its kebab onClick after stopPropagation(). On WebKit that still anchors to document.activeElement (often <body>), so the task-row menu remains broken. Passing the event there (open(event)) would cover it. I found no other bare open() inside a Menu trigger; the other wrapped call sites are local dialog/opening functions, not shared Menu triggers.
|
Follow-up pushed in Checks: Prettier and |
See CONTRIBUTING before starting engineering work.
Change
The Workspace actions menu (and every other shared
Menuinweb/) sometimes opened in the top-left corner instead of under its trigger.Cause.
Menutook its anchor fromdocument.activeElementwhen opened. WebKit (Safari, and WebKit-based desktop webviews) does not focus a<button>on mouse click, so the anchor was<body>and the menu was placed at(8, 8). The menu was also positioned once, so resizing the window while it was open left it stranded.Fix (
web/src/ui/kit.tsxand one call site):opennow accepts the click event and anchors toevent.currentTarget. Call sites that already useonClick={open}get this without changes; callers that use bareopen()keep the previous focus-based fallback.TaskRow.tsx) now passes its click event, so its options menu gets the same trigger anchoring.resizeand onscroll(capture), using the same clamping rules as before.No styles or tokens changed. Keyboard behaviour is unchanged: focus moves to the first item, arrow keys cycle, and Escape closes and returns focus to the trigger (this now also works after an unfocused click).
Workspace shell height (
9c3020f1)After scrolling Today, the app could slide up and show the grey page background below it.
Cause. The workspace shell is fixed at
100dvhand only.workspace-frame-contentscrolls. That scroller was not positioned, so absolutely positioned descendants (the.sr-onlylabels in Today's plan rows) used the initial containing block. They escaped the scroller and stretched the document to 1299px in a 900px viewport, so the whole shell could scroll up by about 400px.Fix (
web/src/app/workspace/workspace.cssonly):.workspace-frame-contentgetsposition: relative, so those descendants stay inside the scroller.Screenshots
Captured in the built-in Chromium browser against the local Vite dev server at
http://127.0.0.1:5175/app(Today screen, synthetic demo data), 100% zoom, device pixel ratio 2. Each menu was opened with the trigger not focused (blur()thenclick()) to reproduce WebKit's click behaviour in Chromium.cadf0db6d6942ee9d6942ee9d6942ee9Before — menu opens over the sidebar at the top-left:
After — menu opens under the Workspace button:
Screenshots are stored on the fork's
pr-evidence/web-menu-anchorbranch so this PR's diff stays at the two source files above.Checks and limits
Shell height, at
9c3020f1in the same browser and dev server: the document's scroll height equals the viewport height after scrolling each frame to the bottom, with no horizontal overflow, at 375×812 (8 routes including Today and Assistant), 768×1024 and 1440×900 (Today), and 1440×600 (all 18 sidebar routes). Before the fix, Today measured 1299px in a 900px viewport.npx prettier --check web/src/app/workspace/workspace.csspasses. Not checked: real Safari, the desktop webview and mobile clients.Menu anchoring, run on
fix/web-menu-anchoratd6942ee9:npx tsc --noEmitinweb/: exit 0.npx prettier --check web/src/ui/kit.tsx: passes.Follow-up
99818e49(3 Nov 2026): theTaskRowcall site was updated;web/typechecking passes with the ES2023 array lib, Prettier andvite buildpass. The stocknpm run buildstill stops at the pre-existingArray.findLast/ES2022 error inlibrary/page/Source.tsx. No new browser pass was run.Not verified:
onClick={open}triggers take the same path.Workspace
repo/, and read the relevant project guides.This contributor confirmation is not an automated claim that a host loaded policies.
Agent disclosure
git,gh,tsc,prettier.claude-opus-5-5); effort as set by the session.🤖 Generated with Claude Code
Follow-up fixes (3 Nov 2026)
git,gh,npm,tsc, Prettier, Vite.