mirror of
https://github.com/block/buzz.git
synced 2026-11-02 15:31:27 +00:00
fix(desktop): stabilize message action morph
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
@@ -14,7 +14,12 @@ import { MessageActionBloomSurface } from "@/features/messages/ui/MessageActionB
|
||||
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/shared/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown";
|
||||
@@ -22,11 +27,13 @@ import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdo
|
||||
const ACTION_BUTTON_CLASS = "h-8 w-8 rounded-full p-0";
|
||||
|
||||
export function BestieMessagePopover({
|
||||
anchorRef,
|
||||
channelId,
|
||||
message,
|
||||
onOpenChange,
|
||||
open,
|
||||
}: {
|
||||
anchorRef: React.RefObject<HTMLElement | null>;
|
||||
channelId: string;
|
||||
message: TimelineMessage;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -85,6 +92,7 @@ export function BestieMessagePopover({
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={onOpenChange} open={open}>
|
||||
<PopoverAnchor virtualRef={anchorRef} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -113,7 +121,7 @@ export function BestieMessagePopover({
|
||||
align="end"
|
||||
className="data-[state=closed]:animate-none data-[state=open]:animate-none"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
sideOffset={2}
|
||||
>
|
||||
<MessageActionBloomSurface
|
||||
className="w-[min(328px,calc(100vw-2rem))] p-4"
|
||||
|
||||
@@ -41,7 +41,12 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import { isPositiveEmojiParticle } from "@/shared/ui/EmojiBurstProvider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/shared/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { BestieMessagePopover } from "./BestieMessagePopover";
|
||||
import {
|
||||
@@ -386,6 +391,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
const [activeSurface, setActiveSurface] = React.useState<
|
||||
"reactions" | "bestie" | "more" | null
|
||||
>(null);
|
||||
const toolbarAnchorRef = React.useRef<HTMLDivElement>(null);
|
||||
const isReactionPickerOpen = activeSurface === "reactions";
|
||||
const isBestiePopoverOpen = activeSurface === "bestie";
|
||||
const isDropdownOpen = activeSurface === "more";
|
||||
@@ -477,6 +483,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
"relative flex items-center gap-0.5 p-1",
|
||||
activeSurface && "pointer-events-none",
|
||||
)}
|
||||
ref={toolbarAnchorRef}
|
||||
transition={{
|
||||
duration: 0.12,
|
||||
ease: MESSAGE_ACTION_BLOOM_EASE_OUT,
|
||||
@@ -489,6 +496,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
}
|
||||
open={isReactionPickerOpen}
|
||||
>
|
||||
<PopoverAnchor virtualRef={toolbarAnchorRef} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -511,7 +519,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
align="end"
|
||||
className="data-[state=closed]:animate-none data-[state=open]:animate-none"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
sideOffset={2}
|
||||
>
|
||||
<MessageActionBloomSurface
|
||||
className="w-auto p-0"
|
||||
@@ -540,6 +548,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
|
||||
{canCopyMessageLink(message, channelId) ? (
|
||||
<BestieMessagePopover
|
||||
anchorRef={toolbarAnchorRef}
|
||||
channelId={channelId}
|
||||
message={message}
|
||||
onOpenChange={(nextOpen) =>
|
||||
|
||||
@@ -6,10 +6,9 @@ import { cn } from "@/shared/lib/cn";
|
||||
export const MESSAGE_ACTION_BLOOM_LAYOUT_ID = "message-action-bloom-surface";
|
||||
export const MESSAGE_ACTION_BLOOM_EASE_OUT = [0.23, 1, 0.32, 1] as const;
|
||||
|
||||
const BLOOM_LAYOUT_SPRING = {
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 28,
|
||||
const BLOOM_LAYOUT_TRANSITION = {
|
||||
duration: 0.24,
|
||||
ease: MESSAGE_ACTION_BLOOM_EASE_OUT,
|
||||
} as const;
|
||||
|
||||
type MessageActionBloomSurfaceProps = Omit<
|
||||
@@ -43,6 +42,23 @@ export const MessageActionBloomSurface = React.forwardRef<
|
||||
ref,
|
||||
) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
const [contentReady, setContentReady] = React.useState(
|
||||
reduceMotion || !revealContent,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (reduceMotion || !revealContent) {
|
||||
setContentReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Radix positions portalled content after it mounts. Keep the destination
|
||||
// content out of sight through that measurement and the first part of the
|
||||
// shell morph; the callback below normally wins, while this is a safety net
|
||||
// for browsers that skip layout-complete notifications.
|
||||
const fallback = window.setTimeout(() => setContentReady(true), 160);
|
||||
return () => window.clearTimeout(fallback);
|
||||
}, [reduceMotion, revealContent]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -51,28 +67,28 @@ export const MessageActionBloomSurface = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
layoutId={MESSAGE_ACTION_BLOOM_LAYOUT_ID}
|
||||
onLayoutAnimationComplete={() => setContentReady(true)}
|
||||
ref={ref}
|
||||
style={{ ...style, borderRadius: surfaceRadius }}
|
||||
transition={
|
||||
reduceMotion
|
||||
? { duration: 0 }
|
||||
: {
|
||||
layout: BLOOM_LAYOUT_SPRING,
|
||||
layout: BLOOM_LAYOUT_TRANSITION,
|
||||
}
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{revealContent ? (
|
||||
<motion.div
|
||||
animate={{ opacity: 1 }}
|
||||
animate={{ opacity: contentReady ? 1 : 0 }}
|
||||
className={contentClassName}
|
||||
initial={{ opacity: 0 }}
|
||||
transition={
|
||||
reduceMotion
|
||||
? { duration: 0 }
|
||||
: {
|
||||
delay: 0.08,
|
||||
duration: 0.14,
|
||||
duration: 0.12,
|
||||
ease: MESSAGE_ACTION_BLOOM_EASE_OUT,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,29 @@ test("message action sends a message link and optional note to Bestie", async ({
|
||||
await expect(
|
||||
page.getByTestId(`reaction-bloom-panel-${messageId}`),
|
||||
).toBeVisible();
|
||||
const reactionPanel = page.getByTestId(`reaction-bloom-panel-${messageId}`);
|
||||
await expect
|
||||
.poll(() =>
|
||||
reactionPanel.evaluate(
|
||||
(element) => window.getComputedStyle(element).transform,
|
||||
),
|
||||
)
|
||||
.toBe("none");
|
||||
const [reactionBox, reactionAnchorBox] = await Promise.all([
|
||||
reactionPanel.boundingBox(),
|
||||
actionBar.boundingBox(),
|
||||
]);
|
||||
expect(reactionBox).not.toBeNull();
|
||||
expect(reactionAnchorBox).not.toBeNull();
|
||||
if (reactionBox && reactionAnchorBox) {
|
||||
expect(
|
||||
Math.abs(
|
||||
reactionBox.x +
|
||||
reactionBox.width -
|
||||
(reactionAnchorBox.x + reactionAnchorBox.width),
|
||||
),
|
||||
).toBeLessThanOrEqual(10);
|
||||
}
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(actionBar).toHaveAttribute("data-bloom-surface", "toolbar");
|
||||
|
||||
@@ -137,6 +160,27 @@ test("message action sends a message link and optional note to Bestie", async ({
|
||||
await expect(
|
||||
page.getByTestId(`more-actions-panel-${messageId}`),
|
||||
).toBeVisible();
|
||||
const morePanel = page.getByTestId(`more-actions-panel-${messageId}`);
|
||||
await expect
|
||||
.poll(() =>
|
||||
morePanel.evaluate(
|
||||
(element) => window.getComputedStyle(element).transform,
|
||||
),
|
||||
)
|
||||
.toBe("none");
|
||||
const [moreBox, moreAnchorBox] = await Promise.all([
|
||||
morePanel.boundingBox(),
|
||||
actionBar.boundingBox(),
|
||||
]);
|
||||
expect(moreBox).not.toBeNull();
|
||||
expect(moreAnchorBox).not.toBeNull();
|
||||
if (moreBox && moreAnchorBox) {
|
||||
expect(
|
||||
Math.abs(
|
||||
moreBox.x + moreBox.width - (moreAnchorBox.x + moreAnchorBox.width),
|
||||
),
|
||||
).toBeLessThanOrEqual(10);
|
||||
}
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(actionBar).toHaveAttribute("data-bloom-surface", "toolbar");
|
||||
|
||||
@@ -166,6 +210,15 @@ test("message action sends a message link and optional note to Bestie", async ({
|
||||
popover.evaluate((element) => window.getComputedStyle(element).transform),
|
||||
)
|
||||
.toBe("none");
|
||||
await expect
|
||||
.poll(() =>
|
||||
popover.evaluate((element) =>
|
||||
Number.parseFloat(
|
||||
window.getComputedStyle(element.firstElementChild as Element).opacity,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toBeGreaterThan(0.99);
|
||||
await expect(popover).toContainText("Bestie");
|
||||
await expect(popover).not.toContainText("Share this message with Bestie");
|
||||
await expect(popover).toContainText("Please fold this launch decision");
|
||||
@@ -184,6 +237,17 @@ test("message action sends a message link and optional note to Bestie", async ({
|
||||
expect(snapshotBodyBox).not.toBeNull();
|
||||
if (popoverBox && snapshotBox && snapshotBodyBox) {
|
||||
expect(popoverBox.width).toBeLessThanOrEqual(328);
|
||||
const actionBarBox = await actionBar.boundingBox();
|
||||
expect(actionBarBox).not.toBeNull();
|
||||
if (actionBarBox) {
|
||||
expect(
|
||||
Math.abs(
|
||||
popoverBox.x +
|
||||
popoverBox.width -
|
||||
(actionBarBox.x + actionBarBox.width),
|
||||
),
|
||||
).toBeLessThanOrEqual(10);
|
||||
}
|
||||
expect(snapshotBox.width / (popoverBox.width - 32)).toBeCloseTo(0.75, 1);
|
||||
expect(Math.abs(snapshotBox.x - (popoverBox.x + 16))).toBeLessThanOrEqual(
|
||||
1,
|
||||
|
||||
Reference in New Issue
Block a user