Skip to content

fix(TextArea): measure autosize off-screen, not on the live element - #1337

Merged
tenphi merged 4 commits into
mainfrom
fix/textarea-autosize-layout-thrash
Aug 21, 2026
Merged

fix(TextArea): measure autosize off-screen, not on the live element#1337
tenphi merged 4 commits into
mainfrom
fix/textarea-autosize-layout-thrash

Conversation

@tenphi

@tenphi tenphi commented Aug 20, 2026

Copy link
Copy Markdown
Member

Fixes CUB-4042 — the Cube Cloud chat UI jittering in time with typing.

What was happening

autoSize measured the textarea by mutating the live element: height: auto → force a layout (getComputedStyle + scrollHeight) → restore. That ran twice per keystroke (chained onto onChange, then again in the layout effect on the value), plus once more from its own ResizeObserver.

height: auto sizes a textarea from its rows attribute, so a textarea that has grown past its minimum — the everyday state of a chat prompt — collapses to one row for the duration of that forced layout. Every ancestor sharing the column re-lays out with it: the input box shrinks, and a scroll container above it grows by the same amount and has its scroll offset moved. Chrome's scroll anchoring normally undoes that, which is why nobody saw a 40px jump; what leaks through is the repair not landing exactly, and the whole conversation bounces a pixel and back on each keystroke.

Frame-by-frame off the bug report's screen recording: the message column shifts +1px then −1px within one or two frames of every keystroke, while the input box itself does not move.

What this changes

  • Measurement moved off-screen. TextArea and CommandTextArea now share useAutoSizeTextArea, which measures a hidden mirror textarea instead of the live one. The mirror carries no padding or border and is sized to the live element's content width (scrollbar excluded), so scrollHeight is the content height and nothing else. No ancestor ever re-lays out, so there is nothing for scroll anchoring to repair.
  • Row counting fixed. The old code divided the rows-and-font-metrics-derived auto height by CSS line-height. Where the line height is tighter than the font's natural line box, one line counted as two rows — an autoSize textarea with rows={1} rendered a row taller than its content (visible today in the Cloud chat input). Measuring at height: 0 removes that floor; the row count now rounds, so a fractional line height (zoomed page, percentage preset) can no longer add a phantom row either.
  • height now respects box-sizing. Padding and border are added only under border-box, which is what <Root> sets globally.
  • A textarea with no layout yet (display: none, pre-layout) is skipped rather than measured at zero width, where every character wraps and the height pins to maxRows.

Behaviour change worth knowing

An autoSize textarea with rows={1} and a single line of text is now one row tall instead of two, wherever the line height is tighter than the font's line box. Consumers relying on the taller box should pass rows={2}.

Tests

New *.browser.test.tsx specs — real layout, which is the only place this is visible; jsdom reports 0 for every box:

  • TextArea.browser.test.tsx — a single line is one row, growth and shrink, maxRows cap, rows minimum.
  • CommandTextArea.browser.test.tsx — a single line is one row, and typing does not move a sibling scroll container (the CUB-4042 regression). That test disables overflow-anchor on purpose: with anchoring left on, the browser hides the perturbation and the assertion would pass either way.

Verified against the unfixed code: 4 of the 6 fail, the scroll one with expected 512 to be 552 — 40px of scroll offset lost on a single keystroke. Full suites green: 2058 jsdom tests, 114 browser tests.

🤖 Generated with Claude Code


Note

Medium Risk
Layout math for shared TextArea / CommandTextArea autosize changes visible height (a rows={1} single line is now one row, not two) and injects a DOM mirror; chat/prompt UIs will feel this immediately.

Overview
Stops chat-input jitter (CUB-4042) by measuring autoSize height off-screen instead of setting height: auto on the live textarea (which collapsed grown prompts, re-laid out the column, and left scroll anchoring bouncing the transcript).

TextArea and CommandTextArea now share useAutoSizeTextArea: a hidden mirror copies typography and content width, so scrollHeight is true content height with no ancestor layout. Row count is rounded from that measure (tight line-height no longer turns one line into two), padding/border are added only under border-box, and zero-width/pre-layout fields are skipped.

Visible change: autoSize + rows={1} with one line is now one row tall. Browser tests cover growth/shrink, maxRows, trailing newlines, and that typing does not move a sibling scroller.

Reviewed by Cursor Bugbot for commit 42f8853. Bugbot is set up for automated code reviews on this repo. Configure here.

`autoSize` sized the textarea by setting `height: auto` on the real
element, forcing a layout, then restoring it — twice per keystroke. Every
ancestor sharing the column re-laid out mid-keystroke, so a chat
transcript above the input had its scroll viewport grow by the collapsed
rows and its scroll offset moved; scroll anchoring undid that
imperfectly, which reads as the conversation bouncing a pixel in time
with the typing (CUB-4042).

Both `TextArea` and `CommandTextArea` now share
`useAutoSizeTextArea`, which measures an off-screen mirror. The mirror
carries no padding or border and is sized to the live element's content
width, so `scrollHeight` is the content height with none of the `rows`
floor that also made one line count as two rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cube-ui-kit Ready Ready Preview Aug 21, 2026 12:48pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 42f8853

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cube-dev/ui-kit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-5bbff30.

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 497.42 KB (+0.08% 🔺) Yes 🎉
Tree shaking (just a Button) 120.73 KB (0% 🟰) Yes 🎉

Compared against main at 99843f4run 32478561414, 2026-08-21T11:44:06Z.

To see which modules changed, download the size-limit-statoscope-report artifact from this run and open report.html.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7bd1d4a. Configure here.

Comment thread src/components/fields/TextInput/useAutoSizeTextArea.ts
A mirror *div* collapses a trailing newline, which is why autosize
implementations built on one need a sentinel character. The mirror here
is a textarea, so it lays the empty last row out exactly like the live
field — asserted rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tenphi
tenphi merged commit 4594454 into main Aug 21, 2026
16 checks passed
@tenphi
tenphi deleted the fix/textarea-autosize-layout-thrash branch August 21, 2026 13:07
@tenphi tenphi mentioned this pull request Aug 21, 2026
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.

1 participant