GrTextarea
Берут, когда текст длиннее строки.
Когда брать
- текст длиннее строки — комментарий, описание, письмо: перенос строк — часть значения;
- высота подстраивается —
autosizeсmaxLinesрастёт по содержимому и не съедает экран; - длина ограничена — счётчик символов и строк рядом с полем;
- пользователь меняет размер сам —
resizeвместо фиксированной высоты.
Когда взять другое
| Нужно | Берите |
|---|---|
| Значение в одну строку | GrInput |
| Нужно форматирование текста | rich-text редактора в экосистеме пока нет |
| Вводится число | GrNumberInput |
| Длинный текст запрашивают окном | GrPromptDialog с multiline |
События
| Событие | Когда |
|---|---|
update:modelValue | на каждый ввод |
change | значение зафиксировано: нативный change (по уходу фокуса) или кнопка очистки |
clear | значение стёрто кнопкой очистки (clearable) |
focus, blur | с объектом FocusEvent |
Набор совпадает с GrInput, чтобы обёртки над контролами
писались одинаково. Нативные события переизлучаются компонентом: объявленный
emit уходит из $attrs, и без этого @change у потребителя перестал бы
работать.
clearable повторяет анатомию GrInput: крестик в правом верхнем углу при
непустом значении, скрыт при disabled/readonly, настраивается через
GrConfigProvider. Обёртка вокруг textarea появляется только под кнопку —
без clearable и счётчиков (showCount, showLineCount) поле остаётся корневым
элементом, и контракт fallthrough-атрибутов не меняется.
Паритет с `GrInput`
size, readonly, maxlength + showCount работают так же, как в поле ввода:
textarea в одной форме рядом с GrInput не должна отличаться ни кеглем, ни
набором возможностей.
Счётчик связан с полем через aria-describedby — иначе «12 / 60» видно
глазами, но не слышно, при том что ограничение длины и есть его смысл.
Счётчик строк
showLineCount печатает вторую подпись в том же ряду: слева строки, справа
символы. Строки считаются логическими — по переводам строки, — поэтому при
autosize число не зависит от ширины поля: визуальный перенос строкой не считается.
Подпись локализована и склоняется (gr.textarea.lines), а maxLines меняет её на
формат 3 / 10. Ограничения ввода у него нет намеренно: обрезать набранный текст
за пользователя компонент не вправе — счётчик показывает перебор (12 / 10), а
решение остаётся за формой.
Оба счётчика включаются независимо и оба связаны с полем через aria-describedby.
Автоподгонка высоты
<GrTextarea v-model="text" autosize :rows="2" />
autosize включает директиву v-autosize, которая уже была в пакете и просто
не была подключена к компоненту. rows задаёт стартовую высоту.
resize (vertical по умолчанию, none, both) управляет ручным
растягиванием: вместе с autosize его обычно выключают.
Тип пропов
GrTextareaProps — объявленный interface, а не typeof props. Прежний
экспорт отдавал тип разрешённых пропов: после withDefaults все поля с
дефолтами становились обязательными и readonly, и попытка написать
const p: GrTextareaProps = { modelValue: '' } падала на ровном месте.
Отключённое состояние
Гасится фоном --gr-muted и текстом --gr-muted-fg, а не opacity:
прозрачность разбавляет выверенные на AA токены.
Playground 20
Загружается…
<GrTextarea />Установка
npm i @feugene/granularityИмпорт
import { GrTextarea } from '@feugene/granularity/components/GrTextarea'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | Только для чтения: значение видно и уходит в форму, но не редактируется. |
invalid | boolean | undefined | false | — |
required | boolean | undefined | false | Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
placeholder | string | undefined | undefined | — |
ariaLabel | string | undefined | undefined | Доступное имя вне `GrFormField`. |
clearable | boolean | undefined | undefined | Кнопка очистки значения. Настраивается через `GrConfigProvider`. |
clearLabel | string | undefined | undefined | A11y-подпись кнопки очистки. |
name | string | undefined | undefined | — |
id | string | undefined | undefined | — |
rows | number | undefined | 4 | — |
state | "default" | "success" | "warning" | "danger" | undefined | "default" | — |
autocomplete | string | undefined | undefined | — |
maxlength | number | undefined | undefined | Ограничение длины + основа для счётчика символов. |
showCount | boolean | undefined | false | Показывать счётчик символов (`len` или `len/maxlength`). |
autosize | boolean | undefined | false | Подгонять высоту под содержимое (директива `v-autosize`). |
resize | GrTextareaResize | undefined | "vertical" | Ручное изменение размера пользователем. |
modelValueобязательный | string | — | — |
showLineCount | boolean | undefined | — | Показывать счётчик строк. Считаются **логические** строки (переводы строки), а не визуальные переносы: при `autosize` число не меняется от ширины поля. |
maxLines | number | undefined | — | Ориентир по числу строк для счётчика (`3 / 10`). Ввод не ограничивает: обрезать набранный текст за пользователя компонент не вправе. |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: string] | — |
change | [value: string] | — |
clear | [] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Описание |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Примеры 7
Счётчик строк
Строки считаются логическими — переводами строки, а не визуальными переносами, поэтому при autosize число не зависит от ширины поля. maxLines задаёт ориентир и намеренно не режет набранное.
Счётчик строк слева, символов — справа
maxLines задаёт ориентир и не режет набранное
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const changelog = ref('fix: чипы автокомплита видно на светлой теме\nfeat: счётчик строк\n')
const script = ref('INT. OFFICE — DAY\n\nОна открывает ноутбук.\n')
</script>
<template>
<div class="grid gap-4">
<GrFormField label="Запись в CHANGELOG" hint="Счётчик строк слева, символов — справа">
<GrTextarea
v-model="changelog"
show-line-count
show-count
:maxlength="240"
:rows="4"
/>
</GrFormField>
<GrFormField label="Сцена" hint="maxLines задаёт ориентир и не режет набранное">
<GrTextarea
v-model="script"
show-line-count
:max-lines="8"
autosize
:rows="3"
/>
</GrFormField>
</div>
</template>Автовысота и счётчик символов
autosize поверх директивы v-autosize, maxlength + showCount со связкой через aria-describedby.
Высота подстраивается под содержимое
Счётчик связан с полем через aria-describedby
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const comment = ref('Поле растёт под текст: директива `v-autosize` была в пакете и просто не была подключена.')
const summary = ref('')
</script>
<template>
<div class="grid gap-4 lg:grid-cols-2">
<GrFormField label="Комментарий" hint="Высота подстраивается под содержимое">
<GrTextarea
v-model="comment"
autosize
resize="none"
:rows="2"
placeholder="Что изменилось в релизе"
/>
</GrFormField>
<GrFormField label="Краткое описание" hint="Счётчик связан с полем через aria-describedby">
<GrTextarea
v-model="summary"
:maxlength="120"
show-count
:rows="3"
size="sm"
placeholder="До 120 символов"
/>
</GrFormField>
</div>
</template>Кнопка очистки
clearable добавляет крестик, когда в поле есть текст. Кнопка стоит в таб-порядке сразу за полем, срабатывает на Enter и Space и возвращает фокус в поле — очистка с клавиатуры не выкидывает пользователя из формы.
Крестик появляется, когда есть что стирать
Кнопка очистки достижима `Tab`, срабатывает `Enter` и `Space`
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const note = ref('Клиент просил перезвонить после 18:00.\nНапомнить про счёт за март.')
const draft = ref('')
</script>
<template>
<div class="grid gap-4">
<GrFormField label="Заметка по клиенту" hint="Крестик появляется, когда есть что стирать">
<GrTextarea v-model="note" clearable :rows="3" />
</GrFormField>
<GrFormField label="Черновик письма" hint="Кнопка очистки достижима `Tab`, срабатывает `Enter` и `Space`">
<GrTextarea
v-model="draft"
clearable
show-count
:maxlength="200"
:rows="3"
placeholder="Наберите текст — появится счётчик и крестик"
/>
</GrFormField>
</div>
</template>Стартовая и увеличенная высота
Базовый сценарий для short-form и long-form контента: одна и та же textarea может быть компактной или сразу подготовленной под большой объём текста.
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const shortNote = ref('Release notes highlight the latest API additions.')
const longNote = ref('This textarea starts taller and fits editorial copy, migration notes or incident postmortems.')
</script>
<template>
<div class="grid gap-4 lg:grid-cols-2">
<GrFormField label="Default rows">
<GrTextarea v-model="shortNote" placeholder="Write a short note" />
</GrFormField>
<GrFormField label="Expanded rows">
<GrTextarea v-model="longNote" :rows="8" placeholder="Long-form content" />
</GrFormField>
</div>
</template>Успех и состояния проверки
Показываем state-driven оформление и связку с form-errors без искусственной ручной таблицы API.
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrSwitch, GrTextarea } from '@feugene/granularity'
const draft = ref('Ship the new showcase after validating all public entities.')
const reviewNotes = ref('')
const invalid = ref(false)
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<div class="grid gap-3">
<GrFormField label="Success state">
<GrTextarea v-model="draft" state="success" />
</GrFormField>
<GrFormField label="Validation state" :error="invalid ? 'Review notes are required before publishing' : undefined">
<GrTextarea
v-model="reviewNotes"
placeholder="Add review notes"
:invalid="invalid"
:state="invalid ? 'danger' : 'default'"
/>
</GrFormField>
</div>
<div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<div class="text-sm font-semibold text-[var(--gr-fg)]">
Validation toggle
</div>
<GrSwitch v-model="invalid" size="sm">
Mark review notes as required
</GrSwitch>
</div>
</div>
</template>Режим просмотра и аудита
Отдельно фиксируем, как GrTextarea выглядит в readonly-like review flow, когда поле временно недоступно для редактирования.
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrSwitch, GrTextarea } from '@feugene/granularity'
const disabled = ref(false)
const summary = ref('Subscribers will receive a digest every Monday at 09:00.')
</script>
<template>
<div class="grid gap-4">
<div class="flex items-center gap-3">
<GrSwitch v-model="disabled" size="sm">
Disable textarea
</GrSwitch>
</div>
<GrFormField label="Operational notes">
<GrTextarea
v-model="summary"
:disabled="disabled"
:rows="6"
placeholder="Editable summary"
/>
</GrFormField>
</div>
</template>Шкала размеров и почему она общая
Поле и textarea рядом обязаны читаться одинаково: size у обоих берётся из одной шкалы xs…lg, поэтому форма не рассыпается при смене масштаба.
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrInput, GrTextarea } from '@feugene/granularity'
const sizes = ['xs', 'sm', 'md', 'lg'] as const
const title = ref('Weekly digest')
const note = ref('Subscribers receive this summary every Monday at 09:00.')
</script>
<template>
<div class="grid gap-4">
<div v-for="size in sizes" :key="size" class="grid gap-2">
<div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
size="{{ size }}"
</div>
<GrFormField label="Title">
<GrInput v-model="title" :size="size" />
</GrFormField>
<GrFormField label="Note">
<GrTextarea v-model="note" :size="size" :rows="2" />
</GrFormField>
</div>
</div>
</template>Доступность
- Паттерн APG
—- Клавиши
Tabиз поля — на кнопку очистки приclearable;Enter/Spaceочищает и возвращает фокус в поле