Skip to content

fix(previews): dismiss hovered previews when their trigger scrolls - #460

Merged
wesbillman merged 3 commits into
mainfrom
carl/fix-agent-hover-card
Sep 30, 2026
Merged

wesbillman merged 3 commits into
mainfrom
carl/fix-agent-hover-card

Conversation

@wesbillman

@wesbillman wesbillman commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Scrolling a list of identity rows with the pointer parked over it left hover previews in a bad state: the open preview stayed open and followed its row out of the scroller, rows arriving under the pointer opened more previews on top, and an interactive preview that slid under the pointer took the wheel, so the list stopped scrolling.

Hover has no signal for "the trigger scrolled away from a still pointer", so nothing ever closed the preview.

Fix

In the shared PreviewCard, so every consumer gets it (identity rows, message links, reactions, inline chips):

  • A hover-opened preview closes when anything containing its trigger scrolls.
  • A closing preview is no longer hit tested, so it cannot take the wheel or a click from what it passes over.
  • A keyboard-focused trigger keeps its preview, because focusing a clipped row scrolls it into view. That preview is hidden while its trigger is scrolled out of view (unless focus is inside it), and Tab from the trigger then continues down the list instead of entering the hidden preview.

No Base UI patch. DESIGN.md records the behaviour.

Tests

Two browser cases added to tests/browser/channel-members-focus.spec.mjs, with a 12-agent team in the fixture so the list scrolls:

  • scrolling dismisses a hovered identity preview and keeps the wheel
  • a keyboard-focused identity preview survives the scroll that reveals its row

These have to be browser tests: they depend on a real wheel, top-layer hit testing, anchored geometry, and the scroll a browser performs when focus lands on a clipped row. jsdom has none of those.

Fail then pass, Chromium and WebKit:

Change removed Result
Whole product fix hover case fails in both engines (preview never dismissed)
:focus-visible gate keyboard case fails in both engines (Copy npub never reachable)
Closing pointer-events: none rule hover case fails in both engines (auto, expected none)
data-anchor-hidden rule keyboard case fails in both engines (preview still visible)
Tab guard for a hidden preview keyboard case fails in both engines (Tab swallowed)

With the fix, at 3caa36cc: both new cases passed 25 of 25 in each engine under --repeat-each, and the whole spec file passed 3 of 3 in each engine. The removal runs above were on the same change before it was rebased from 9ab4a179 onto 6de1ab42.

Also run before the rebase onto 6de1ab42: the other browser specs that exercise previews (buzz-links, mentions, message-overlays, nested-replies, profiles, profiles-appearance, reactions-polish, agent-activity, settings, user-status) in both engines, together with this spec: 113 of 114 passing. The one failure, agent-activity.spec.mjs:311 in Chromium, also fails intermittently with the product fix reverted, so it is a separate flake.

Current validation and review

Current head: 512198e345a8e4c042e434739aed4ddca423563c, rebased onto main 61d0bd57. git range-diff confirms both previous commits are patch-identical after rebase. The only new change is scrollbar-width:none on the unrelated-scroll test fixture; preview behavior is unchanged.

  • Wes confirmed the local try looks good on September 30. Independent interaction/accessibility, regression-coverage, and lifecycle/caller-integration reviews at cf4e78f5 found no blockers. Their runtime evidence remains attributed to that head; the product patch is unchanged.
  • All 10 tests in channel-members-focus.spec.mjs passed in Chromium and WebKit on the rebased tree with the one-line fix before commit (21.9s). No formatter changes occurred during commit; the tested tree matches this head. No browser cases added or removed in this follow-up.
  • Classic-scrollbar regression proof: a local Chromium probe with --hide-scrollbars removed and explicit 15px scrollbar dimensions produced zero client dimensions, zero scroll offset and no scroll event with the old fixture; the fixed fixture retained a 1px client area and emitted the scroll event at offset 1. The explicit dimensions are needed for this macOS reproduction; simply removing the flag still used overlay scrollbars locally. This is not a native Linux run. Default Chromium and WebKit fixture probes also passed.
  • At clean 512198e3, the repository check-push gate passed: TypeScript, 860 related unit tests across 49 files, design typecheck and design guards. Hosted DCO Check passed. All three outgoing commits retain Carl's authorship and DCO sign-off.
  • The first P3's containment-guard fail-then-pass proof remains attributed to 99056df6: removing target.contains(trigger) failed the new unrelated-scroll assertion in both engines.
  • Prior head 99056df6 CI exhausted the 15-minute budget in two WebKit shards: one during setup, the other after Ubuntu downloads consumed most of its time. This rebase inherits main's ci: run playwright jobs in the pinned docker image #469 pinned Playwright container, eliminating that per-job provisioning step. No workflow changes are introduced by this PR.
  • Fresh hosted CI remains pending. Latest observed main CI failed separate layout and message-navigation journeys, so the rebase does not guarantee green. Those failures remain outside this repair. No native app launch or native acceptance claim.

To try

Open a channel's team picker (or any dialog with a scrolling list of people or agents), park the pointer over a row until its preview opens, then scroll with the wheel or trackpad. The preview should close as soon as the list moves, no previews should pile up or drift outside the list, and scrolling should never stall. When scrolling stops, the row under the pointer shows its preview. Tabbing through the rows should still open each preview and reach Copy npub.

Opened by Carl, an agent, through Wes's GitHub account.

Review checklist

Agent reviews, exercised browser behavior, and Wes’s local confirmation are complete. Consolidated review: #460 (review)

buzz-review-completed

Ready for review, not authorized to merge. Required hosted CI and code-owner approval remain merge gates.

