Skip to content

fix(VTextarea): keep native undo history with auto-grow - #23279

Merged
J-Sek merged 4 commits into
vuetifyjs:devfrom
b84330808:fix/23153-textarea-auto-grow-undo
Oct 19, 2026
Merged

J-Sek merged 4 commits into
vuetifyjs:devfrom
b84330808:fix/23153-textarea-auto-grow-undo

Conversation

@b84330808

@b84330808 b84330808 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

fixes #23153
fixes #21982 + undo/removing hides scrollbar if content actually fits within max-rows

This is the field-sizing: content approach @J-Sek suggested, with the JS sizer kept as the fallback.

Why undo breaks. auto-grow measures the content with a hidden sizer <textarea> that gets the value on every input. In Chromium, setting another textarea's value while the user is typing ends the current typing step, so every keystroke became its own undo step. Testing a plain page in Chromium, the same happens when nodes are replaced anywhere in the page, but not when a text node, an attribute or a style changes in place:

Change on each input event One Ctrl+Z after typing hello world foo
none ""
sizer.value = value (sync, microtask, requestAnimationFrame or setTimeout) "hello world fo"
div.textContent = value "hello world fo"
textNode.data = value, attribute or style changes ""

So deferring the sizer update doesn't help, and a <div> mirror would only work if it kept updating one text node in place.

What changes. After mount, if CSS.supports('field-sizing', 'content'):

  • The sizer isn't rendered, and the textarea gets field-sizing: content and height: auto (v-textarea--field-sizing).
  • rows and max-rows / max-height are computed as before and applied as --v-textarea-min-height / --v-textarea-max-height.
  • A ResizeObserver on the textarea updates rows (for update:rows and center-affix) and the control height from its size.

Browsers without field-sizing keep using the sizer as before. The check runs after mount, so SSR and hydration render the sizer as today.

Same sizes as before. In Chromium I rendered 18 combinations with both paths (the real one and the sizer forced via a mocked SUPPORTS_FIELD_SIZING) and compared the textarea height, field height, emitted rows and whether it scrolls. All were identical. The combinations were:

  • empty, rows 1 and 3, a trailing newline
  • wrapped text, max-rows 2 and 4, max-height
  • compact and comfortable density
  • outlined with a label, plain, underlined
  • prefix and suffix, counter, placeholder

One difference to expect: with field-sizing: content, an empty textarea is sized to its placeholder, so a placeholder longer than the field could make an empty field taller than before.

Tests.

  • A new browser test checks that one Ctrl+Z undoes the typed text. It fails without the change.
  • A shrink test checks that the field gets smaller after lines are removed.
  • VTextarea.sizer.spec.browser.tsx runs the sizer fallback with SUPPORTS_FIELD_SIZING mocked to false.

Markup:

<template>
  <v-app>
    <v-container style="max-width: 500px">
      <p class="mb-4">Type a few words, then press Ctrl+Z (Cmd+Z): the whole word run should be undone, not one character.</p>
      <v-textarea v-model="text" label="auto-grow" rows="1" auto-grow />
      <v-textarea v-model="text" label="auto-grow, max-rows 3" rows="1" max-rows="3" auto-grow />
      <v-textarea v-model="text" label="auto-grow, rows 3, outlined" rows="3" variant="outlined" auto-grow />
      <v-textarea v-model="text" label="auto-grow, compact" density="compact" rows="1" auto-grow @update:rows="rows = $event" />
      <p>rows: {{ rows }}</p>
    </v-container>
  </v-app>
</template>

<script setup>
  import { ref } from 'vue'

  const text = ref('')
  const rows = ref(1)
</script>

I used Claude Code for this change and reviewed it.

auto-grow measured the content with a hidden sizer textarea that gets the
value on every input. In Chromium, replacing the content of another
element while typing ends the current typing step, so Ctrl+Z undid one
character at a time.

Where `field-sizing: content` is supported, the textarea now sizes itself:
the rows and max rows limits are set as min/max height, and rows and the
field height follow its size. Other browsers keep using the sizer.

fixes #23153

Co-authored-by: Claude Opus 5.5 <[email protected]>
@J-Sek J-Sek added T: bug Functionality that does not work as intended/expected C: VTextarea labels Oct 19, 2026
@J-Sek
J-Sek marked this pull request as ready for review October 19, 2026 11:38
@J-Sek
J-Sek merged commit 4849474 into vuetifyjs:dev Oct 19, 2026
7 checks passed
@J-Sek

J-Sek commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Sorry for rushing it out of Draft, it looked really well overall 👍🏼
If you had Draft state because you intended to do more testing, then please feel free to do so and post your findings as bug reports (in case you find any regression).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C: VTextarea T: bug Functionality that does not work as intended/expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants