GrSwitch
Берут, когда настройка включается сразу.
Обзор
Когда брать
- настройка включается сразу — уведомления, тёмная тема, доступ по ссылке: кнопки «Сохранить» рядом нет;
- применение идёт на сервер —
loadingдержит состояние на время запроса, не давая переключить дважды; - состояние важнее выбора — «включено/выключено» читается быстрее пары вариантов;
- значение не булево —
valueзадаёт, что уходит наружу во включённом положении.
Когда взять другое
| Нужно | Берите |
|---|---|
| Значение уедет с отправкой формы | GrCheckbox |
| Вариантов два, но это режимы отображения | GrSegmented |
| Вариантов больше двух | GrRadioGroup |
| Значений несколько | GrCheckboxGroup |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrSwitch>Weekly revenue digest</GrSwitch>Примеры 3 у каждого — живое демо и его код
Уведомления, которые применяются сразу
Переключатель — для изменения, которое происходит сразу: «Сохранить» внизу нет.
:model-valueс@update:model-valueдвигают его, только когда сервер согласен; пока запрос в пути,loadingкрутит спиннер в бегунке, не даёт переключить второй раз и черезloading-textговорит, что сохраняется. Дайджест не сохраняется и остаётся выключенным.label-position="start"ставит текст первым, уведомления о спорах —disabled.Флаги функций по окружениям
В колонке флагов состояние должно читаться само по себе:
show-state-textпишет его внутри дорожки. Флаг, удалённый из кода, —readonly: виден и фокусируется, но отсюда не переключается. Аварийный выключатель окрашен черезactive-background-color, а слова на нём берут подходящий цвет через--gr-switch-state-text-fg.Flag Staging Production proration-v2New proration for seats added mid-cyclesepa-instantSEPA Instant payoutslegacy-vatOld VAT lookup — removed in 4.2Maintenance modeCheckout shows «Back in a few minutes»; webhooks are queued.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Недоступен: не переключается и в форму не уходит. Складывается по «или» с полем и формой. |
modelValuev-model | boolean | undefined | false | Значение переключателя. Необязательное: без `v-model` контрол рисуется выключенным — так же, как `GrCheckbox`. Обязательный проп заставлял бы заводить `ref` даже там, где переключатель presentational. |
readonly | boolean | undefined | false | Только для чтения: состояние видно, но не переключается. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер дорожки и бегунка: `xs`–`lg`. По умолчанию `md`; берётся из `GrConfigProvider`. |
ariaLabel | string | undefined | undefined | Доступное имя, когда подписи нет ни в слоте, ни у `GrFormField`. |
loading | boolean | undefined | false | Идёт сохранение: бегунок показывает спиннер, переключение заблокировано. |
loadingText | string | undefined | undefined | i18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет. |
name | string | undefined | undefined | Имя поля для нативной отправки формы. Без него скрытое поле не рендерится. |
value | string | undefined | "on" | Значение, уходящее в форму во включённом состоянии. |
form | string | undefined | undefined | `id` формы, если переключатель лежит вне неё. |
labelPosition | "end" | "start" | undefined | "end" | Сторона подписи относительно дорожки. |
activeBackgroundColor | string | undefined | undefined | Кастомный цвет фона в активном состоянии. Если не задан — `var(--gr-primary)`. |
inactiveBackgroundColor | string | undefined | undefined | Кастомный цвет фона в неактивном состоянии. Если не задан — `var(--gr-muted)`. |
showStateText | boolean | undefined | undefined | Подпись состояния внутри дорожки — на свободной от бегунка стороне. Помещается только на `md` и `lg`; на мелких ступенях не рисуется. |
checkedText | string | undefined | undefined | Текст включённого состояния. Без него — `gr.switch.on` из локали. |
uncheckedText | string | undefined | undefined | Текст выключенного состояния. Без него — `gr.switch.off` из локали. |
autoWidth | boolean | undefined | undefined | Дорожка растягивается под подпись, а ступень размера становится нижней границей ширины. Без `showStateText` смысла не имеет: тянуться не под что. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Подпись переключателя. |
checked-icon | any | Знак на бегунке во включённом состоянии — галочка, замок, что угодно. Размер задаёт обёртка по ступени `size`, поэтому содержимое слота тянется до неё: `class="h-full w-full"`, как у `GrBadge` и `GrChip`. Знак декоративен — бегунок целиком `aria-hidden`, а состояние диктор берёт из `aria-checked`. Смысла, которого нет больше нигде, сюда класть нельзя. |
unchecked-icon | any | То же для выключенного состояния. Слоты независимы. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: boolean): void | — |
change | (event: "change", value: boolean): void | — |
focus | (event: "focus", event: FocusEvent): void | — |
blur | (event: "blur", event: FocusEvent): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
focus | () => void | Поставить фокус на переключатель. |
blur | () => void | Снять фокус с переключателя. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrSwitchConfigurableProps, GrSwitchSize, GrSwitchLabelPosition } from '@feugene/granularity/components/GrSwitch'Файлdist/types/components/GrSwitch/config.d.ts
const grSwitchConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSwitch">;Файлdist/types/components/GrSwitch/defaults.d.ts
Пропы GrSwitch, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrSwitch: { … } }">.
Только оформление — см. GrButton/defaults.ts.
interface GrSwitchConfigurableProps {
size: GrSwitchSize;
showStateText: boolean;
autoWidth: boolean;
}Файлdist/types/components/GrSwitch/safelist.d.ts
const grSwitchSafelist: string[];Файлdist/types/components/GrSwitch/grSwitchStyles.d.ts
type GrSwitchSize = GrComponentSize;type GrSwitchLabelPosition = typeof GR_SWITCH_LABEL_POSITIONS[number];type GrSwitchStateTextSize = typeof GR_SWITCH_STATE_TEXT_SIZES[number];Файлdist/types/components/GrSwitch/GrSwitch.vue.d.ts
Пропсы публичного GR-примитива «Switch».
interface GrSwitchProps {
/**
* Значение переключателя. Необязательное: без `v-model` контрол рисуется
* выключенным — так же, как `GrCheckbox`. Обязательный проп заставлял бы
* заводить `ref` даже там, где переключатель presentational.
*/
modelValue?: boolean;
/** Недоступен: не переключается и в форму не уходит. Складывается по «или» с полем и формой. */
disabled?: boolean;
/** Только для чтения: состояние видно, но не переключается. */
readonly?: boolean;
/** Визуальное и ARIA-состояние ошибки. */
invalid?: boolean;
/** Обязательное поле (`aria-required`). */
required?: boolean;
/** Доступное имя, когда подписи нет ни в слоте, ни у `GrFormField`. */
ariaLabel?: string;
/** Размер дорожки и бегунка: `xs`–`lg`. По умолчанию `md`; берётся из `GrConfigProvider`. */
size?: GrSwitchSize;
/** Сторона подписи относительно дорожки. */
labelPosition?: GrSwitchLabelPosition;
/** Идёт сохранение: бегунок показывает спиннер, переключение заблокировано. */
loading?: boolean;
/** i18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет. */
loadingText?: string;
/** Имя поля для нативной отправки формы. Без него скрытое поле не рендерится. */
name?: string;
/** Значение, уходящее в форму во включённом состоянии. */
value?: string;
/** `id` формы, если переключатель лежит вне неё. */
form?: string;
/** Кастомный цвет фона в активном состоянии. Если не задан — `var(--gr-primary)`. */
activeBackgroundColor?: string;
/** Кастомный цвет фона в неактивном состоянии. Если не задан — `var(--gr-muted)`. */
inactiveBackgroundColor?: string;
/**
* Подпись состояния внутри дорожки — на свободной от бегунка стороне.
* Помещается только на `md` и `lg`; на мелких ступенях не рисуется.
*/
showStateText?: boolean;
/** Текст включённого состояния. Без него — `gr.switch.on` из локали. */
checkedText?: string;
/** Текст выключенного состояния. Без него — `gr.switch.off` из локали. */
uncheckedText?: string;
/**
* Дорожка растягивается под подпись, а ступень размера становится нижней
* границей ширины. Без `showStateText` смысла не имеет: тянуться не под что.
*/
autoWidth?: boolean;
}interface GrSwitchEmits {
/** Новое состояние для `v-model`. При `disabled`, `readonly` и `loading` не шлётся. */
(e: 'update:modelValue', value: boolean): void;
/** Пользователь переключил — тот же payload, что у `update:modelValue`. */
(e: 'change', value: boolean): void;
/** Фокус пришёл на переключатель — в том числе кликом по подписи `GrFormField`. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл с переключателя. */
(e: 'blur', event: FocusEvent): void;
}Файлdist/types/components/GrSwitch/index.d.ts
type GrSwitchInstance = ComponentExposed<typeof GrSwitchComponent>;Доступность
Паттерн APG
switch
Клавиши
Enter
| Клавиша | Действие |
|---|---|
| — | Space |
| Enter | переключить (нативная <button role="switch">) |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Нативная форма
<GrSwitch v-model="notifications" name="notifications" value="on">
Email-уведомления
</GrSwitch>Семантика чекбокса: включённый переключатель отправляет name=value,
выключенный не отправляется вовсе — сервер отличает «выкл» по отсутствию
ключа. Без name скрытого поля нет; form связывает переключатель с формой,
если он лежит вне неё.
Внутри <button> интерактивный контент невалиден, поэтому скрытое поле —
сосед кнопки, а корень компонента — фрагмент. class и прочие атрибуты
по-прежнему садятся на кнопку.
Загрузка
<GrSwitch :model-value="backup" :loading="syncing" @change="save" />loading показывает спиннер в бегунке, помечает контрол aria-busy и блокирует
переключение, пока запрос в полёте. Что именно грузится, задаёт loadingText
(по умолчанию — ключ gr.switch.loading): aria-busy сам по себе часть
скринридеров не объявляет.
Подпись
Подпись приходит слотом по умолчанию, labelPosition="start" переносит её влево
от дорожки. Ряд именно разворачивается, а не переставляется в DOM: диктор читает
порядок узлов, и дорожка должна оставаться первой.
Без подписи имя обязателен задать иначе — ariaLabel или GrFormField, который
свяжет <label for> с кнопкой.
Подпись состояния живёт не на всех ступенях
<GrSwitch v-model="notifications" show-state-text>
Уведомления
</GrSwitch>showStateText ставит «вкл/выкл» внутрь дорожки, на свободную от бегунка
сторону: состояние читается само по себе, без сравнения с соседним
переключателем. Тексты берутся из локали (gr.switch.on / gr.switch.off) и
перекрываются пропами checkedText / uncheckedText.
Подпись рисуется только на md и lg, на xs и sm её нет, а в dev-сборке
компонент объясняет почему. Дело в геометрии: рядом с бегунком остаётся
13 / 17 / 21 / 29px на ступенях xs…lg, а трёхбуквенное слово занимает около
19px даже на 9px кегля. Дорожка при этом не растягивается под текст — её ширина
фиксирована таблицей размеров, и от неё же считается ход бегунка.
Отсюда же и русский дефолт «ДА» / «НЕТ»: «ВЫКЛ» занимает 31px и не помещается ни на одной ступени. Собственный текст стоит мерить так же — длинный молча обрежется.
Для диктора подписи нет вовсе (aria-hidden): дорожка лежит внутри
<button role="switch">, и видимый текст вошёл бы в доступное имя — «Wi-Fi ON»
вместо «Wi-Fi», хотя состояние уже объявлено через aria-checked.
Цвет подписи — роль -fg той заливки, на которой она лежит. Задали дорожке свой
цвет через activeBackgroundColor — задайте и цвет текста: контраст на
произвольной заливке библиотека не вычислит. Оба хука (цвет и кегль) — в
../tokens.md.
Знак на бегунке
<GrSwitch v-model="autosave">
<template #checked-icon>
<IconCheck class="h-full w-full" />
</template>
<template #unchecked-icon>
<IconX class="h-full w-full" />
</template>
Автосохранение
</GrSwitch>Слоты checked-icon и unchecked-icon кладут знак на сам бегунок. Размер
держит обёртка по ступени size, а содержимое тянется до неё (h-full w-full)
— тот же приём, что у GrBadge и GrChip, и по
той же причине: шкала знака обязана идти за переключателем, а не за разметкой
места вызова.
Слоты независимы: нужен знак только во включённом состоянии — второй просто не передаётся.
Знаку доступны все ступени, включая xs и sm, где подпись состояния в
дорожке не помещается: знак ужимается без потери узнаваемости, в отличие от
трёхбуквенного слова. Внутри бегунка ему остаётся 8 / 10 / 12 / 16px.
loading вытесняет знак. Спиннер и знак занимают один центр, и пока идёт
запрос, важнее «состояние меняется», чем «каким оно стало».
Знак декоративен: бегунок целиком aria-hidden, а состояние диктор берёт из
aria-checked. Смысла, которого нет больше нигде, сюда класть нельзя.
Цвет знака и спиннера — общий, из --gr-switch-thumb-fg. По состоянию он не
ветвится и в disabled не тускнеет: гасится дорожка, а бегунок остаётся
--gr-card в любом состоянии, и содержимое, меняющее цвет, разошлось бы со
своей же подложкой.
Дорожка растягивается только по просьбе
<GrSwitch v-model="access" show-state-text auto-width checked-text="Включено" unchecked-text="Отключено">
Доступ по ссылке
</GrSwitch>autoWidth снимает ограничение по длине: ступень размера становится нижней
границей ширины, а дальше дорожка растёт под подпись. Высота при этом не
меняется — тянется только строка.
Дефолт остаётся фиксированным намеренно. Ряд переключателей с разными подписями становится рваным, а тот, кто сверстал колонку под ширину ступени, получит перекос — это выбор потребителя, а не поведение по умолчанию.
Ширина не дёргается при переключении, хотя «вкл» и «выкл» почти никогда не равны по ширине: в ту же клетку кладётся невидимый дубль противоположного текста, и ширину задаёт максимум из двух.
Бегунок в обоих режимах ходит без единого числа, зависящего от ширины: пара
left: calc(100% − зазор) и translateX(-100%) считает проценты от разных
элементов — первый от дорожки, второй от самого бегунка. Поэтому растягивание не
требует ни измерений, ни ResizeObserver, а ход остаётся линейным: обе величины
анимируются вместе.
Состояния
| Проп | Что делает |
|---|---|
disabled | гасит контрол токенами --gr-disabled-bg / -brd / -fg; они перебивают и кастомные цвета дорожки |
readonly | состояние видно, но не меняется (aria-readonly) |
invalid, required | aria-invalid / aria-required; складываются с контекстом GrFormField |
Недоступность гасится фоном, а не opacity: прозрачность разбавляет
выверенные на AA токены текста и роняет контраст подписи.
События и клавиатура
update:modelValue и change эмитятся вместе. Экземпляр отдаёт focus() и
blur().
Клавиатура нативная: контрол — настоящая <button>, поэтому Space и Enter
переключают его силами браузера, а Tab обходит в общем порядке.
Оформление
size — xs…lg, читается из GrConfigProvider
(componentDefaults.GrSwitch.size). Цвет дорожки точечно задаётся пропами
activeBackgroundColor / inactiveBackgroundColor.
Пропы этого примера