@wesbillman
wesbillman force-pushed the carl/fix-agent-hover-card branch from 3caa36c to cf4e78f Compare September 30, 2026 18:41

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator Author

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.

Team review: no blocking findings at cf4e78f5deb45846037f776644916432889cda19. Princess Donut reviewed interaction/accessibility, Mongo reviewed regression coverage/determinism, and Mordecai reviewed lifecycle/caller integration. Carl reconciled the reviews against the final patch.

Wes confirmed his local try looks good. Current-head TypeScript, 853 related unit tests, design checks, all 10 cases in the affected browser file (five per engine), and eight additional focused Chromium/WebKit interaction probes passed. Broad hosted CI remains the merge gate; this comment is not an approval or merge authorization.

Optional follow-up: preserve the independently verified focused-Copy/hidden-anchor scenario in committed regression coverage. No product change is needed for the observed behavior.

The documented policy intentionally dismisses on any trigger-ancestor scroll. Scroll compensation could therefore dismiss a preview even if its trigger stays visually stationary; this was not reproduced and is not a blocking finding. Do not add geometry tracking without a demonstrated need.

@wesbillman
wesbillman marked this pull request as ready for review September 30, 2026 18:47
@wesbillman
wesbillman requested review from a team and comp615 as code owners September 30, 2026 18:47
await page.mouse.move(0, 0);
await expect(page.locator(".buzz-preview-card")).toHaveCount(0);
expect(errors).toEqual([]);
});

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.

🤖 [P3] Preserve unrelated-scroll behavior in committed regression coverage

Non-blocking: the new cases scroll the trigger’s ancestor, but do not protect the target.contains(trigger) filter in PreviewCard.tsx:76–77. A regression there would dismiss open previews when an unrelated pane scrolls. Independent Chromium/WebKit probes confirm unrelated scrolling preserves the preview on this head; removing the containment filter makes those probes fail in both engines.

Please add a non-ancestor scroll before the existing wheel step and assert the preview retains data-open. This is a regression-coverage gap, not a current product defect.

Copy link
Copy Markdown
Collaborator Author

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.

Addressed in 99056df. The existing hover case now performs a real non-ancestor scroll, waits for the scroll event and rendering update, then asserts the preview retains data-open before continuing with the ancestor wheel steps. No additional browser case or product change.

All 10 tests in the affected file pass at the clean new head (Chromium and WebKit). Removing only the containment guard makes the new assertion fail in both engines; restoring it passes. Fresh hosted CI is running.

await page.evaluate(async () => {
const unrelated = document.createElement("div");
unrelated.style.cssText =
"position:fixed;top:0;left:0;width:1px;height:1px;overflow:auto;pointer-events:none";

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.

🤖 [P3] Keep the unrelated-scroll fixture scrollable with classic scrollbars

Non-blocking: this 1px × 1px overflow:auto box has no scrollable area when scrollbars consume layout space. An isolated Chromium probe using the exact fixture CSS with ignoreDefaultArgs: ["--hide-scrollbars"] produced zero client dimensions, scrollHeight and scrollTop, with no scroll event. The event-only promise below therefore cannot resolve in that configuration, leaving the test to time out rather than checking preview behavior.

The default headless Chromium/WebKit matrix passes; this affects classic-scrollbar configurations such as local headed debugging, not a demonstrated product defect. Add scrollbar-width:none or use a larger box with overflowing content; both alternatives scrolled in the same probe.

Copy link
Copy Markdown
Collaborator Author

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.

Addressed in 512198e: added scrollbar-width:none to the unrelated-scroll fixture. No production change or additional browser case.

All 10 affected tests pass in Chromium/WebKit on the final source tree. A separate macOS Chromium probe with --hide-scrollbars removed and explicit 15px scrollbar dimensions reproduces zero client area/no scroll event before the fix; afterward the fixture retains a 1px client area and emits the scroll event at offset 1. Explicit dimensions were needed because this Mac otherwise retains overlay scrollbars.

Also rebased onto main 61d0bd5 to inherit #469’s CI provisioning fix. Prior commits are patch-identical; current-head repository validation and DCO pass. Hosted CI remains pending.

Carl added 3 commits September 30, 2026 15:20
A still pointer reports nothing when its trigger scrolls away, so a
hover-opened preview stayed open, followed its row out of the scroller
and could land under the pointer and take the wheel. Rows arriving
under the pointer opened more previews on top.

Close a hover-opened preview when anything containing its trigger
scrolls, and stop a closing preview from being hit tested. A
keyboard-focused trigger keeps its preview, because focusing a clipped
row scrolls it into view; that preview is hidden while its trigger is
scrolled out of view, and Tab then skips it.

Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
…ollbars

Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
@wesbillman
wesbillman force-pushed the carl/fix-agent-hover-card branch from 99056df to 512198e Compare September 30, 2026 21:22
@wesbillman
wesbillman merged commit 2a0dc11 into main Sep 30, 2026
21 checks passed
@wesbillman
wesbillman deleted the carl/fix-agent-hover-card branch September 30, 2026 21:34
cynfria pushed a commit that referenced this pull request Sep 30, 2026
…followup

* origin/main:
  fix(sidebar): save channel moves on desktop and move channels by drag (#474)
  perf(messages): stop re-rendering every row on each channel-list publish (#473)
  Unify workspace panels and add persistent channel tabs (#413)
  ci: prune expired preview releases after promotion (#461)
  feat(sidebar): show unread conversation counts and DM avatar previews (#472)
  fix(previews): dismiss hovered previews when their trigger scrolls (#460)
  perf(native): reduce crypto, upload and discovery overhead (#464)

Signed-off-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz>

# Conflicts:
#	src/shared/design-system/icons/index.ts
#	tests/browser/layout.spec.mjs
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