GrSegmented
Берут, когда переключение вида одного и того же.
Обзор
Когда брать
- переключение вида одного и того же — список или плитка, день или месяц, график или таблица;
- вариантов 2–5 и все видны — выбор без раскрытия панели и без лишнего клика;
- вариант — иконка — режимы отображения узнаются значком быстрее, чем словом;
- значение уходит в форму — скрытое поле с
nameподключает сегменты к нативной отправке.
Когда взять другое
| Нужно | Берите |
|---|---|
| Вариантов больше пяти | GrSelect |
| Это значение поля, а не режим | GrRadioGroup |
| Разделы с разным содержимым | GrTabs |
| Вариантов два и это «включено/выключено» | GrSwitch |
| Действия, а не выбор | GrButtonGroup |
Сегменты против вкладок: сегменты меняют вид одного содержимого, вкладки переключают разное содержимое. Спутать их легко, а пользователю разница видна сразу — от неё зависит, ждать ли смены всей области.
Варианты все тона и виды — настоящим компонентом
variant
pillsbutton
Размеры
xssmmdlg
Состояния
обычнаяdisabledreadonlyinvalid
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrSegmented
:options="options"
v-model="value"
ariaLabel="Calendar range"
/>Примеры 4 у каждого — живое демо и его код
Одна выручка — по периодам и в нужном виде
Сегменты меняют вид одного и того же содержимого, а не само содержимое — этим они и отличаются от вкладок. Один ряд группирует выручку по неделям, месяцам или кварталам, второй рисует её графиком или таблицей: у сегментов-иконок свой
ariaLabel,variant="button"поднимает индикатор над дорожкой, аchangeотдаёт опцию целиком — подпись над графиком называет её словами.RevenueBy month · Sep €204.6k, +16.1% €168kMay€172kJun€182kJul€176kAug€205kSepОчередь ревью со счётчиками
blockзанимает ширину панели, аitem-width="content"делит её по подписям: равные трети обрезали бы «Needs my review», пока «All» держит место, которое ему не нужно. Слот добавляет к подписи счётчик. Пока CI не ответил, «Failing checks» —loading: его не выбрать, стрелки его перешагивают, но он доступен —aria-busy, а неaria-disabled.billing-service · pull requests - Retry failed webhooks with backoff#1842 · minaChecking…
- Prorate seats added mid-cycle#1839 · danielChecking…
- Dunning emails in the customer locale#1828 · graceChecking…
Период оплаты в обычной форме
Рядом с полем ввода пилюля читается гостем из другой системы, поэтому
shape="box"берёт скругление полей, аblock— ширину колонки.nameотправляет выбор одним скрытым полем нативной<form>. Годовой договор — от пяти мест: правило связывает два поля, и вердиктinvalidставит страница. Валюта зафиксирована первым счётом:readonlyоставляет её на виду и в форме.Очереди поддержки в боковой колонке
orientation="vertical"собирает сегменты в колонку фильтров, аblockотдаёт им ширину боковой панели. Архива в этом тарифе нет: недоступную опцию стрелки перешагивают. Пустая очередь предлагает выход —focus()ставит клавиатуру на выбранный сегмент, а не на первый, и дальше работают стрелки.Assigned to me0 Nothing is assigned to youNew conversations land in «All open» first.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Ряд недоступен: сегменты не выбираются и не фокусируются. Включается и от `GrFormField`. |
variant | GrSegmentedVariant | undefined | undefined | Оформление: `button` добавляет тень дорожке и индикатору. По умолчанию `pills`. |
readonly | boolean | undefined | false | Только для чтения: выбор видно, но он не меняется. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Высота и кегль по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. |
ariaLabel | string | undefined | undefined | Доступное имя ряда. Внутри `GrFormField` не нужно: ряд именует подпись поля. |
name | string | undefined | undefined | Имя скрытого поля, которым выбранное значение уходит в нативную форму. |
shape | "box" | "pill" | undefined | undefined | Форма рамки. `pill` — дорожка-пилюля, как была всегда; `box` — то же скругление, что у полей ввода, чтобы сегменты не выглядели в форме гостем из другой системы. |
block | boolean | undefined | false | Растягивать сегмент на всю ширину контейнера. |
orientation | "horizontal" | "vertical" | undefined | "horizontal" | Направление ряда. Вертикаль — боковые фильтры; индикатор к ней готов по построению, он двумерный. |
itemWidth | "equal" | "content" | undefined | undefined | Как делится ширина в `block`-ряду: `equal` — поровну, как было всегда; `content` — по содержимому, свободное место делится поверх него. `equal` остаётся умолчанием: с `content` ширина ряда зависела бы от длины подписей, и перевод интерфейса переставлял бы границы сегментов у всех. Без `block` и в вертикали проп не значит ничего — там ширина и так по содержимому, а колонка одна. |
indicatorDuration | number | undefined | 300 | Длительность анимации индикатора в мс. |
modelValueобязательныйv-model | GrSegmentedValue | — | Выбранное значение (`v-model`). Стрелки переносят выбор вместе с фокусом. |
optionsобязательный | GrSegmentedOption[] | — | Сегменты: значение, подпись, иконка, `disabled`, `loading`. Сегменту-иконке нужен `ariaLabel`. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default |
| Содержимое сегмента вместо подписи из `options`. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: GrSegmentedValue): void | — |
change | (event: "change", value: GrSegmentedValue, option: GrSegmentedOption): void | — |
focus | (event: "focus", event: FocusEvent): void | — |
blur | (event: "blur", event: FocusEvent): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
focus | () => void | Фокус на текущую остановку `Tab` ряда — обычно выбранный сегмент, а не первый. |
blur | () => void | Снять фокус с текущего сегмента ряда. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrSegmentedConfigurableProps, GrSegmentedVariant, GrSegmentedSize } from '@feugene/granularity/components/GrSegmented'Файлdist/types/components/GrSegmented/config.d.ts
const grSegmentedConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSegmented">;Файлdist/types/components/GrSegmented/defaults.d.ts
Пропы GrSegmented, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrSegmented: { … } }">.
Только оформление — см. GrButton/defaults.ts.
interface GrSegmentedConfigurableProps {
size: GrSegmentedSize;
variant: GrSegmentedVariant;
shape: GrControlShape;
/** Как ряд делит ширину: поровну или по содержимому. */
itemWidth: GrSegmentedItemWidth;
}Файлdist/types/components/GrSegmented/grSegmentedStyles.d.ts
type GrSegmentedVariant = 'pills' | 'button';type GrSegmentedSize = GrComponentSize;type GrSegmentedOrientation = typeof GR_SEGMENTED_ORIENTATIONS[number];type GrSegmentedItemWidth = typeof GR_SEGMENTED_ITEM_WIDTHS[number];type GrSegmentedValue = string | number;type GrSegmentedOption = {
value: GrSegmentedValue;
label?: string;
icon?: Component;
disabled?: boolean;
/**
* Сегмент занят: вместо иконки крутится спиннер, выбор не принимается.
* От `disabled` отличается смыслом — сегмент доступен, но сейчас работает.
*/
loading?: boolean;
/**
* Доступное имя сегмента. Обязательно для icon-only опций: иконка декоративна
* (`aria-hidden`), и без этого у сегмента нет имени вовсе — скринридер объявит
* пустую кнопку. Когда есть `label`, имя берётся из него, и поле не нужно.
*/
ariaLabel?: string;
};Файлdist/types/components/GrSegmented/GrSegmented.vue.d.ts
Пропсы публичного GR-примитива «Segmented».
interface GrSegmentedProps {
/** Выбранное значение (`v-model`). Стрелки переносят выбор вместе с фокусом. */
modelValue: GrSegmentedValue;
/** Сегменты: значение, подпись, иконка, `disabled`, `loading`. Сегменту-иконке нужен `ariaLabel`. */
options: GrSegmentedOption[];
/** Оформление: `button` добавляет тень дорожке и индикатору. По умолчанию `pills`. */
variant?: GrSegmentedVariant;
/** Высота и кегль по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. */
size?: GrSegmentedSize;
/**
* Форма рамки. `pill` — дорожка-пилюля, как была всегда; `box` — то же
* скругление, что у полей ввода, чтобы сегменты не выглядели в форме гостем
* из другой системы.
*/
shape?: GrControlShape;
/**
* Направление ряда. Вертикаль — боковые фильтры; индикатор к ней готов по
* построению, он двумерный.
*/
orientation?: GrSegmentedOrientation;
/**
* Как делится ширина в `block`-ряду: `equal` — поровну, как было всегда;
* `content` — по содержимому, свободное место делится поверх него.
*
* `equal` остаётся умолчанием: с `content` ширина ряда зависела бы от длины
* подписей, и перевод интерфейса переставлял бы границы сегментов у всех.
* Без `block` и в вертикали проп не значит ничего — там ширина и так по
* содержимому, а колонка одна.
*/
itemWidth?: GrSegmentedItemWidth;
/** Длительность анимации индикатора в мс. */
indicatorDuration?: number;
/** Растягивать сегмент на всю ширину контейнера. */
block?: boolean;
/** Ряд недоступен: сегменты не выбираются и не фокусируются. Включается и от `GrFormField`. */
disabled?: boolean;
/** Только для чтения: выбор видно, но он не меняется. */
readonly?: boolean;
/** Визуальное и ARIA-состояние ошибки. */
invalid?: boolean;
/** Обязательное поле (`aria-required`). */
required?: boolean;
/** Имя скрытого поля, которым выбранное значение уходит в нативную форму. */
name?: string;
/** Доступное имя ряда. Внутри `GrFormField` не нужно: ряд именует подпись поля. */
ariaLabel?: string;
}interface GrSegmentedEmits {
/** Новое значение (`v-model`): клик, стрелка, `Space` или `Enter` по другому сегменту. */
(e: 'update:modelValue', value: GrSegmentedValue): void;
/** Выбор сменился; вместе со значением приходит сама опция — с подписью и прочими полями. */
(e: 'change', value: GrSegmentedValue, option: GrSegmentedOption): void;
/** Фокус вошёл в ряд. Переходы между сегментами стрелками событием не считаются. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл из ряда целиком. */
(e: 'blur', event: FocusEvent): void;
}Файлdist/types/components/GrSegmented/safelist.d.ts
const grSegmentedSafelist: string[];Файлdist/types/components/GrSegmented/index.d.ts
type GrSegmentedInstance = ComponentExposed<typeof GrSegmentedComponent>;Доступность
Паттерн APG
radiogroup
Клавиши
← / → / ↑ / ↓Home / EndSpace / Enter
| Клавиша | Действие |
|---|---|
| ← / → / ↑ / ↓ | по сегментам (обе оси работают в любой orientation, как требует APG для radiogroup) |
| Home / End | к краям |
| Space / Enter | выбрать |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Опции и слот
<GrSegmented v-model="view" :options="options" aria-label="Вид списка" />Опция — { value, label?, icon?, disabled?, loading?, ariaLabel? }. Иконка
декоративна (aria-hidden), поэтому у icon-only сегмента обязателен
ariaLabel: иначе у него нет имени вовсе.
Дефолтный слот заменяет содержимое сегмента и получает
{ option, selected, disabled, loading }.
Занятый сегмент
{ value: 'review', label: 'Review', loading: syncing }loading показывает спиннер на месте иконки и помечает сегмент aria-busy.
Выбор он не принимает, а стрелки его перешагивают — как и disabled.
Разница с disabled смысловая, и она видна скринридеру: занятый сегмент
не получает aria-disabled и нативный disabled — он доступен, просто
сейчас работает. Недоступный получает и то, и другое.
Только чтение и недоступность
readonly показывает выбор, но не даёт его менять — ни кликом, ни клавиатурой;
группа объявляется aria-readonly. disabled гасит весь контрол.
Оба состояния гасятся токеном --gr-disabled-fg, а не прозрачностью: opacity
разбавляет выверенные на AA цвета текста.
invalid — свой или пришедший из ошибки GrFormField — красит рамку дорожки и
кольцо фокуса ролями ошибки (--gr-invalid-brd, --gr-invalid-ring), как у
полей ввода и GrRadioGroup, и ставит aria-invalid. Свой цвет рамки —
--gr-segmented-invalid-brd.
Нативная форма
Значение уходит одним скрытым полем рядом с сегментами (name), а не
вложенным в каждый role="radio": роль объявляет своих потомков
презентационными, и вложенный интерактивный контрол ломает виджет для
скринридеров. Недоступный выбранный сегмент значение не отправляет.
Кому достаётся свободное место
block растягивает ряд на контейнер, а itemWidth решает, как он делит ширину:
| Значение | Что делает |
|---|---|
equal (умолчание) | сегменты равны — правильно для «День / Неделя / Месяц» |
content | ширина от содержимого, свободное место делится поверх неё |
<GrSegmented v-model="filter" :options="filters" item-width="content" block />Равные треки в шторке на 400 px дают три ячейки по 121 px: слову «Все» столько
не нужно, а «С документами» не помещается и режется многоточием — при том, что
суммарно места достаточно. content раскладывает его туда, где оно нужно.
Умолчание не меняется, и это осознанно. С content ширина ряда зависит от
длины подписей, и перевод интерфейса переставлял бы границы сегментов у всех
потребителей разом.
Технически режим — это трек minmax(0, auto). Пока места хватает, сегменты
получают ширину своего содержимого, а остаток делится поверх поровну. Когда не
хватает, треки сжимаются: короткие подписи остаются целиком, длинные делят
остаток и уходят в многоточие с полным текстом в title. Шире контейнера ряд
не становится никогда.
Минимум трека — ноль, а не min-content: подпись однострочная, и у неё
min-content равен max-content — с таким минимумом трек не сжимался вовсе, и
ряд выталкивал за край карточку вокруг себя.
Без block и в вертикали проп не значит ничего: там ширина и так по
содержимому, а колонка одна.
Форма рамки
Дорожка — пилюля, и это дефолт: так сегменты выглядели всегда. Второе значение —
box, то же скругление, что у полей ввода: в форме рядом с GrSelect пилюля
читается гостем из другой системы.
<GrSegmented v-model="view" :options="options" shape="box" />Радиус сегмента внутри дорожки производный (calc(радиус − поле дорожки)),
поэтому вторая форма получается сама и отдельной настройки не требует.
В вертикали пилюля считается формулой, а не берёт 9999px: на высокой колонке
такой радиус превратил бы дорожку в эллипс, поэтому он равен высоте одного
сегмента. box этой поправки не знает — 6 px эллипсом не становятся.
Ось общая с полями ввода — ../sizes.md.
Оформление
| Проп | Что делает |
|---|---|
variant | pills (по умолчанию) или button — тень у дорожки и индикатора |
size | xs…lg, читается из GrConfigProvider |
orientation | horizontal (по умолчанию) или vertical |
block | сегменты растягиваются на всю ширину контейнера |
indicatorDuration | длительность анимации индикатора, мс |
Точечная кастомизация — через --gr-segmented-* (радиус, отступы, цвета
дорожки и индикатора, кегль).
Длинная подпись обрезается, но не пропадает
Подпись сегмента не переносится: ряд обязан оставаться рядом. Не поместившийся хвост прячется многоточием, и «Включено в подписку» превращается в «Включе…».
Полный текст при этом никуда не девается: truncate — правило отрисовки, в DOM
строка остаётся целой, и скринридер читает её целиком. Теряет её ровно один
читатель — тот, кто смотрит глазами, и ему подпись отдаётся нативной подсказкой
при наведении. Подсказка появляется только когда текст действительно обрезан:
тултип, дублирующий видимую целиком подпись, — шум, которого никто не просил.
Тот же обработчик доступен снаружи — titleWhenTruncated из пакета, для своей
разметки с truncate.
Вертикальный ряд
<GrSegmented v-model="scope" :options="filters" orientation="vertical" block />Типовой сценарий — боковые фильтры. Ряд разворачивается в колонку, корень
объявляет aria-orientation.
Содержимое сегмента в колонке — у начала, а не по центру: подписи и значки разной длины по центру вставали рваной линией, и колонка читалась меню, а не списком. В горизонтальном ряду сегмент по-прежнему центрирован — там он читается кнопкой. Островок от выравнивания не зависит: он меряет сам сегмент и накрывает его целиком.
Индикатору для этого ничего не понадобилось: он измеряется в двух измерениях
(translate3d плюс width/height) и едет вниз ровно так же, как вдоль ряда.
Смена ориентации на лету пересчитывает геометрию — иначе индикатор остался бы в
координатах прежней раскладки.
В вертикали колонка одна, поэтому сегменты одинаковой ширины по построению, а
block решает только, занимать ли ширину контейнера.
Радиус дорожки в вертикали считается от высоты сегмента, а не берётся пилюлей:
9999px выверен под короткий ряд и на высокой колонке превращал бы дорожку в
эллипс. Сегменты внутри при этом остаются пилюлями — они считают свой радиус от
того же значения.
Клавиатура
←/↑ и →/↓ двигают выбор с переносом через край, Home/End — к первому
и последнему доступному сегменту. Недоступные и занятые пропускаются.
Обе оси работают в любой ориентации — так требует APG для radiogroup:
вертикальный ряд не отключает горизонтальные стрелки и наоборот.
Пропы этого примера