Skip to content

fix: clear temporary web scroll padding after the browser re-serializes it - #538

Open
badcuban wants to merge 1 commit into
LegendApp:mainfrom
badcuban:fix/web-temporary-padding-serialization
Open

fix: clear temporary web scroll padding after the browser re-serializes it#538
badcuban wants to merge 1 commit into
LegendApp:mainfrom
badcuban:fix/web-temporary-padding-serialization

Conversation

@badcuban

@badcuban badcuban commented Sep 1, 2026

Copy link
Copy Markdown

Problem

On web, ScrollAdjust pads the content container so a scroll adjustment can move past content that has not grown yet, then reverts the padding on the next animation frame only if the inline style still equals the string it wrote:

const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`;
temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd };
contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd;
// next frame:
if (contentNode.style[axis.paddingEndProp] === temporaryPadding?.value) { /* revert */ }

Browsers re-serialize CSS lengths, keeping about six significant digits and dropping float noise:

el.style.paddingBottom = "607.46875px";          // reads back "607.469px"
el.style.paddingBottom = "258.15000000000003px"; // reads back "258.15px"

So the guard never matches and the padding stays forever. Users see permanent blank scrollable space below the last item, and scrollToEnd lands above it because getContentSize() does not include the padding. Fractional scroll offsets on non-integer device pixel ratios (a 125% Windows laptop, for example) make this frequent. A chat-style list with maintainScrollAtEnd and maintainVisibleContentPosition hits it within a few row updates.

Fixes #537. Reported by @szado; @chrisnojima's patch-package workaround in that thread takes the same approach.

Fix

Record the value the browser actually kept after the write, so the frame-later comparison is like for like. This covers both routes to the mismatch (six-digit truncation and float noise) without guessing which values round-trip.

Repro

In a Chromium app at devicePixelRatio 1.25, pin a vertical list at the end with initialScrollAtEnd, maintainScrollAtEnd and maintainVisibleContentPosition, then let rows near the end grow and shrink (tool-call rows in a chat transcript). Within a few updates getComputedStyle(contentContainer).paddingBottom is left at a few hundred px and the list scrolls into empty space. Captured in a real app: 140px and 406px of stuck padding while every row's position and size were correct.

Tests

  • New case in __tests__/components/ScrollAdjust.web.test.ts uses a style stand-in that re-serializes to six significant digits like a browser. It fails on main (Expected: "607px", Received: "607.469px") and passes with the fix.
  • bun test __tests__/components/ScrollAdjust.web.test.ts: 13 pass, 0 fail.
  • bun run lint, bun run tsc:src, bun run build: pass.
  • Full bun test on my Windows machine (bun 1.3.14) reports 247 failures on untouched main, all TypeError: undefined is not an object (evaluating 'listeners.get') across scrollToIndex, requestAdjust, findAvailableContainers and others, including the existing ScrollAdjust (web) cases. With this branch it is 248: the only difference is the new case, which fails there the same way the existing ones do and passes when the file runs alone. I could not get a clean full run in this environment, so please run it in yours.

No changelog edit: prep-changelog looks like the release-time script.

…es it

ScrollAdjust pads the content container so a scroll adjustment can move
past content that has not grown yet, then removes the padding on the next
animation frame only if the inline style still equals the string it wrote.
Browsers re-serialize CSS lengths to about six significant digits and drop
float noise ("607.46875px" reads back as "607.469px",
"258.15000000000003px" as "258.15px"), so the comparison never matched and
the padding stayed forever: permanent blank scrollable space below the
last item that getContentSize() knows nothing about. Fractional scroll
offsets on non-integer device pixel ratios make this common.

Record the value the browser actually stored after the write so the
frame-later comparison is like for like. The regression test covers the
six-digit truncation route; the same comparison also covers float noise.

Fixes LegendApp#537
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

badcuban added a commit to Threadlines/threadlines that referenced this pull request Sep 1, 2026
The release smoke job deletes pnpm-lock.yaml and resolves the workspace
from scratch. With "^3.3.9", that now picks 3.3.10 (published today),
and pnpm fails with ERR_PNPM_UNUSED_PATCH because our patch is keyed to
3.3.9. Pin the exact version the patch targets; bump both together when
upstream ships the fix (LegendApp/legend-list#538).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web: ScrollAdjust never removes its temporary padding-bottom (string comparison vs. CSS serialization)

1 participant