Skip to content

fix(messages): stop pointer focus pinning the message action bar - #397

Merged
wesbillman merged 7 commits into
mainfrom
message-hovers-dont-dismiss-correctly
Sep 30, 2026
Merged

wesbillman merged 7 commits into
mainfrom
message-hovers-dont-dismiss-correctly

Conversation

@matt2e

@matt2e matt2e commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

If you clicked a message action like Copy link, focus stayed on the button. Because of that, .message:focus-within kept the row's action bar visible after the mouse moved to another row, for example into the thread panel. Closing an overflow menu opened with the mouse did the same thing. The bar only went away once something else took focus.

  • Focus reveals the action bar and continuation timestamp for keyboard navigation or browser :focus-visible, not lingering pointer focus. Hovering and keyboard focus still show the bar, and an open popup still keeps it visible.
  • message-actions.spec.mjs has a new regression check: after a Copy link click the bar follows hover, and it still appears on Tab.
  • message-actions.spec.mjs and nested-replies.spec.mjs had steps that relied on pointer or programmatic focus to show the controls. Those steps now hover the row instead.

Integration review

At b1d0ab9, integrated main 5b6bd71d, including the landed #393 floating toolbar. Resolved both merge conflicts while preserving the slot/top-layer layout, pointer-open → keyboard-close focus return, and existing handoff safeguards. The floating hook now rechecks keyboard/focus-visible modality on each update instead of treating all focused descendants as a reveal. Continuation timestamps use the same modality rules.

  • Fail then pass: combining the prior fix(messages): float hover actions above scroll containers #393/fix(messages): stop pointer focus pinning the message action bar #397 heads reproduced Copy link → hover-away pinning in both Chromium and WebKit. The existing assertion passes after the hook fix. One new unit case covers pointer focus across leave, popup transitions, control removal and the keyboard fallback. Existing modal coverage now proves mouse Cancel leaves the unhovered toolbar hidden while keyboard continuation reveals it; no browser cases added/removed or assertions/budgets relaxed.
  • Exact final tree c2bbff39: full Vitest 448 files / 5,427 tests passed; 40 browser cases passed across complete message-actions, message-actions-floating, message-overlays and message-management files in Chromium/WebKit. These checks exercise real focus, popover painting, layout and hit testing, not native desktop acceptance.
  • Independent source review closed all three combined-integration findings. Required staged formatting/lint, secret scan, TypeScript, related units (39 files / 636 tests) and design gates passed; the Clippy selector correctly skipped because the PR diff has no Rust changes. All seven PR commits have DCO sign-offs.
  • Earlier shared timeline repairs are now on main through fix(messages): float hover actions above scroll containers #393; this PR no longer carries a separate timeline diff. Main advanced again to 59e87ce5 during delivery (feat(agents): port bundled agent lifecycle to Windows #417/feat: enable packaged channel writes, encrypted recipes, and direct messages #436, agent lifecycle/native writes). A merge-tree check is conflict-free; those newer changes are not part of the locally tested snapshot. Hosted CI validates the resulting merge tree.
  • Remaining: hosted CI at this new head, native app and human acceptance. No approval or merge asserted. The optional click-focus → non-focus-moving-key reveal behavior remains unchanged; ordinary Tab/Enter/Escape navigation is covered.

Demo

Recorded with a scratch Playwright script against the built app in headless Chromium; the pointer is drawn in by the script because Playwright recordings do not include it. Steps: hover Thread root 1, click Reply to open the thread panel, hover the row again, click Copy link, then move the mouse onto the messages in the thread panel.

Before (main): after the Copy link click, the left row's action bar stays pinned while the mouse hovers messages in the thread panel.

before.mp4

After (this branch): the left row's action bar hides once the mouse leaves it, and only the hovered thread message shows its actions.

after.mp4

🤖 Generated with Claude Code

Clicking a message action such as Copy link left focus on the button,
so `.message:focus-within` kept the row's action bar visible after the
mouse moved to another row (for example into the thread panel). Closing
a mouse-opened overflow menu had the same effect. The bar only went
away when something else took focus.

Gate the focus reveal for the action bar and the continuation clock on
`html[data-keyboard-navigation]`, the same modality flag that already
gates focus rings. Hover and keyboard focus still reveal the bar; open
popups still keep it visible.

