mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(reactions): wrap long popover names (#3834)
**Category:** fix **User Impact:** Long custom emoji names now stay contained inside reaction popovers and remain fully readable. **Problem:** An unbroken custom emoji name could force a reaction popover beyond its intended maximum width and overflow the message view. **Solution:** Give the reaction popover a definite 288px width and allow the complete emoji name to wrap within it without truncation or ellipsis. Short names retain the same content and interaction behavior. <details> <summary>File changes</summary> **desktop/src/features/messages/ui/MessageReactions.tsx** Bounds the reaction popover width and allows long names to break across lines while preserving the full shortcode. **desktop/tests/e2e/reaction-names.spec.ts** Covers fixed width, full text preservation, and wrapping for the maximum supported colon-wrapped reaction name, with deterministic seeded Picsum visual fixtures and explicit image-load waits. </details> ## Reproduction Steps 1. Open a message with a custom emoji reaction whose name is 64 characters. 2. Hover or focus the reaction pill to open its details popover. 3. Confirm the popover remains 288px wide and the complete name wraps within it without ellipsis. 4. Open a short-name reaction and confirm its popover remains readable and unchanged in behavior. ## Screenshots | Before | After | | --- | --- | |  |  | **Short-name regression check**  ## Verification - `pnpm test` in `desktop`: 3,858 passed - Focused reaction-name E2E with seeded Picsum captures: 2 passed - Desktop checks and commit hooks passed Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f` --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -126,7 +126,10 @@ function ReactionPopoverContent({ reaction }: { reaction: TimelineReaction }) {
|
||||
<div className="max-w-[14rem] text-balance text-sm font-semibold leading-snug text-popover-foreground">
|
||||
{userText} <span className="text-muted-foreground">reacted with</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-sm font-semibold leading-snug text-muted-foreground">
|
||||
<div
|
||||
className="mt-0.5 break-all text-sm font-semibold leading-snug text-muted-foreground"
|
||||
data-testid="reaction-popover-name"
|
||||
>
|
||||
{displayName}
|
||||
</div>
|
||||
</div>
|
||||
@@ -504,7 +507,7 @@ function ReactionPill({
|
||||
align="start"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="w-auto min-w-56 max-w-72 rounded-xl p-3"
|
||||
className="w-72 rounded-xl p-3"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={scheduleClose}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
const REACTION_TARGET_CONTENT = "React to me with a custom emoji";
|
||||
const REACTION_TARGET_EVENT_ID = "d".repeat(64);
|
||||
const BOB_PUBKEY =
|
||||
"bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260";
|
||||
const MAX_REACTION_NAME =
|
||||
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl";
|
||||
const MAX_REACTION_AVATAR_URL =
|
||||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Crect width="16" height="16" rx="4" fill="%23e5484d"/%3E%3C/svg%3E';
|
||||
const SHORT_REACTION_AVATAR_URL =
|
||||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Crect width="16" height="16" rx="4" fill="%2300a36c"/%3E%3C/svg%3E';
|
||||
const SCREENSHOT_DIR =
|
||||
process.env.REACTION_POPOVER_SCREENSHOT_DIR ??
|
||||
"test-results/reaction-popover-screenshots";
|
||||
|
||||
function reactionTargetRow(page: import("@playwright/test").Page) {
|
||||
return page
|
||||
@@ -14,8 +26,45 @@ function reactionTargetRow(page: import("@playwright/test").Page) {
|
||||
.last();
|
||||
}
|
||||
|
||||
async function waitForImage(
|
||||
image: import("@playwright/test").Locator,
|
||||
): Promise<void> {
|
||||
await expect(image).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
image.evaluate(
|
||||
(element) =>
|
||||
element instanceof HTMLImageElement &&
|
||||
element.complete &&
|
||||
element.naturalWidth > 0,
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function capturePopover(
|
||||
page: import("@playwright/test").Page,
|
||||
popover: import("@playwright/test").Locator,
|
||||
filename: string,
|
||||
): Promise<void> {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
await waitForAnimations(page);
|
||||
await popover.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(SCREENSHOT_DIR, filename),
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
await installMockBridge(page, {
|
||||
searchProfiles: [
|
||||
{
|
||||
pubkey: BOB_PUBKEY,
|
||||
displayName: "bob",
|
||||
avatarUrl: SHORT_REACTION_AVATAR_URL,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("reaction popover resolves a reactor with no authored message in the window", async ({
|
||||
@@ -33,21 +82,89 @@ test("reaction popover resolves a reactor with no authored message in the window
|
||||
);
|
||||
|
||||
await page.evaluate(
|
||||
({ pubkey, targetId }) => {
|
||||
({ pubkey, targetId, avatarUrl }) => {
|
||||
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: "general",
|
||||
content: "🎉",
|
||||
extraTags: [["e", targetId]],
|
||||
content: ":react:",
|
||||
extraTags: [
|
||||
["e", targetId],
|
||||
["emoji", "react", avatarUrl],
|
||||
],
|
||||
kind: 7,
|
||||
pubkey,
|
||||
});
|
||||
},
|
||||
{ pubkey: BOB_PUBKEY, targetId: REACTION_TARGET_EVENT_ID },
|
||||
{
|
||||
avatarUrl: SHORT_REACTION_AVATAR_URL,
|
||||
pubkey: BOB_PUBKEY,
|
||||
targetId: REACTION_TARGET_EVENT_ID,
|
||||
},
|
||||
);
|
||||
|
||||
const row = reactionTargetRow(page);
|
||||
const pill = row.getByRole("button", { name: "Toggle 🎉 reaction" });
|
||||
const pill = row.getByRole("button", { name: "Toggle :react: reaction" });
|
||||
await expect(pill).toBeVisible();
|
||||
await pill.hover();
|
||||
await expect(page.getByText("bob reacted with")).toBeVisible();
|
||||
const popover = page
|
||||
.locator("[data-radix-popper-content-wrapper]")
|
||||
.filter({ hasText: "bob reacted with" });
|
||||
const avatar = popover.locator("img");
|
||||
await expect(avatar).toHaveAttribute("src", SHORT_REACTION_AVATAR_URL);
|
||||
await waitForImage(avatar);
|
||||
await capturePopover(page, popover, "short-name-after.png");
|
||||
});
|
||||
|
||||
test("maximum-length reaction name wraps inside a fixed-width popover", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
|
||||
channelName: "general",
|
||||
kind: 7,
|
||||
}) === true,
|
||||
);
|
||||
|
||||
const reaction = `:${MAX_REACTION_NAME}:`;
|
||||
await page.evaluate(
|
||||
({ content, targetId, avatarUrl }) => {
|
||||
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: "general",
|
||||
content,
|
||||
extraTags: [
|
||||
["e", targetId],
|
||||
["emoji", content.slice(1, -1), avatarUrl],
|
||||
],
|
||||
kind: 7,
|
||||
});
|
||||
},
|
||||
{
|
||||
avatarUrl: MAX_REACTION_AVATAR_URL,
|
||||
content: reaction,
|
||||
targetId: REACTION_TARGET_EVENT_ID,
|
||||
},
|
||||
);
|
||||
|
||||
const pill = reactionTargetRow(page).getByRole("button", {
|
||||
name: `Toggle ${reaction} reaction`,
|
||||
});
|
||||
await expect(pill).toBeVisible();
|
||||
await pill.focus();
|
||||
|
||||
const popover = page
|
||||
.locator("[data-radix-popper-content-wrapper]")
|
||||
.filter({ hasText: reaction });
|
||||
await expect(popover).toBeVisible();
|
||||
await expect(popover).toHaveCSS("width", "288px");
|
||||
const reactionName = popover.getByTestId("reaction-popover-name");
|
||||
await expect(reactionName).toHaveCSS("word-break", "break-all");
|
||||
await expect(reactionName).toHaveText(reaction);
|
||||
const avatar = popover.locator("img");
|
||||
await expect(avatar).toHaveAttribute("src", MAX_REACTION_AVATAR_URL);
|
||||
await waitForImage(avatar);
|
||||
await capturePopover(page, popover, "max-length-after.png");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user