Repository navigation
Wait for a resting pointer before hover content opens - #1641
Conversation
ede4bed to
49226e7
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Reviewed commit 49226e7. Three findings:
- [P2] Quick copy produces two confirmations —
src/app/CommunityAddress.tsx:66–72,82–99. With the new 500ms tooltip rest andcloseOnClick={false}, clicking before the hint opens emits “Community URL copied.”, then the pending timer opens “✓ Copied”. Reproduced with mouseenter → mousemove → click after 100ms → advance 500ms; the base showed only the tooltip. Suppress the pending hover confirmation after a toast until pointer leave, or otherwise retain one confirmation path. Add a click-before-rest regression test. - [P3] Update the Members history contract —
docs/channels.md:788–791. The paragraph still says dismissal creates a new visit so Back recovers Members. The implementation now traverses to the previous visit or replaces Members, including profile handoff. Update this paragraph and document the reload fallback, which replaces the entry and leaves a same-target Back step. - [P3] Protect pressed-trigger cancellation —
src/shared/design-system/ui/Tooltip.tsx:54–75,87–91. Disabling the cancellation branch still passed 158 files / 2,913 related Vitest tests. Add a regression test for pointerdown → focus-visible focus → no hint, then Tab → focus → hint. This is a test-coverage gap, not a separate reproduced runtime defect.
…opens Hover hints, previews and hover-opened controls now open only once the pointer rests on their trigger, so crossing an avatar or toolbar on the way somewhere else opens nothing. Keyboard focus still opens hints and previews at once. Every hover surface picks a named timing bucket (familiar hint, helpful hint, preview, hover control) from hoverIntent.ts; the components no longer accept a raw delay, and DESIGN.md documents the buckets. Also in this polish pass: - Follow-up messages in a group no longer show a whole-message date tooltip; the first message's time keeps its tooltip. - Closing Channel access returns to the visit it was opened from instead of adding a new one, so Back no longer reopens it in a loop. Signed-off-by: morgmart <[email protected]> Co-authored-by: Morgarita <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@buzz.block.builderlab.xyz> Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]>
…th resting hovers Opening a profile from Members closed the list by traversing Back, which dropped the profile panel that had just opened. A handoff now replaces the Members visit in place; plain dismissal still returns to the opener. A profile preview reopens after return focus only once the pointer has left the avatar. The check recomputed "outside" on every move, so a rest-delayed open saw the pointer back inside and refused it. It now remembers the exit. Browser specs now follow the hover timing buckets, the removed follow-up message date tooltip, and main's "Manage members" trigger name. Signed-off-by: morgmart <[email protected]> Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]>
…us return When a menu opened from a hinted trigger closes, focus returns to the trigger. Base UI only suppresses that focus hint if the hint had already opened on hover, which no longer happens before a quick click now that hints wait for a resting pointer. The returned hint then swallowed the next Escape. A press now suppresses focus hints until the user tabs. Signed-off-by: morgmart <[email protected]> Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clicking the community address before its hint rests showed a toast, and the pending hint then opened with a second "Copied" confirmation. A toast now keeps that hint closed until the pointer leaves. Also adds a regression test for pressed triggers keeping their hint closed when focus returns until the user tabs, and updates the Members history contract: dismissal returns to the visit it was opened from, or replaces the Members visit when that visit is unknown, such as after a reload. Signed-off-by: morgmart <[email protected]> Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]>
49226e7 to
7b8cc83
Compare
|
🤖 Carl, an automated reviewer, commenting via Morgan's GitHub account. Thanks @kalvinnchau. All three are addressed in 7b8cc83, which is rebased on the Base UI 1.9.0 upgrade. The preview-card rest change is carried into the 1.9.0 patch.
Typecheck and the full unit suite pass at this head. |
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Two remaining findings: copy-hint suppression outlives its interaction, and the Members documentation misstates DM history behavior. Exact-head CI also has failing JavaScript and WebKit shards; their relationship to this change is unconfirmed.
A copy confirmed by toast kept the address hint closed until the pointer left, so a keyboard visit never released it, and a copy finishing after the pointer left suppressed the next hover. Suppression now belongs to the visit that copied: leaving by pointer or focus ends it, and a late result cannot start it. The Members history notes now separate profile handoffs, which replace the Members visit, from DMs, which add a visit after it. Signed-off-by: morgmart <[email protected]> Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]>
wesbillman
left a comment
There was a problem hiding this comment.
🤖 No changes requested. Star Lord’s automated source review via Wes’s account integrates independent Rocket, Mantis and Groot lanes at head fcf4c85, base 848288b.
The hover timing, focus handoffs and Members dismissal changes match the stated behavior; no new actionable defects were found. Source-only: tests and app behavior were not executed, CI was not assessed, and the disclosed pending-traversal/navigation race remains a limitation; this COMMENT is not approval.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Reviewed fcf4c85. Both prior findings are repaired; no new actionable findings. Clipboard lifecycle regressions pass in Chromium and WebKit, and exact-head CI passed. A pre-existing stale “Copied” label after delayed completion remains; native clipboard and real-relay behavior were not validated.
… Back/Forward destination Follow-up to cd78176 and d36df14, which kept channel Members and the Agents provider dialog out of history. Settings was the last address-driven dialog that still consumed Back: it pushed a visit on open, one per section switch, and Close traversed back with `returnTo`. It now follows the same rule: dialogs never create a history visit, Back while one is open leaves the page, and reload/Forward restore the dialog from the address. Design (option A of the plan): home, page and conversation targets gain an optional `overlay: { kind: "settings", section?, scope? }`. `pageKey` strips it (and keys the cold `kind: "settings"` form as one page), `targetKey` keeps it, so the controller's existing forced in-place rule handles Settings with no Settings knowledge. `provideNavigation` wraps `open`: a `kind: "settings"` intent opened while a page visit is current is rewritten to that visit plus the overlay (scope recorded only when it differs from the page's), so every existing caller - shortcut, Cmd+K, pickers, section switch, plugins - complies unchanged. The bare `kind: "settings"` target remains the cold form of saved addresses; Close there still falls back to Messages, now with `replace`. - `useAppNavigation`: `settings`/`section`/scope come from `settingsOf` and `targetScope`; page availability is not checked for the page beneath Settings (Close re-checks it). `leaveSettings` opens `stripOverlay(current)` with `{ replace: true, focus: "preserve" }`; `lastNonSettings` is gone. Legacy Home/Inbox/Bestie/Messages-settings resolutions carry the overlay. A retained workspace is only shown and kept when it is the page the Settings visit covers. - `App.tsx`: focus restore keys on the covered page's target, not a visit id. `openSettingsSection` opens a fresh `{ Messages, scope: null, overlay }` visit when the community beneath is no longer joined (the post-departure Profile), since that page can't be returned to. - `Settings.tsx` reads its section via `settingsOf`. - `returnTo` is deleted: the option, the controller's deferred return, and both drivers' `returnTo` (plus the browser driver's visit index). One deviation from the plan, forced by evidence: the plan recommended dropping `resume` and handing the page a fresh request on Close. The App.profile tests showed ChannelsPage treats any new request as a new visit (`open(undefined)` dismisses the New-message composer; `enterVisit` resets selection), so Close lost the draft and its focus. Instead, a presentation-only write now goes through `history.resolve`, so the visit keeps its identity (Members and Agents included; their tests flip from "new id" to "same id"), and the existing `resume`/replay keyed on id+target stays, keyed on `settingsOf`. That also removes the plan's objection to resuming (a stale `entryId` for the Members guard). Tests: targets (overlay stripping across kinds, rejection of bad overlays, cold form), service (intent rewritten onto the page, scope dedupe, cross-community scope kept, cold form stays bare; retained workspace resumes through a replace Close), controller (Settings overlay in place with `replace: false`, Back leaves, Forward restores, Close in place, another page pushes, cold sections replace), App-level (entity-navigation: open/section/Close leave `history.length` and canGoForward unchanged, Back lands before the page, Forward restores page+Settings, Close keeps the visit; Close over a failed destination keeps that visit; departure opens a new Personal Profile visit; cross-community Back case now travels via a page). Notifications tests expect the presented form. New `tests/browser/settings-history.spec.mjs` covers real history.length, Back/Forward and reload; `launch-home` Closes instead of goBack. Docs: shell-design and plugin-architecture state the rule and the `overlay` contract. Validation: `tsc --noEmit` clean; biome check clean on changed files. vitest: navigation, notifications, entity-navigation, navigation.test, App.profile, Settings, NotificationSettings, SearchSettings, ChannelsPage.test, agents: 36 files, 515 passed. Playwright settings-history, channel-members, launch-home, navigation-repairs, settings specs: chromium 31/31, webkit 31/31 (the four-spec set also passed 18/18 on both engines before the resume change). Full `vitest run` twice under a load average of 47-61 from other work on this machine: run one 7 failed / 11859 passed (App.profile x2, notifications x4 fixed above; WorkflowEditor passed alone 22/22); run two 33 failed / 11835 passed, all 5-20 s timeouts in codex-sh, claude-sh, project-git, reminders and other files this change does not touch; rerunning those ten files alone still left 7 shell-spawn timeouts, so they are not shown green here. Not exercised: the native desktop app or a human walkthrough. Known gap: on a cold overlay visit (reload with Settings open, or Forward into one) the shell beneath the modal is empty until Close, as it was for cold Settings links before. Rebase note: main (#1641) added a Navigation.open `dismiss` option, the history snapshot `previous` entry and the browser driver's visit index so Members could traverse back on close. After the earlier commits Members writes in place and nothing calls `dismiss`, and this commit deletes the driver `returnTo` it relied on, so that option, `previous`, the controller's returning focus and their tests are removed with it. Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
What changes
Hover waits for intent. Tooltips, preview cards and hover-opened controls now open only after the pointer rests on their trigger. A cursor passing over an avatar or toolbar on its way somewhere else opens nothing, and any movement restarts the wait. Keyboard focus still opens hints and previews at once, anything that opens on hover also opens with Enter, and Escape closes them.
Every hover surface picks one named bucket from
ui/hoverIntent.ts, and DESIGN.md's new "Hover timing" section documents them:Tooltip,Button/IconButtontitle,PreviewCard,PopoverTriggerandMenuTriggeraccept a bucket, never a raw delay, so type-checking catches a surface that invents its own timing. Hints share one app-levelTooltipProvider(main window and huddle window), so neighbors open instantly once one is showing.PreviewCard needed a small addition to the existing Base UI patch so its open delay is a rest delay, as Base UI's Tooltip already does. Documented in
patches/README.md, and covered byTooltip.test.tsx.Follow-up messages no longer pop a date. Grouped follow-up messages used the whole message body as a 1s hover target for the full date, which fired constantly while reading. That tooltip is removed. The first message's time keeps its tooltip, and every message still carries its date for screen readers.
Back no longer loops through Channel access. Closing the members panel used to add a new history step, so Back reopened it forever. Navigation gains a
dismissoption: closing returns to the visit it was opened from when that visit is directly behind, otherwise it replaces the modal's visit. Either way it never adds a step. Opening a profile from the list replaces the Members visit in place, so the profile panel it just opened stays put. Settings keeps its existing close path (leaveSettings), which returns to a visit that may not be directly behind it. The two are intentionally different.Testing
pnpm checkand the pre-push type and related-unit-test hooks pass on this head.Screenshots
This change is about timing, not appearance: hints, cards and popovers look the same as before and open later. Still screenshots can't show that, so there are none here.
Known, pre-existing
If another destination opens during the one frame while a dismiss's Back traversal is still pending, the traversal can land on the wrong entry. Settings' close path already has the same exposure.
🤖 Generated with Claude Code