fix(shared-ui): delay hover disclosures by default (#5821)

**Category:** fix
**User Impact:** Hover tooltips and informational popovers now wait for
deliberate pointer dwell instead of appearing while users move around
the app.

**Problem:** Tooltips and hover-controlled popovers appeared after
inconsistent, often very short delays, so moving across composer and
navigation controls could obstruct the next interaction.

**Solution:** Establish a 500 ms shared dwell default with no tooltip
skip-delay cascade, apply it to informational hover-controlled Popovers,
and preserve immediate click and keyboard behavior. The responsive
Community actions navigation submenu retains its documented 80 ms open /
160 ms close timing.

## Before / after

| Before | After |
|---|---|
| ![Before: hover disclosures appear during pointer
transit](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5821/before-hover-disclosure.gif)
| ![After: hover disclosures wait for deliberate pointer
dwell](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5821/after-hover-disclosure.gif)
|

<details>
<summary>File changes</summary>

**desktop/src/shared/ui/tooltip.tsx**
Wraps the Radix provider with documented 500 ms and zero skip-delay
defaults. The provider API still permits a future proven exception, but
no current `desktop/src` caller overrides either timing.

**desktop/src/shared/ui/popover.tsx**
Exports the documented shared hover-open timing for controlled popovers;
ordinary click/focus Popovers remain immediate.

**desktop/src/main.tsx**
Uses the shared Tooltip provider defaults at the application root.

**desktop/src/shared/ui/sidebar.tsx**
Removes the sidebar's instant Tooltip timing override.

**desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx**
Removes the local short Tooltip timing override.

**desktop/src/features/home/ui/InboxDetailPane.tsx**
Removes the local short Tooltip timing override.

**desktop/src/features/messages/ui/MessageTimeline.tsx**
Removes the timeline's local short Tooltip timing override.

**desktop/src/features/messages/ui/MessageTimestamp.tsx**
Drops the timestamp-only provider now that its 500 ms, zero-skip
behavior is shared globally.

**desktop/src/features/channels/ui/BotActivityBar.tsx**
Raises composer agent-activity hover dwell from 150 ms to the shared
default while preserving immediate click/focus opening.

**desktop/src/features/sidebar/ui/ChannelActivityPopover.tsx**
Raises channel activity hover dwell from 250 ms to the shared default
while preserving immediate focus opening.

**desktop/src/features/profile/ui/UserProfilePopover.tsx**
Reuses the shared hover timing in place of its equivalent local
constant.

**desktop/src/shared/ui/PubKey.tsx**
Reuses the shared hover timing in place of its equivalent local
constant.

**desktop/src/shared/ui/markdown/InlineEmojiPopover.tsx**
Raises emoji inspection hover dwell from 200 ms to the shared default
while preserving immediate focus opening.

**desktop/src/features/messages/ui/MessageReactions.tsx**
Raises reaction inspection hover dwell from 200 ms to the shared default
while preserving immediate focus and reaction clicks.

**desktop/src/features/communities/ui/CommunitySwitcher.tsx**
Documents the Community actions navigation submenu as an intentional
timing exception: 80 ms to open responsively and 160 ms to preserve the
pointer bridge into its portalled panel.

</details>

## Reproduction steps

Move the pointer rapidly across each surface first, then hold it still
over a labeled control. Hover-only disclosures should stay closed during
transit and open after about **500 ms** of deliberate dwell. Moving
directly between adjacent Tooltip triggers should start a fresh 500 ms
dwell rather than cascading the next Tooltip open immediately.

| Surface | Where to test | What to expect |
|---|---|---|
| Composer controls | Attachment, emoji, image editor, formatting, and
composer toolbar buttons | No Tooltip while sweeping across controls;
the hovered control's Tooltip opens after ~500 ms. Clicking remains
immediate. |
| Message actions | Hover a message, then test reply, react,
more-actions, edit, and related action-bar controls | Each Tooltip waits
~500 ms, including when moving between adjacent actions. The action
itself still runs immediately on click or keyboard activation. |
| Message metadata and content tools | Message timestamps, code-block
copy controls, diff controls, system-message controls, and video-player
controls | Tooltip appears after ~500 ms. Timestamp behavior should look
unchanged; it was already 500 ms with no skip cascade. |
| Reaction pills | Hover a reaction with one or more reactors; also
click the pill | Reactor Popover waits ~500 ms instead of 200 ms.
Clicking still toggles the reaction immediately. |
| Inline custom emoji | Hover a rendered custom emoji in message
Markdown, then focus it with the keyboard | Emoji inspector waits ~500
ms on hover instead of 200 ms. Keyboard focus opens it immediately. |
| Masked links | Hover a masked Markdown link, including one revealed
inside a spoiler | Destination Tooltip waits ~500 ms instead of the
former app-level 300 ms. Hidden spoilers still reveal no destination;
keyboard focus remains immediate. |
| Main and collapsed sidebar controls | Collapse the sidebar and hover
icon-only navigation/menu buttons; also test community-rail controls |
Sidebar Tooltips wait ~500 ms instead of opening instantly. Rapid
movement across icons should not produce a tooltip cascade. |
| Channel header and management controls | Channel members, huddle,
settings, thread-view mode, management rows, and quick-agent controls |
Each Tooltip waits ~500 ms; click/keyboard behavior remains immediate. |
| Add-channel team chips | Open Add channel where saved teams are
available and hover a team chip | Team details Tooltip waits ~500 ms
instead of 150 ms; clicking the chip still toggles the team immediately.
|
| Channel activity preview | Hover a sidebar channel that has activity,
then keyboard-focus its trigger | Activity Popover waits ~500 ms instead
of 250 ms. Focus opens it immediately, and quickly crossing channel rows
should not leave previews in the way. |
| Composer agent activity | Run an agent so the composer activity
control is present; hover, click, and focus it | Hover Popover waits
~500 ms instead of 150 ms. Click and keyboard focus still open it
immediately. |
| Inbox and draft controls | Home inbox open-context/more-actions
controls, inbox-list controls, draft detail, and drafts panel | Tooltips
wait ~500 ms instead of the inbox detail's former 200 ms/local defaults.
Clicks remain immediate. |
| Profile and public-key previews | Hover avatars, names/mentions,
project author identities, and displayed public keys | Profile/pubkey
Popovers open after ~500 ms, matching their prior behavior; the change
centralizes that timing. Click actions and focus behavior remain
immediate. |
| Agent, team, memory, and update controls | Managed-agent rows, team
identity cards, restart-diff badges, memory actions, setup steps, and
update indicator | Tooltips wait ~500 ms and do not cascade when
traversing adjacent controls. Actions remain immediate. |
| Huddle controls | Huddle bar/indicator, mic controls, and
participant-list actions | Tooltips wait ~500 ms; mute, join,
participant, and keyboard actions remain immediate. |
| Projects and activity surfaces | Project/repository cards, overview
rail, contribution graph, activity feed, reviewers, and Pulse note
controls | Tooltips wait ~500 ms with a fresh dwell between adjacent
targets. Clicking/focusing interactive controls remains immediate. |
| Intentional interaction-mode exceptions | Focus a Tooltip trigger;
click/focus an ordinary Popover; move the pointer from an open hover
Popover into its panel | Tooltip focus and Popover click/focus open
immediately because they are explicit user intent, not incidental hover.
Hover Popovers retain their 180–200 ms close grace so the pointer can
cross into the panel. The Community actions navigation submenu is the
intentional exception: it opens after 80 ms and keeps its 160 ms pointer
bridge; informational hover Popovers use the shared 500 ms delay. |

## Verification

- `pnpm typecheck`
- `pnpm check` (passes with three existing diagnostics outside this
diff)
- `pnpm test` — 4,775 passed
- `pnpm build`

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-18 12:55:00 -07:00
committed by GitHub
parent c442a90a17
commit d7e8fdb10c
17 changed files with 207 additions and 57 deletions

View File

@@ -78,7 +78,7 @@ export function AddChannelBotTeamsSection({
</p>
</div>
<TooltipProvider delayDuration={150}>
<TooltipProvider>
<div className="flex flex-wrap gap-2">
{teams.map((team) => {
const resolution = resolveTeamPersonas(team, personas);

View File

@@ -10,7 +10,12 @@ import {
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type { ManagedAgent } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/shared/ui/popover";
import { Shimmer } from "@/shared/ui/Shimmer";
import { UserAvatar } from "@/shared/ui/UserAvatar";
@@ -26,7 +31,6 @@ type BotActivityBarProps = {
variant?: "toolbar" | "inline";
};
const HOVER_OPEN_DELAY_MS = 150;
const HOVER_CLOSE_DELAY_MS = 180;
const HEADLINE_ROTATION_MS = 2200;
@@ -106,7 +110,7 @@ export function BotActivityComposerAction({
clearHoverTimer();
hoverTimerRef.current = setTimeout(() => {
setOpen(true);
}, HOVER_OPEN_DELAY_MS);
}, DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS);
}, [clearHoverTimer]);
const closeWithDelay = React.useCallback(() => {

View File

@@ -39,6 +39,12 @@ import { writeTextToClipboard } from "@/shared/lib/clipboard";
import { useActiveCommunityIcon } from "@/features/communities/useCommunityIcons";
import { EditCommunityDialog } from "./EditCommunityDialog";
// Community actions is a responsive navigation submenu, not an informational
// disclosure. Keep its short hover dwell explicit rather than inheriting the
// shared 500 ms Popover delay intended to prevent incidental inspection UI.
const PROFILE_MENU_HOVER_OPEN_DELAY_MS = 80;
const PROFILE_MENU_HOVER_CLOSE_DELAY_MS = 160;
const CONNECTION_STATE_LABEL: Record<ConnectionState, string> = {
idle: "Not connected",
connecting: "Connecting…",
@@ -128,7 +134,9 @@ export function CommunitySwitcher({
clearProfileMenuHoverTimer();
profileMenuHoverTimer.current = window.setTimeout(
() => setDropdownOpen(nextOpen),
nextOpen ? 80 : 160,
nextOpen
? PROFILE_MENU_HOVER_OPEN_DELAY_MS
: PROFILE_MENU_HOVER_CLOSE_DELAY_MS,
);
}

View File

@@ -622,7 +622,7 @@ function InboxMessageDetailPane({
</div>
</div>
<TooltipProvider delayDuration={200}>
<TooltipProvider>
<div className="flex shrink-0 items-center gap-1">
<UpdateIndicator />
{canOpenChannel && contextChannelId ? (

View File

@@ -12,7 +12,12 @@ import {
isPositiveEmojiParticle,
useEmojiBurst,
} from "@/shared/ui/EmojiBurstProvider";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/shared/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
const REACTION_PILL_BASE_CLASSES =
@@ -379,7 +384,10 @@ function ReactionPill({
const handleMouseEnter = React.useCallback(() => {
if (reaction.users.length === 0) return;
clearTimers();
openTimeout.current = setTimeout(() => setOpen(true), 200);
openTimeout.current = setTimeout(
() => setOpen(true),
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
);
}, [reaction.users.length, clearTimers]);
const scheduleClose = React.useCallback(() => {

View File

@@ -692,7 +692,7 @@ const MessageTimelineBase = React.forwardRef<
) : null;
return (
<TooltipProvider delayDuration={200}>
<TooltipProvider>
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{showUnreadPill ? (
<div

View File

@@ -5,14 +5,7 @@ import {
} from "@/features/messages/lib/dateFormatters";
import { cn } from "@/shared/lib/cn";
import { formatItemTimestamp } from "@/shared/lib/datetime";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/shared/ui/tooltip";
const TIMESTAMP_TOOLTIP_DELAY_MS = 500;
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
/**
* The timestamp beside a message author, and the clock that fades in over the
@@ -50,26 +43,21 @@ export function MessageTimestamp({
: formatItemTimestamp(createdAt, { withTime: true });
return (
<TooltipProvider
delayDuration={TIMESTAMP_TOOLTIP_DELAY_MS}
skipDelayDuration={0}
>
<Tooltip>
<TooltipTrigger asChild>
<p
className={cn(
"shrink-0 cursor-default whitespace-nowrap text-xs font-normal leading-4 tabular-nums text-muted-foreground/55",
className,
)}
data-testid="message-timestamp"
>
{displayTime}
</p>
</TooltipTrigger>
<TooltipContent side="top">
{formatFullDateTime(createdAt)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<p
className={cn(
"shrink-0 cursor-default whitespace-nowrap text-xs font-normal leading-4 tabular-nums text-muted-foreground/55",
className,
)}
data-testid="message-timestamp"
>
{displayTime}
</p>
</TooltipTrigger>
<TooltipContent side="top">
{formatFullDateTime(createdAt)}
</TooltipContent>
</Tooltip>
);
}

View File

@@ -28,7 +28,12 @@ import { cn } from "@/shared/lib/cn";
import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey";
import { useProfileInteractionActions } from "@/features/profile/ui/useProfileInteractionActions";
import { Popover, PopoverAnchor, PopoverContent } from "@/shared/ui/popover";
import {
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
Popover,
PopoverAnchor,
PopoverContent,
} from "@/shared/ui/popover";
import { BotIdenticon } from "@/features/messages/ui/BotIdenticon";
import { useNow } from "@/shared/lib/useNow";
import { Button } from "@/shared/ui/button";
@@ -51,7 +56,6 @@ type UserProfilePopoverProps = {
botIdenticonValue?: string;
};
const HOVER_OPEN_DELAY_MS = 500;
const HOVER_CLOSE_DELAY_MS = 200;
const RUNTIME_LABELS: Record<string, string> = {
@@ -244,7 +248,7 @@ export function UserProfilePopover({
clearHoverTimer();
hoverTimerRef.current = setTimeout(() => {
setOpen(true);
}, HOVER_OPEN_DELAY_MS);
}, DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS);
}, [clearHoverTimer, enableHoverPopover]);
const handleMouseLeave = React.useCallback(() => {

View File

@@ -16,10 +16,14 @@ import type { Channel, FeedItem, HomeFeedResponse } from "@/shared/api/types";
import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey";
import { useNow } from "@/shared/lib/useNow";
import { Markdown } from "@/shared/ui/markdown";
import { Popover, PopoverAnchor, PopoverContent } from "@/shared/ui/popover";
import {
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
Popover,
PopoverAnchor,
PopoverContent,
} from "@/shared/ui/popover";
import { UserAvatar } from "@/shared/ui/UserAvatar";
const HOVER_OPEN_DELAY_MS = 250;
const HOVER_CLOSE_DELAY_MS = 180;
const ACTIVITY_POPOVER_MOTION_STYLE = {
"--tw-enter-scale": "1",
@@ -310,7 +314,7 @@ export function ChannelActivityPopover({
clearHoverTimer();
hoverTimerRef.current = setTimeout(() => {
setOpen(true);
}, HOVER_OPEN_DELAY_MS);
}, DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS);
}, [clearHoverTimer, hasContent]);
const openImmediately = React.useCallback(() => {
if (!hasContent) return;

View File

@@ -86,7 +86,7 @@ function renderApp() {
enabled={huddleWindowChannelId() === null}
>
<ThemeProvider defaultTheme="buzz">
<TooltipProvider delayDuration={300}>
<TooltipProvider>
<EmojiBurstProvider>
<PoofBurstProvider>
<UpdaterProvider>

View File

@@ -6,9 +6,13 @@ import { cn } from "@/shared/lib/cn";
import { safeNpub } from "@/shared/lib/nostrUtils";
import { truncatePubkey } from "@/shared/lib/pubkey";
import { Button } from "@/shared/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/shared/ui/popover";
const HOVER_OPEN_DELAY_MS = 500;
const HOVER_CLOSE_DELAY_MS = 200;
type PubKeyProps = {
@@ -99,7 +103,7 @@ export function PubKey({
clearHoverTimer();
hoverTimerRef.current = setTimeout(() => {
setOpen(true);
}, HOVER_OPEN_DELAY_MS);
}, DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS);
}, [clearHoverTimer]);
const handleMouseLeave = React.useCallback(() => {

View File

@@ -1,6 +1,11 @@
import * as React from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/shared/ui/popover";
export function InlineEmojiPopover({
alt,
@@ -27,7 +32,10 @@ export function InlineEmojiPopover({
const handleMouseEnter = React.useCallback(() => {
clearTimers();
openTimeout.current = setTimeout(() => setOpen(true), 200);
openTimeout.current = setTimeout(
() => setOpen(true),
DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS,
);
}, [clearTimers]);
const scheduleClose = React.useCallback(() => {

View File

@@ -14,6 +14,10 @@ import {
POPOVER_SURFACE_CLASS,
} from "@/shared/ui/popoverSurface";
// Radix Popover has no hover timing API: controlled hover popovers must use this
// shared dwell default themselves. Keep click and keyboard opens immediate.
export const DEFAULT_POPOVER_HOVER_OPEN_DELAY_MS = 500;
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;

View File

@@ -251,7 +251,7 @@ const SidebarProvider = React.forwardRef<
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<TooltipProvider>
<div
style={
{

View File

@@ -3,7 +3,23 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/shared/lib/cn";
const TooltipProvider = TooltipPrimitive.Provider;
// Hover-only disclosure should require deliberate pointer dwell. Disabling Radix's
// skip-delay grace prevents tooltips from cascading open while the pointer moves
// across adjacent controls. Callers may override both values for a proven case.
const DEFAULT_TOOLTIP_DELAY_MS = 500;
const DEFAULT_TOOLTIP_SKIP_DELAY_MS = 0;
const TooltipProvider = ({
delayDuration = DEFAULT_TOOLTIP_DELAY_MS,
skipDelayDuration = DEFAULT_TOOLTIP_SKIP_DELAY_MS,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) => (
<TooltipPrimitive.Provider
delayDuration={delayDuration}
skipDelayDuration={skipDelayDuration}
{...props}
/>
);
const Tooltip = TooltipPrimitive.Root;

View File

@@ -296,10 +296,87 @@ test.describe("community rail", () => {
expect(communityBox?.y).toBeLessThan(feedbackBox?.y ?? 0);
expect(feedbackBox?.y).toBeLessThan(settingsBox?.y ?? 0);
await page.getByTestId("community-switcher").click();
const menu = page.getByRole("menu", { name: "Community actions" });
await communityTrigger.evaluate((trigger) => {
trigger.addEventListener(
"mouseenter",
() => {
trigger.dataset.hoverStartedAt = String(performance.now());
},
{ once: true },
);
trigger.addEventListener("mouseleave", () => {
trigger.dataset.leftAt = String(performance.now());
});
const observer = new MutationObserver((records) => {
if (
trigger.getAttribute("aria-expanded") === "true" &&
!trigger.dataset.expandedAt
) {
trigger.dataset.expandedAt = String(performance.now());
}
if (
records.some(
(record) =>
record.attributeName === "aria-expanded" &&
record.oldValue === "true",
)
) {
trigger.dataset.closedAfterOpening = "true";
}
});
observer.observe(trigger, {
attributeFilter: ["aria-expanded"],
attributeOldValue: true,
attributes: true,
});
});
await communityTrigger.hover();
await expect(menu).toBeVisible({ timeout: 700 });
const openDelayMs = await communityTrigger.evaluate((trigger) => {
const hoverStartedAt = Number(trigger.dataset.hoverStartedAt);
const expandedAt = Number(trigger.dataset.expandedAt);
if (!Number.isFinite(hoverStartedAt) || !Number.isFinite(expandedAt)) {
throw new Error("Community actions open timing was not recorded");
}
return expandedAt - hoverStartedAt;
});
expect(openDelayMs).toBeGreaterThanOrEqual(40);
expect(openDelayMs).toBeLessThan(300);
const openTriggerBox = await communityTrigger.boundingBox();
const menuBox = await menu.boundingBox();
expect(openTriggerBox).not.toBeNull();
expect(menuBox).not.toBeNull();
if (!openTriggerBox || !menuBox) {
throw new Error("Community actions geometry unavailable");
}
const triggerExitX = openTriggerBox.x + openTriggerBox.width - 1;
const triggerExitY = Math.min(
openTriggerBox.y + openTriggerBox.height - 4,
menuBox.y + menuBox.height - 4,
);
await page.mouse.move(triggerExitX, triggerExitY);
await page.mouse.move(menuBox.x + 8, menuBox.y - 8);
await page.waitForTimeout(80);
await page.mouse.move(menuBox.x + 8, menuBox.y + 8);
const bridgeDurationMs = await communityTrigger.evaluate((trigger) => {
const leftAt = Number(trigger.dataset.leftAt);
if (!Number.isFinite(leftAt)) {
throw new Error(
"Community actions trigger exit timing was not recorded",
);
}
return performance.now() - leftAt;
});
expect(bridgeDurationMs).toBeGreaterThanOrEqual(60);
expect(bridgeDurationMs).toBeLessThan(140);
await page.waitForTimeout(180);
await expect(menu).toBeVisible();
await expect(communityTrigger).not.toHaveAttribute(
"data-closed-after-opening",
"true",
);
await expect(
menu.getByRole("menuitem", { name: "Copy community URL" }),
).toBeVisible();

View File

@@ -12,8 +12,8 @@ test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
/** Hover the trigger, then slide the cursor onto the tooltip popup and
* assert the tooltip dismisses instead of persisting. */
/** Hover the trigger through the shared dwell, then slide the cursor onto the
* tooltip popup and assert the tooltip dismisses instead of persisting. */
async function expectTooltipDismissesOnLeave(
page: import("@playwright/test").Page,
trigger: import("@playwright/test").Locator,
@@ -22,7 +22,9 @@ async function expectTooltipDismissesOnLeave(
await trigger.hover();
const tip = page.getByRole("tooltip", { name: tooltipName });
await expect(tip).toBeVisible();
await page.waitForTimeout(400);
await expect(tip).toHaveCount(0);
await expect(tip).toBeVisible({ timeout: 1_000 });
// Slide off the trigger onto the tooltip popup.
const box = await tip.boundingBox();
@@ -48,6 +50,26 @@ test("composer toolbar tooltip dismisses when cursor leaves the trigger", async
);
});
test("adjacent composer tooltips each require a fresh dwell", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("message-insert-mention").hover();
await page.waitForTimeout(400);
const mentionTooltip = page.getByRole("tooltip", { name: "Mention someone" });
await expect(mentionTooltip).toHaveCount(0);
await expect(mentionTooltip).toBeVisible({ timeout: 1_000 });
await page.getByRole("button", { name: "Attach file" }).hover();
await page.waitForTimeout(400);
const attachTooltip = page.getByRole("tooltip", { name: "Attach file" });
await expect(attachTooltip).toHaveCount(0);
await expect(attachTooltip).toBeVisible({ timeout: 1_000 });
});
test("formatting sub-toolbar tooltip dismisses when cursor leaves the trigger", async ({
page,
}) => {
@@ -60,6 +82,9 @@ test("formatting sub-toolbar tooltip dismisses when cursor leaves the trigger",
const bold = page.getByRole("button", { name: "Bold" });
await expect(bold).toBeVisible();
// The formatting strip animates into place; wait for its delayed entrance to
// settle so the pointer remains over the trigger for the full dwell.
await page.waitForTimeout(300);
// Tooltip text is "<label> (<shortcut>)" for items that carry a shortcut.
await expectTooltipDismissesOnLeave(page, bold, "Bold (⌘B)");