Skip to content

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

Open
tenphi wants to merge 3 commits into
mainfrom
fix/textarea-autosize-layout-thrash
Open

fix(TextArea): measure autosize off-screen, not on the live element#1337
tenphi wants to merge 3 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-sensitive autosize rewrite for TextArea/CommandTextArea. Visual height can change (rows={1} is now one row, not two) and a hidden DOM node is appended for measurement.

Overview
Fixes CUB-4042: autoSize no longer collapses the live textarea (height: autoscrollHeight → restore) on every keystroke, which re-laid out a shared chat column and made the transcript bounce.

TextArea and CommandTextArea now share useAutoSizeTextArea, which copies typography onto a hidden mirror and sizes the real field from that. Row count is rounded from content height (no rows/font-metrics floor), so a single line with tight line-height is one row again; padding/border are added only under border-box.

Browser tests cover growth/shrink, maxRows, trailing newlines, and that typing does not move a sibling scroller. Consumers that relied on the extra empty row at rows={1} should pass rows={2}.

Reviewed by Cursor Bugbot for commit 98ee61a. 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 9:09am

Request Review

@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 98ee61a

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-a98ac41.

@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 4ffa6e0run 32466261879, 2026-08-21T09:05:59Z.

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>
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