fix(desktop): align jump-to-latest pill with composer height (#6606)

**Category:** fix
**User Impact:** Jump to Latest now stays above the composer as a draft
grows to multiple lines.

**Problem:** On WebKit, the pill's transform could retain a stale
inherited composer-height value after the composer expanded, leaving the
control stranded inside the composer.

**Solution:** Position and animate the pill with its absolute bottom
offset, which consumes the live composer height through layout rather
than a promoted transform layer. A smoke test now verifies that the pill
rises by the full composer growth and remains clear of the composer.

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

**desktop/src/features/messages/ui/MessageTimeline.tsx**
Anchor Jump to Latest with a live bottom offset instead of a translated
compositor layer so composer resizing reliably moves it.

**desktop/tests/e2e/smoke.spec.ts**
Add coverage that expands a detached timeline's composer and checks the
pill tracks the full height increase without overlapping it.

</details>

## Reproduction steps

1. Open a channel with enough messages to scroll.
2. Scroll away from the newest message until Jump to Latest appears.
3. Add several lines to the composer without sending.
4. Confirm Jump to Latest rises with the composer and remains directly
above it.

## Validation

- `pnpm --dir desktop test` — 5,397 passed
- `pnpm --dir desktop check` — passed with four existing informational
warnings outside this diff
- `pnpm --dir desktop typecheck` — passed
- `pnpm --dir desktop exec playwright test tests/e2e/smoke.spec.ts
--project=smoke` — 26 passed
- Push hooks — desktop check, typecheck, and tests passed


## Screenshots / Demos

Both captures use the same long, mid-history timeline and the same
four-line composer.

| Before | After |
| --- | --- |
| The stale pill position overflows into the expanded composer. | The
pill tracks the live composer height and stays clear above it. |
| ![Before — Jump to Latest overlaps the tall
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6606/jump-pill-tall-composer-before.png)
| ![After — Jump to Latest clears the tall
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6606/jump-pill-tall-composer-after.png)
|

Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
This commit is contained in:
Taylor Ho
2026-08-24 11:56:27 -07:00
committed by GitHub
parent db5617dd15
commit 9f55bf6745
2 changed files with 57 additions and 1 deletions

View File

@@ -863,8 +863,12 @@ const MessageTimelineBase = React.forwardRef<
<div
className={cn(
"pointer-events-none absolute inset-x-0 bottom-4 z-50 flex justify-center px-4",
// Position the pill with layout rather than a transform. WebKit
// can keep an inherited custom property stale on a promoted
// transform layer when the composer grows, leaving the pill
// stranded inside the dock.
hasComposerOverlay &&
"translate-y-[calc(-1*var(--composer-overlay-height,8rem))] transform-gpu transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
"bottom-[calc(1rem+var(--composer-overlay-height,8rem))] transition-[bottom] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
)}
>
<UnreadPill

View File

@@ -892,3 +892,55 @@ test("does not shift the timeline when the composer grows", async ({
expect(Math.abs(after.scrollTop - before.scrollTop)).toBeLessThanOrEqual(2);
expect(after.distanceFromBottom).toBeGreaterThan(160);
});
test("lifts Jump to latest when the composer grows", async ({ page }) => {
const input = page.getByTestId("message-input");
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await ensureTimelineScrollable(page, `Jump pill growth ${Date.now()}`);
await page.waitForTimeout(400);
const timeline = page.getByTestId("message-timeline");
await timeline.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { deltaY: -100 }));
element.scrollTop = 0;
element.dispatchEvent(new Event("scroll", { bubbles: true }));
});
const jumpToLatest = page.getByTestId("message-scroll-to-latest");
const composer = page.getByTestId("message-composer");
await expect(jumpToLatest).toBeVisible();
const initialPillBox = await jumpToLatest.boundingBox();
const initialComposerBox = await composer.boundingBox();
await input.fill(
[
"Composer growth line one",
"Composer growth line two",
"Composer growth line three",
"Composer growth line four",
].join("\n"),
);
await expect
.poll(async () => (await composer.boundingBox())?.height ?? 0)
.toBeGreaterThan((initialComposerBox?.height ?? 0) + 40);
await page.waitForTimeout(250);
const expandedPillBox = await jumpToLatest.boundingBox();
const expandedComposerBox = await composer.boundingBox();
expect(initialPillBox).not.toBeNull();
expect(initialComposerBox).not.toBeNull();
expect(expandedPillBox).not.toBeNull();
expect(expandedComposerBox).not.toBeNull();
const composerGrowth =
(expandedComposerBox?.height ?? 0) - (initialComposerBox?.height ?? 0);
const pillLift = (initialPillBox?.y ?? 0) - (expandedPillBox?.y ?? 0);
expect(pillLift).toBeGreaterThanOrEqual(composerGrowth - 2);
expect(
(expandedPillBox?.y ?? 0) + (expandedPillBox?.height ?? 0),
).toBeLessThanOrEqual(expandedComposerBox?.y ?? 0);
});