mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(desktop): keep FileCard inside narrow message rows (#8612)
## Summary Non-image attachment cards ran past their message row in the thread side pane, and the download icon was clipped off. `FileCard` is an `inline-flex` box, so it shrinks to fit its content and is never narrower than its min-content width. That width comes from the `truncate` (nowrap) filename. The only width cap was `max-w-sm` (24rem), so a long filename pushed the card out to the full 24rem even when the pane body was only about 300px wide (the default 380px thread pane). - Add `max-w-full`, so the cap becomes `min(24rem, 100%)`. The flex column ancestors already have `min-w-0`, so the percentage resolves against the pane width. The card shrinks, the filename gets an ellipsis, and the size and download icon stay visible. Wider surfaces (forum posts, DMs, wide timelines) look the same because 100% is wider than 24rem there. - Add a Playwright regression test in `file-attachment.spec.ts`. It seeds a text attachment with a long filename and opens its thread. It then checks that the card and its trailing icon stay inside the row and that the filename is actually truncated. Without the fix, the card's right edge lands about 82px past the row. ## Before / After | Before | After | | --- | --- | |  |  | Measured in the default 380px thread pane: before, the card's right edge sits at 1342px against a row edge of 1260px (+82px overflow); after, it sits at 1252px (8px inside the row). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@ export function FileCard({
|
||||
);
|
||||
}}
|
||||
data-testid="file-card"
|
||||
className="my-1 inline-flex max-w-sm items-center gap-3 rounded-2xl border border-border/70 bg-muted/40 px-3 py-2 text-left no-underline transition-colors hover:bg-muted/70"
|
||||
className="my-1 inline-flex max-w-[min(24rem,100%)] items-center gap-3 rounded-2xl border border-border/70 bg-muted/40 px-3 py-2 text-left no-underline transition-colors hover:bg-muted/70"
|
||||
style={{ borderRadius: "1rem" }}
|
||||
>
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-background text-muted-foreground">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
@@ -725,3 +725,139 @@ test("a non-media attachment's remove button is named and keyboard-operable", as
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(composer).not.toContainText("quarterly-report.pdf");
|
||||
});
|
||||
|
||||
// Shared by the two FileCard width-cap tests below. FileCard is an inline-flex
|
||||
// (shrink-to-fit) box whose min-content width is set by its `truncate`
|
||||
// (nowrap) filename, so a long name is what drives the card to its cap.
|
||||
const LONG_ATTACHMENT_FILENAME =
|
||||
"Running onboarding-flow first run with a temporary identity and wiped communities.txt";
|
||||
// 24rem at the default 16px root — the 24rem half of FileCard's
|
||||
// `max-w-[min(24rem,100%)]` cap.
|
||||
const FILE_CARD_CAP_PX = 384;
|
||||
|
||||
async function seedLongNamedAttachment(page: Page) {
|
||||
const sha256 = "b".repeat(64);
|
||||
const url = `https://mock.relay/media/${sha256}.txt`;
|
||||
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof (window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown })
|
||||
.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function",
|
||||
null,
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await page.evaluate(
|
||||
async ({ content, mediaTags }) => {
|
||||
const invoke = (
|
||||
window as Window & {
|
||||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__: (
|
||||
command: string,
|
||||
payload: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
}
|
||||
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
||||
await invoke("send_channel_message", {
|
||||
channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50",
|
||||
content,
|
||||
mediaTags,
|
||||
});
|
||||
},
|
||||
{
|
||||
content: `[${LONG_ATTACHMENT_FILENAME}](${url})`,
|
||||
mediaTags: [
|
||||
[
|
||||
"imeta",
|
||||
`url ${url}`,
|
||||
"m text/plain",
|
||||
`x ${sha256}`,
|
||||
"size 13312",
|
||||
`filename ${LONG_ATTACHMENT_FILENAME}`,
|
||||
],
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await page.getByTestId("channel-general").click();
|
||||
const timelineRow = page
|
||||
.getByTestId("message-timeline")
|
||||
.getByTestId("message-row")
|
||||
.filter({ has: page.getByTestId("file-card") })
|
||||
.last();
|
||||
await expect(timelineRow).toBeVisible();
|
||||
return timelineRow;
|
||||
}
|
||||
|
||||
async function expectFilenameEllipsised(card: Locator) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
card
|
||||
.getByText(LONG_ATTACHMENT_FILENAME)
|
||||
.evaluate((el) => el.scrollWidth > el.clientWidth),
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
test("a long filename stays inside the narrow thread pane row", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Regression: with only the 24rem cap the card grew past a ~300px
|
||||
// thread-pane row and clipped the download icon. The cap is
|
||||
// min(24rem, 100%) so the card shrinks and the filename ellipsises instead.
|
||||
const timelineRow = await seedLongNamedAttachment(page);
|
||||
await timelineRow.hover();
|
||||
await timelineRow.getByRole("button", { name: "Reply" }).click();
|
||||
|
||||
// The thread head renders the same message in the default 380px pane.
|
||||
const threadRow = page
|
||||
.getByTestId("message-thread-panel")
|
||||
.getByTestId("message-thread-head")
|
||||
.getByTestId("message-row");
|
||||
const card = threadRow.getByTestId("file-card");
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card).toContainText("13 KB");
|
||||
|
||||
const [rowBox, cardBox, iconBox] = await Promise.all([
|
||||
threadRow.boundingBox(),
|
||||
card.boundingBox(),
|
||||
card.locator("svg").last().boundingBox(),
|
||||
]);
|
||||
if (!rowBox || !cardBox || !iconBox) {
|
||||
throw new Error("Expected the thread row, card, and icon to lay out.");
|
||||
}
|
||||
// The pane is narrower than the card's 24rem cap, so the filename must be
|
||||
// ellipsised — otherwise this test would not be exercising the overflow.
|
||||
expect(rowBox.width).toBeLessThan(FILE_CARD_CAP_PX);
|
||||
await expectFilenameEllipsised(card);
|
||||
expect(cardBox.x + cardBox.width).toBeLessThanOrEqual(
|
||||
rowBox.x + rowBox.width + 0.5,
|
||||
);
|
||||
expect(iconBox.x + iconBox.width).toBeLessThanOrEqual(
|
||||
rowBox.x + rowBox.width + 0.5,
|
||||
);
|
||||
});
|
||||
|
||||
test("a long filename keeps the 24rem cap in a wide timeline row", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Regression: `max-w-full` alone (without the 24rem cap) let a long
|
||||
// filename stretch the card across the whole main-timeline row. The cap
|
||||
// must still bind where the row is wider than 24rem.
|
||||
const timelineRow = await seedLongNamedAttachment(page);
|
||||
const card = timelineRow.getByTestId("file-card");
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card).toContainText("13 KB");
|
||||
|
||||
const [rowBox, cardBox] = await Promise.all([
|
||||
timelineRow.boundingBox(),
|
||||
card.boundingBox(),
|
||||
]);
|
||||
if (!rowBox || !cardBox) {
|
||||
throw new Error("Expected the timeline row and card to lay out.");
|
||||
}
|
||||
// The row must be comfortably wider than the cap, otherwise `100%` would
|
||||
// bind first and this test would not be exercising the 24rem limit.
|
||||
expect(rowBox.width).toBeGreaterThan(FILE_CARD_CAP_PX + 100);
|
||||
expect(cardBox.width).toBeLessThanOrEqual(FILE_CARD_CAP_PX + 0.5);
|
||||
await expectFilenameEllipsised(card);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user