fix(TextArea): measure autosize off-screen, not on the live element - #1337
fix(TextArea): measure autosize off-screen, not on the live element#1337tenphi wants to merge 3 commits into
Conversation
`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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 98ee61a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
📦 NPM canary releaseDeployed canary version 0.0.0-canary-a98ac41. |
🧪 Storybook is successfully deployed!
|
🏋️ Size limit report
Compared against main at 4ffa6e0 — run 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. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
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>

Fixes CUB-4042 — the Cube Cloud chat UI jittering in time with typing.
What was happening
autoSizemeasured the textarea by mutating the live element:height: auto→ force a layout (getComputedStyle+scrollHeight) → restore. That ran twice per keystroke (chained ontoonChange, then again in the layout effect on the value), plus once more from its ownResizeObserver.height: autosizes a textarea from itsrowsattribute, 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
+1pxthen−1pxwithin one or two frames of every keystroke, while the input box itself does not move.What this changes
TextAreaandCommandTextAreanow shareuseAutoSizeTextArea, 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), soscrollHeightis the content height and nothing else. No ancestor ever re-lays out, so there is nothing for scroll anchoring to repair.rows-and-font-metrics-derivedautoheight by CSSline-height. Where the line height is tighter than the font's natural line box, one line counted as two rows — anautoSizetextarea withrows={1}rendered a row taller than its content (visible today in the Cloud chat input). Measuring atheight: 0removes that floor; the row count now rounds, so a fractional line height (zoomed page, percentage preset) can no longer add a phantom row either.heightnow respectsbox-sizing. Padding and border are added only underborder-box, which is what<Root>sets globally.display: none, pre-layout) is skipped rather than measured at zero width, where every character wraps and the height pins tomaxRows.Behaviour change worth knowing
An
autoSizetextarea withrows={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 passrows={2}.Tests
New
*.browser.test.tsxspecs — 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,maxRowscap,rowsminimum.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 disablesoverflow-anchoron 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:
autoSizeno longer collapses the live textarea (height: auto→scrollHeight→ restore) on every keystroke, which re-laid out a shared chat column and made the transcript bounce.TextAreaandCommandTextAreanow shareuseAutoSizeTextArea, which copies typography onto a hidden mirror and sizes the real field from that. Row count is rounded from content height (norows/font-metrics floor), so a single line with tightline-heightis one row again; padding/border are added only underborder-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 atrows={1}should passrows={2}.Reviewed by Cursor Bugbot for commit 98ee61a. Bugbot is set up for automated code reviews on this repo. Configure here.