GrFormField

  • ядро
  • Формы
  • API · 14 props

Берут, когда у поля есть подпись.

import { GrFormField } from '@feugene/granularity/components/GrFormField'

Обзор

Когда брать

  • у поля есть подпись — она связывается с контролом по id автоматически, без ручного for;
  • поле показывает ошибку — текст, красная рамка и aria-invalid приезжают в контрол через контекст;
  • поле участвует в валидации формы — name подключает его к GrForm;
  • подпись стоит сбоку — labelPosition и labelWidth дают горизонтальную раскладку без своей сетки.

Когда взять другое

НужноБерите
Правила и блокировка отправкиGrForm
Контрол без подписи и ошибкисам контрол: GrInput, GrSelect, …
Блок полей с заголовкомGrFormSection
Ошибка относится ко всей форме, а не к полюGrResponseErrorBanner

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 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-0419
    63 / 160
  • Группа переключателей с разметкой в подписи

    Группа переключателей — роль, а не поле ввода, поэтому поле называет её через aria-labelledby, а required доходит до каждого переключателя. Слот label несёт бейдж, слот hint — ссылку: разметку, которую пропы label и hint не вмещают.

    Times are for Dublin. Shipping policy

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
disabledboolean | undefinedfalseПоле недоступно. Складывается с `disabled` формы по «или».
readonlyboolean | undefinedfalseВсё поле только для чтения: контролы внутри перестают редактироваться.
requiredboolean | undefinedfalseПомечает поле обязательным: маркер `*` в подписи и `aria-required` у контрола. Маркер помечен `aria-hidden`, поэтому объявляет обязательность именно контрол — а чем именно, решает его роль. Разбор случая `role="group"`, который `aria-required` не допускает, — на странице компонента.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedРазмер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`.
namestring | undefinedundefinedИмя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по этому имени, а потеря фокуса триггерит валидацию поля.
labelstring | undefinedundefinedПодпись поля: `<label for>` контрола и его доступное имя. Слот `#label` сильнее.
labelPosition"top" | "start" | undefinedundefinedПодпись сверху (по умолчанию) или сбоку — для плотных форм.
errorstring | string[] | undefinedundefinedЯвная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной режим без формы тоже здесь. Массив нужен там, где источник ошибок один, а претензий несколько: ответ сервера, валидация файла.
labelWidthstring | number | undefinedundefinedШирина колонки подписи при `labelPosition="start"`. Число — пиксели.
reserveMessageboolean | undefinedundefinedДержать место под строку сообщения, пока ошибки нет: появление текста не сдвигает форму, диалог и панель фиксированной высоты. Не задан — берётся у `GrForm` (`reserveMessage`), затем из `GrConfigProvider`. Несколько ошибок (`error: string[]`) по-прежнему растут ниже одной строки.
forIdstring | undefinedundefinedЯвный id контрола. Если не задан — генерируется автоматически.
hintstring | undefinedundefinedПодсказка под лейблом/над контролом (можно также через слот `#hint`).
showMessageboolean | undefinedtrueПоказывать текст ошибки. `false` — поле остаётся невалидным для контрола и AT (`aria-invalid`), но сообщение не занимает места: так делают в плотных таблицах-формах, где ошибка объясняется сводкой сверху.
labelClassLabelClass | undefinedundefinedКлассы на `<label>` поверх встроенных — точечная правка подписи без своего слота.

Слоты

SlotTypeОписание
defaultanyКонтрол поля.
labelanyПодпись вместо пропа `label`.
hintanyПодсказка под контролом вместо пропа `hint`.
error{ errors: string[]; }Текст ошибки вместо стандартного. Получает уже разрешённый список.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrFormFieldContext, GrFormFieldConfigurableProps, GrFormFieldSize } from '@feugene/granularity/components/GrFormField'

Файлdist/types/components/shared/formFieldContext.d.ts

GrFormFieldContextinterface

Контекст поля формы, который 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>;
}
useGrFormFieldContextfunction

Возвращает контекст поля формы или null, если контрол вне GrFormField.

function useGrFormFieldContext(): GrFormFieldContext | null;

Файлdist/types/components/GrFormField/config.d.ts

grFormFieldConfigconst
const grFormFieldConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFormField">;

Файлdist/types/components/GrFormField/defaults.d.ts

GrFormFieldConfigurablePropsinterface

Пропы GrFormField, настраиваемые глобально через <GrConfigProvider :component-defaults="{ GrFormField: { … } }">. Только оформление — см. GrButton/defaults.ts.

interface GrFormFieldConfigurableProps {
    size: GrFormFieldSize;
    labelPosition: GrFormFieldLabelPosition;
    /** Держать место под строку сообщения — см. проп `reserveMessage`. */
    reserveMessage: boolean;
}

Файлdist/types/components/GrFormField/grFormFieldStyles.d.ts

GrFormFieldSizetype
type GrFormFieldSize = GrComponentSize;
GrFormFieldLabelPositiontype
type GrFormFieldLabelPosition = typeof GR_FORM_FIELD_LABEL_POSITIONS[number];

Файлdist/types/components/GrFormField/safelist.d.ts

grFormFieldSafelistconst
const grFormFieldSafelist: string[];

Файлdist/types/components/GrFormField/GrFormField.vue.d.ts

GrFormFieldPropsinterface
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 объявляется, а в поле — объявляется вместе с подсказкой поля:

vue
<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 держит место под строку сообщения, пока ошибки нет, — появление текста высоту поля не меняет:

vue
<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 нет канала состояния асинхронного правила, и проп был бы чисто ручным — сначала нужен статус на стороне формы.