Add a regression check that the bar follows hover after a Copy link
click and still appears on Tab, and switch two fixtures that relied on
pointer or programmatic focus for the reveal to hover the row instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e requested review from a team, comp615 and wesbillman as code owners September 29, 2026 07:20

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Star Lord’s automated source review (via Wes’s account)

No actionable findings in this revision. Non-blocking COMMENT only; not approval.

Reviewed head 0e3fa747d9dacf69c31c6f0c8028728380b2525d against base c6b47a5837fd8912dc84d98bb959714a23d70818 using a Git-blob-verified source snapshot, with no dirty source inputs.

  • The two selector changes reuse the existing app-root input-modality owner: navigation keys enable keyboard reveal, pointer input clears it, and ordinary hover/open-popup selectors still keep actions available. Narrow/coarse-pointer action-bar behavior is unchanged (Messages.module.css:1535–1566,1728–1735; useKeyboardFocusVisibility.ts:21–46).
  • Reviewed success/cancel focus restoration separately from error/retry behavior. Menu return and dialog focus targets remain intact. Clipboard failures retain the error toast and release the busy state for retry; report/delete errors keep their own dialog recovery. The change only stops pointer-restored focus from holding the row controls visibly open, while keyboard navigation restores reveal. These are source conclusions, not observed workflow results.
  • The added browser regression checks pointer-click → hover another row → hidden actions, then Tab → visible actions. The nested-branch fixture now uses hover for pointer hit-testing without removing its collapse/expand focus assertions.
  • Public-material check covered the PR description, complete three-file diff and commit message: no internal URLs/deployment identifiers or exposed secrets found in those surfaces. The description has no attached images to inspect.

Validation limits: no PR code, tests, builds or app workflows executed in this review. A one-time hosted check-runs snapshot at this head showed CI required, all twelve Chromium/WebKit browser shards, JavaScript, Rust/tool integration and DCO successful; Windows native validation was skipped. This is not independent runtime/native verification, human acceptance or merge authorization.

matt2e and others added 3 commits September 29, 2026 18:27
The action-bar regression check presses Tab after a pointer click on
Copy link and expects keyboard focus to reveal the bar. That only
exercises the keyboard path if focus is still on Copy link, so Tab lands
on the row's next control. Copy link is briefly disabled while copying,
and only the mocked clipboard resolving in a microtask stops the
browser's focus fixup from dropping focus to the body first.

Assert that Copy link is focused right before the Tab so a future
failure points at the fixture precondition rather than at the CSS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…k relies on

The nested-replies spec focuses the grandchild row's Reply button with
`.focus()` and expects the same-author continuation clock to stay
visible with the mouse elsewhere. Since the action bar and clock gate
their focus reveal on `html[data-keyboard-navigation]`, programmatic
focus only counts as keyboard focus because the earlier ArrowUp edit
shortcut switched the flag on and no pointer click has fired since.

Assert that the html element carries the flag right before the focus
and say so in the comment, so a click added between the two fails on
the named precondition rather than looking like a CSS regression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Gating the action bar and continuation clock focus reveal on
`html[data-keyboard-navigation]` hid them whenever focus moved without
a navigation keydown, which is how screen readers, switch access and
other assistive presses move focus. Their controls stayed focusable but
sat at opacity 0.

Add `.message:has(:focus-visible)` as a second reveal. Both Chromium and
WebKit mark assistive and keyboard focus visible and leave pointer focus
(including script focus after a click) non-visible, so the Copy link
pinning fixed in 0e3fa74 stays fixed.

That reintroduced one pointer path: a mouse-opened overflow menu closed
with Escape handed focus back to the trigger as focus-visible and pinned
the bar. A pointer-opened menu now returns focus nowhere. Base UI opens
on mousedown, so a real press carries a click count while keyboard and
assistive presses arrive as a click with 0. `finalFocus` stays a boolean
to keep Base UI's safeguard when focus has already moved elsewhere. The
edit and delete dialogs pass their own `finalFocus` and are unaffected.

Cover both paths in the browser spec (Enter on a script-focused trigger
then Escape reveals the bar; a mouse-opened menu closed with Escape
leaves the trigger unfocused and the bar hidden) and split the unit test
that expected pointer-opened menus to return focus. Each new assertion
fails without its half of the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Star Lord’s automated source review (via Wes’s account)

One P2 correctness finding in the new menu-focus policy, inline below. Non-blocking COMMENT only; not approval.

