GrFormField
Берут, когда у поля есть подпись.
Обзор
Когда брать
- у поля есть подпись — она связывается с контролом по
idавтоматически, без ручногоfor; - поле показывает ошибку — текст, красная рамка и
aria-invalidприезжают в контрол через контекст; - поле участвует в валидации формы —
nameподключает его кGrForm; - подпись стоит сбоку —
labelPositionиlabelWidthдают горизонтальную раскладку без своей сетки.
Когда взять другое
| Нужно | Берите |
|---|---|
| Правила и блокировка отправки | GrForm |
| Контрол без подписи и ошибки | сам контрол: GrInput, GrSelect, … |
| Блок полей с заголовком | GrFormSection |
| Ошибка относится ко всей форме, а не к полю | GrResponseErrorBanner |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrFormField label="Email" hint="We never show it to other members."><GrInput modelValue="[email protected]" /></GrFormField>Примеры 3 у каждого — живое демо и его код
Ошибки сервера — по полям
Без GrForm судит только сервер: его ответ 422 раскладывается по полям через
error, который принимает строку или список. Контейнер сообщения живёт в DOM всё время и входит вaria-describedbyконтрола, поэтому скринридер слышит его вместе с полем.requiredставит маркер в подпись иaria-requiredна контрол.Invite a teammate Настройки — таблица пар
label-position="start"ставит подписи в свою колонку, аlabel-widthдержит её одинаковой во всех строках — выпадающий список, текстовое поле, число с единицей и заметка выстраиваются. Что значит значение, говорит сам контрол: следующий номер и период стоят в его аддонах.Invoice defaultsApplied to new invoices; existing ones keep their terms. next: INV-041963 / 160Группа переключателей с разметкой в подписи
Группа переключателей — роль, а не поле ввода, поэтому поле называет её через
aria-labelledby, аrequiredдоходит до каждого переключателя. Слотlabelнесёт бейдж, слотhint— ссылку: разметку, которую пропыlabelиhintне вмещают.Times are for Dublin. Shipping policy
Standard 2–3 working days · €4.90Express Tomorrow before 13:00 · €9.90Pick up in store Grafton Street, from today 17:00 · free
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Поле недоступно. Складывается с `disabled` формы по «или». |
readonly | boolean | undefined | false | Всё поле только для чтения: контролы внутри перестают редактироваться. |
required | boolean | undefined | false | Помечает поле обязательным: маркер `*` в подписи и `aria-required` у контрола. Маркер помечен `aria-hidden`, поэтому объявляет обязательность именно контрол — а чем именно, решает его роль. Разбор случая `role="group"`, который `aria-required` не допускает, — на странице компонента. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. |
name | string | undefined | undefined | Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по этому имени, а потеря фокуса триггерит валидацию поля. |
label | string | undefined | undefined | Подпись поля: `<label for>` контрола и его доступное имя. Слот `#label` сильнее. |
labelPosition | "top" | "start" | undefined | undefined | Подпись сверху (по умолчанию) или сбоку — для плотных форм. |
error | string | string[] | undefined | undefined | Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной режим без формы тоже здесь. Массив нужен там, где источник ошибок один, а претензий несколько: ответ сервера, валидация файла. |
labelWidth | string | number | undefined | undefined | Ширина колонки подписи при `labelPosition="start"`. Число — пиксели. |
reserveMessage | boolean | undefined | undefined | Держать место под строку сообщения, пока ошибки нет: появление текста не сдвигает форму, диалог и панель фиксированной высоты. Не задан — берётся у `GrForm` (`reserveMessage`), затем из `GrConfigProvider`. Несколько ошибок (`error: string[]`) по-прежнему растут ниже одной строки. |
forId | string | undefined | undefined | Явный id контрола. Если не задан — генерируется автоматически. |
hint | string | undefined | undefined | Подсказка под лейблом/над контролом (можно также через слот `#hint`). |
showMessage | boolean | undefined | true | Показывать текст ошибки. `false` — поле остаётся невалидным для контрола и AT (`aria-invalid`), но сообщение не занимает места: так делают в плотных таблицах-формах, где ошибка объясняется сводкой сверху. |
labelClass | LabelClass | undefined | undefined | Классы на `<label>` поверх встроенных — точечная правка подписи без своего слота. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Контрол поля. |
label | any | Подпись вместо пропа `label`. |
hint | any | Подсказка под контролом вместо пропа `hint`. |
error | { errors: string[]; } | Текст ошибки вместо стандартного. Получает уже разрешённый список. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrFormFieldContext, GrFormFieldConfigurableProps, GrFormFieldSize } from '@feugene/granularity/components/GrFormField'Файлdist/types/components/shared/formFieldContext.d.ts
Контекст поля формы, который GrFormField предоставляет своим контролам
(GrInput / GrSelect / GrTextarea). Контролы читают его через
{@link useGrFormFieldContext} и используют как fallback для id,
aria-describedby и aria-invalid — без ручного прокидывания forId.
interface GrFormFieldContext {
/** Сгенерированный (или переданный `forId`) id контрола. */
id: ComputedRef<string | undefined>;
/**
* Id элемента подписи. Нужен контролам, чей виджет — не labelable-элемент
* (`span[role="checkbox"]`, `div[role="radiogroup"]`): к ним `<label for>`
* не применяется, и имя приходит через `aria-labelledby`.
*/
labelId: ComputedRef<string | undefined>;
/** `aria-describedby`: id подсказки и/или сообщения об ошибке. */
describedById: ComputedRef<string | undefined>;
/** Невалидно ли поле (есть `error`). */
invalid: ComputedRef<boolean>;
/** Обязательное ли поле. */
required: ComputedRef<boolean>;
/**
* Поле только для чтения. Отличается от `disabled`: значение видно, уходит в
* форму и доступно скринридеру — просто не редактируется. Ставится на всё
* поле целиком, чтобы «форма только на чтение» не требовала обходить контролы
* по одному.
*/
readonly: ComputedRef<boolean>;
/**
* Поле недоступно: не фокусируется и не отправляется. Приходит от самого
* поля или от формы целиком (`GrForm disabled`) — «выключить форму на время
* отправки» не должно требовать обхода контролов по одному.
*/
disabled: ComputedRef<boolean>;
}Возвращает контекст поля формы или null, если контрол вне GrFormField.
function useGrFormFieldContext(): GrFormFieldContext | null;Файлdist/types/components/GrFormField/config.d.ts
const grFormFieldConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFormField">;Файлdist/types/components/GrFormField/defaults.d.ts
Пропы GrFormField, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrFormField: { … } }">.
Только оформление — см. GrButton/defaults.ts.
interface GrFormFieldConfigurableProps {
size: GrFormFieldSize;
labelPosition: GrFormFieldLabelPosition;
/** Держать место под строку сообщения — см. проп `reserveMessage`. */
reserveMessage: boolean;
}Файлdist/types/components/GrFormField/grFormFieldStyles.d.ts
type GrFormFieldSize = GrComponentSize;type GrFormFieldLabelPosition = typeof GR_FORM_FIELD_LABEL_POSITIONS[number];Файлdist/types/components/GrFormField/safelist.d.ts
const grFormFieldSafelist: string[];Файлdist/types/components/GrFormField/GrFormField.vue.d.ts
interface GrFormFieldProps {
/** Подпись поля: `<label for>` контрола и его доступное имя. Слот `#label` сильнее. */
label?: string;
/**
* Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри
* `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по
* этому имени, а потеря фокуса триггерит валидацию поля.
*/
name?: string;
/** Явный id контрола. Если не задан — генерируется автоматически. */
forId?: string;
/**
* Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной
* режим без формы тоже здесь. Массив нужен там, где источник ошибок один,
* а претензий несколько: ответ сервера, валидация файла.
*/
error?: string | string[];
/** Подсказка под лейблом/над контролом (можно также через слот `#hint`). */
hint?: string;
/**
* Помечает поле обязательным: маркер `*` в подписи и `aria-required` у контрола.
*
* Маркер помечен `aria-hidden`, поэтому объявляет обязательность именно
* контрол — а чем именно, решает его роль. Разбор случая `role="group"`,
* который `aria-required` не допускает, — на странице компонента.
*/
required?: boolean;
/** Всё поле только для чтения: контролы внутри перестают редактироваться. */
readonly?: boolean;
/** Поле недоступно. Складывается с `disabled` формы по «или». */
disabled?: boolean;
/**
* Показывать текст ошибки. `false` — поле остаётся невалидным для контрола
* и AT (`aria-invalid`), но сообщение не занимает места: так делают в
* плотных таблицах-формах, где ошибка объясняется сводкой сверху.
*/
showMessage?: boolean;
/** Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. */
size?: GrFormFieldSize;
/** Подпись сверху (по умолчанию) или сбоку — для плотных форм. */
labelPosition?: GrFormFieldLabelPosition;
/** Ширина колонки подписи при `labelPosition="start"`. Число — пиксели. */
labelWidth?: string | number;
/** Классы на `<label>` поверх встроенных — точечная правка подписи без своего слота. */
labelClass?: LabelClass;
/**
* Держать место под строку сообщения, пока ошибки нет: появление текста не
* сдвигает форму, диалог и панель фиксированной высоты. Не задан — берётся у
* `GrForm` (`reserveMessage`), затем из `GrConfigProvider`. Несколько ошибок
* (`error: string[]`) по-прежнему растут ниже одной строки.
*/
reserveMessage?: boolean;
}Использование из прозы библиотеки
Ошибка объявляется, а не появляется
Контейнер ошибки живёт в DOM всегда и меняет только текст, а aria-describedby
контрола всегда содержит его id. Причина: часть AT не перечитывает описание
после смены самого атрибута — ошибка, добавленная вместе с новым
aria-describedby, могла остаться непрочитанной. Пустой контейнер уходит в
sr-only, поэтому пустой строки в разметке поля не появляется.
error принимает и строку, и массив: у одного поля бывает несколько претензий
(ответ сервера, валидация файла). Внутри формы ошибка берётся из GrForm по
name, явный проп её перекрывает.
showMessage: false оставляет поле невалидным для контрола и AT, но текст не
показывает — для плотных форм, где ошибки объясняет сводка сверху.
Валидация по blur — только при реальном уходе
focusout всплывает и когда фокус переезжает внутри поля: с input на его же
кнопку очистки, между чекбоксами группы. Поле валидируется, только если фокус
ушёл за пределы корня (relatedTarget вне поля или null) — иначе оно
краснело раньше, чем пользователь его дозаполнил.
Связь подписи с контролом
<label for> указывает на id из контекста, а вешает этот id на себя сам
контрол (useGrFormFieldContext()). Если внутри поля такого элемента нет,
компонент в dev-режиме предупреждает в консоль: иначе клик по подписи молча
ничего не делает, а для скринридера связи нет вовсе.
Виджеты с ARIA-ролью (GrCheckbox, GrRadioGroup, GrCheckboxGroup)
<label for> не поддерживают — они берут имя через aria-labelledby на
field.labelId.
Свои связи складываются со связями поля
aria-describedby, aria-labelledby и aria-errormessage, поставленные на
контрол, уходят на элемент с ролью или на само поле ввода — не на обёртку, где
они ничего не значат, — и складываются с id подсказки и ошибки поля через
пробел, без повторов. Так строка с пояснением под чекбоксом вне GrFormField
объявляется, а в поле — объявляется вместе с подсказкой поля:
<template>
<GrCheckbox v-model="consent" aria-describedby="consent-terms-note">
Согласен
</GrCheckbox>
<p id="consent-terms-note">
Условия можно отозвать в профиле.
</p>
</template>Обязательность объявляет контрол, и роль решает чем
Маркер * в подписи помечен aria-hidden: для скринридера его нет вовсе, и
единственный источник обязательности — сам контрол. required из контекста он
забирает через useGrFormFieldContext() и превращает в aria-required.
Прямой путь открыт не всем ролям. aria-required и aria-readonly допускают
textbox, radiogroup, checkbox, combobox и соседи, а role="group" —
нет: axe роняет это как critical aria-allowed-attr. Контрол-группа поэтому
объявляет оба состояния своими фокусируемыми потомками, а не собой.
Работающий образец — GrCheckboxGroup: required
группы доезжает до aria-required каждого чекбокса.
aria-invalid, наоборот, остаётся на самой группе. Продублированный на каждом
пункте, он заставил бы диктор повторить «неверное значение» столько раз,
сколько в группе чекбоксов; пункты при ошибке только перекрашиваются.
Это касается и вашего виджета: роль с ограниченным набором атрибутов — не повод оставить поле без объявленной обязательности, а повод спустить её на потомков.
Раскладка
labelPosition="start" ставит подпись слева, labelWidth задаёт ширину её
колонки — иначе колонки контролов в форме разъезжаются по длине подписей.
Подсказка и ошибка остаются рядом с контролом: они про него, а не про подпись.
При подписи сбоку обе стоят под контролом, а подпись — высотой с
однострочный контрол своего размера и по центру, то есть на линии текста в поле.
Над контролом подсказка уводила бы подпись от поля: строки с подсказкой и без
неё разъезжались. Порядок описаний для AT задаёт aria-describedby (подсказка,
затем ошибка), а не DOM.
size (xs…lg) масштабирует подпись, подсказку, ошибку и вертикальный ритм;
берётся из GrConfigProvider, если не задан локально.
Без сдвигов раскладки
Ошибка рендерится в потоке под контролом: submit с тремя пустыми полями
добавляет три строки, и форма в диалоге, выезжающей панели или блоке
фиксированной высоты прыгает или обрезается. reserveMessage держит место под
строку сообщения, пока ошибки нет, — появление текста высоту поля не меняет:
<template>
<GrFormField label="IBAN" :error="ibanError" reserve-message>
<GrInput v-model="iban" />
</GrFormField>
</template>Не задан у поля — берётся у GrForm (reserveMessage на всю форму), затем из
GrConfigProvider (componentDefaults.GrFormField.reserveMessage). Свой проп
поля сильнее: :reserve-message="false" снимает резерв у одного поля в
резервирующей форме. Резервируется одна строка: несколько ошибок
(error: string[]) и длинный текст, ушедший на вторую строку, по-прежнему
растут ниже. showMessage: false резерва не держит — текста не будет.
Слоты
#label, #hint, #error (получает errors: string[]) и default — сам
контрол.
Чего нет
validateStatus (validating/success): у GrForm нет канала состояния
асинхронного правила, и проп был бы чисто ручным — сначала нужен статус на
стороне формы.
Пропы этого примера