Reviewed head 95e009e4660a38e666104ce42769edf706ffafa0 against base c6b47a5837fd8912dc84d98bb959714a23d70818. This follow-up reviews the actual code changes since the earlier reviewed head 0e3fa747d9dacf69c31c6f0c8028728380b2525d, with particular attention to the newly introduced focus-restoration override. The five changed files were checked against their Git blobs; no dirty source inputs or product edits.

Scope and evidence

  • Traced the action-bar selectors, app input-modality owner, menu wrapper/callers, and Base UI 1.8.0 menu/focus implementation. The repository’s Base UI patch changes form-reset handling, not this menu behavior. The :focus-visible fallback and existing hover/open-popup reveals are retained.
  • Checked success/cancel separately from clipboard failure/retry and edit/delete/report handoffs. Clipboard rejection still clears busy state and displays the recovery notice, but mixed-input users are affected by the same focus-loss finding when trying to return to the menu. Explicit downstream dialog/editor handoffs must remain intact in the fix.
  • Public-material review covered the current PR description, all five changed files, four commit messages and extracted frames across both attached demo videos. No additional internal-link, deployment-identifier or secret exposure found in those inspected surfaces. The demos use fixture content and show the Copy link hover case; they do not establish the mixed-input menu behavior.

Validation limits

Source-only: no PR code, tests, builds or app workflows were executed. The one-time hosted check-runs snapshot for this head showed JavaScript, Rust/tool integration, measurements, DCO, security checks, all six Chromium shards and four WebKit shards successful; two WebKit shards were still in progress and Windows native validation was skipped. No CI polling or wait. Script focus + Enter/Escape coverage is not an actual screen-reader or switch-access acceptance check; native/assistive behavior remains unverified. This is not human acceptance or merge authorization.

finalFocus={!handingOffFocus}
// A pointer-opened menu hands nothing back: a trigger silently
// holding focus would pin the hover-revealed bar.
finalFocus={!handingOffFocus && !openedByPointer}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Preserve focus return when a pointer-opened menu switches to keyboard use

openedByPointer is latched only on open, so finalFocus remains false even after the user clicks the overflow button, navigates its items with ArrowDown, and presses Escape (or activates Copy message with Enter). Base UI initially moves focus into the popup; with finalFocus={false}, its close/unmount path deliberately selects no return target. Removing the focused popup therefore loses the user's focused message control instead of returning to the trigger. Enter can no longer reopen that message's menu, including for a clipboard retry. The ArrowDown already set data-keyboard-navigation, so this is active keyboard use, not stale pointer focus that should be hidden.

Please retain the existing boolean/no-focus-stealing and explicit-handoff safeguards, but allow return focus after keyboard interaction/dismissal rather than deciding the entire menu lifetime from the opening event. Keep the pointer-only hover fix, and add click → ArrowDown → Escape → focused trigger → Enter reopens coverage (plus the keyboard-selected clipboard failure/retry path). The new negative-focus test currently locks in the loss instead of checking a usable keyboard continuation. This also preserves the focus-restoration contract in src/shared/design-system/DESIGN.md:142–148.

Signed-off-by: Star Lord <b89298dbe87c6b3fd8a425b535d9d161f23c88d555a47925ae0b04dfe02b201e@buzz.block.builderlab.xyz>
@wesbillman

Copy link
Copy Markdown
Collaborator

Repaired the prior keyboard focus-return blocker in 4e3ae2cd (main 6b0730cf integrated). Pointer-open → Escape or keyboard Copy now returns focus for continuation/retry; pointer-only selection still dismisses without pinning. Synthetic item-click events are handled, with existing focus-handoff/no-steal safeguards preserved.

Independent source review clear for this repair; 28 Chromium/WebKit cases passed and required hooks pass. CI and human acceptance remain open. Integration with #393 is still pending: its JS floating-action reveal must use the same modality rule. No approval or merge asserted.

Signed-off-by: Star Lord <b89298dbe87c6b3fd8a425b535d9d161f23c88d555a47925ae0b04dfe02b201e@buzz.block.builderlab.xyz>
@wesbillman

Copy link
Copy Markdown
Collaborator

Pushed 71aece46 with current main and the CI repairs. Sent-message reveal now retains the existing late-measurement observer, launch follow survives the first intermediate scroll, and newer reader input wins permanently.

All 5,415 Vitest tests and 60 local Chromium/WebKit cases across the five affected files pass; required hooks pass. No waits, retries or geometry budgets were loosened. Hosted checks and human/native acceptance remain pending; this is not an approval.

wesbillman pushed a commit that referenced this pull request Sep 30, 2026
Integrate main #437, keeping its restoration convergence and removal
tests. Carry the shared send-reveal/bottom-follow repair and the deferred
menu-focus test barrier from #397. Toggle the conversation fixture probe
through the plugin change transaction and remove the Copy link wrapper so
the message action bar keeps its cursor DOM contract.

Signed-off-by: Groot <f4c08302591916fb4b80926bf7e18d22eef8aa0c877e4e841acc46405a45700b@buzz.block.builderlab.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Not ready to approve/merge against current main. Reviewed head 71aece46cb2485d44cfa10c18f85f21a32383ad5 against its integrated base f3fe889eec574a4ecee9b3dfa10697378ef0faa9. During review, #393 merged as 5b6bd71d389c8171652bbc009341a2e9a3b55031. GitHub reports conflicts in MessageActionBar.tsx and Messages.module.css; independently reproduced with git merge-tree.

  • Original-base result: the previous mixed-input focus finding is repaired, including keyboard clipboard retry and explicit focus handoffs. No additional material source defect found in that diff. The timeline repair is already on main through #393; preserve it rather than layering another repair.
  • Integration gate: preserve #393’s floating structure and #397’s menu-close modality, and move the keyboard/:focus-visible reveal rule into useFloatingActionBar. That hook treats any retained focus as revealed, so resolving only the CSS conflict leaves the original pointer-pinning defect. Keep continuation timestamps consistent. Check the combined pointer-leave, mixed-input close/reopen, clipboard retry and floating-toolbar workflows in Chromium/WebKit, then obtain fresh required CI and human acceptance.
  • Evidence/limits: required CI/DCO and all 12 browser shards passed for the old-base head. This review used source/caller/test inspection and merge reproduction, not new local suites or native/assistive testing. Windows native validation was skipped; two documented WebKit scroll cases remain local-only. Full diff, commit messages, PR text and sampled demo frames showed no public-material exposure in those inspected surfaces.

This is a merge/integration hold, not a newly found defect in the original-base revision. COMMENT only; no approval or merge performed.

Signed-off-by: Star Lord <b89298dbe87c6b3fd8a425b535d9d161f23c88d555a47925ae0b04dfe02b201e@buzz.block.builderlab.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

The earlier integration hold is cleared. No remaining code blockers found in this follow-up. Reviewed head b1d0ab944cfea0542c35faa3cc78a3ec0fd0b4be against integrated base 5b6bd71d389c8171652bbc009341a2e9a3b55031.

  • The floating hook now rechecks keyboard/:focus-visible modality instead of retaining pointer focus, with matching continuation timestamps. Mixed-input menu close/retry and explicit focus-handoff protections remain intact; the timeline diff is gone. Regression coverage checks pointer leave and keyboard continuation across the combined floating-toolbar behavior.
  • Current main 59e87ce5de792737a93d335ea345f7d829bc61ac merges without conflicts. Local git merge-tree produced the same tree as GitHub’s tested merge 51ec9b8aa82fba07b3fa70ae374db1ed9aee4534. Hosted JavaScript logs confirm 448 files / 5,436 tests passed on that merge; all 12 Chromium/WebKit shards, CI required, Rust/tool integration, security and DCO checks passed.
  • This is source and existing-CI verification, not a new local/native run. Windows native validation was skipped. Human app acceptance is not established by this review; if that smoke test is accepted, I see no further engineering gate to merging.

COMMENT only. No approval or merge performed.

@wesbillman
wesbillman merged commit b2f245e into main Sep 30, 2026
20 checks passed
@wesbillman
wesbillman deleted the message-hovers-dont-dismiss-correctly branch September 30, 2026 01:10
wesbillman pushed a commit that referenced this pull request Sep 30, 2026
Integrate main through #440. The shared send-reveal, bottom-follow and
Copy link repairs #401 carried have landed in main via #393/#397/#437;
resolve MessageActionBar to main so its floating popover slot replaces
the data-open bar. #440 sidebar preferences do not overlap #401.

Signed-off-by: Groot <f4c08302591916fb4b80926bf7e18d22eef8aa0c877e4e841acc46405a45700b@buzz.block.builderlab.xyz>
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.

2 participants