GrSteps
Берут, когда оформление заказа или регистрация.
Обзор
Когда брать
- оформление заказа или регистрация — несколько экранов с формой, между которыми надо ходить и видеть, сколько осталось;
- импорт данных — загрузка, сопоставление колонок, предпросмотр, запуск: каждый этап зависит от предыдущего;
- длинная форма, разбитая на этапы — когда одна страница на сорок полей
пугает, а
GrFormSectionуже не спасает; - процесс с проверкой на каждом шаге —
beforeLeaveне пустит дальше, пока текущий шаг не сойдётся.
Когда взять другое
| Нужно | Берите |
|---|---|
| Переключить панели одного экрана, порядок не важен | GrTabs |
| Показать историю событий, а не пройти процесс | GrTimeline |
| Показать, где пользователь в иерархии страниц | GrBreadcrumbs |
| Долю выполненного числом, без этапов | GrProgressBar / GrProgressCircle |
| Сгруппировать поля одной формы заголовками | GrFormSection |
Варианты все тона и виды — настоящим компонентом
variant
stepscompact
Размеры
xssmmdlg
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrSteps :steps="steps" v-model="value" />Примеры 3 у каждого — живое демо и его код
Оформление заказа, которое проверяет адрес
Шаги — навигация: пройденные — кнопки, текущий —
aria-current="step", будущие ждут, иlinearдержит путь вперёд закрытым. Мастер ничего не знает о формах: можно ли уйти с шага доставки, решает страница вbefore-leave, а назад можно всегда. Кнопки живут в подвале страницы и ведут мастер черезnext(),back(),isFirstиisLast.Импорт с шагом, который не сошёлся
В колонке есть место для строки под каждым шагом.
status: 'error'ставит страница — ошибка живёт в сопоставлении колонок, а не в порядке шагов, — и несошедшийся шаг не пускаетlinearдальше. Ночного расписания нет в этом тарифе, оноdisabled, аgoTo()возвращает прямо к колонкам. Слотlabelставит рядом с подписью оценку времени, а маркер и скрытое состояние оставляет компоненту.Check a previewTwo columns are still not matched, so the import cannot run yet.Семь шагов на телефоне
Семь шагов не помещаются в ряд на телефоне, а прокрутка спрятала бы половину.
variant="compact"показывает подпись текущего шага, счётчик и полосу; полоса декоративна — то же уже сказано текстом и скрытым живым регионом.Where customers pay; IBAN and the account holder.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
variant | GrStepsVariant | undefined | undefined | Компактный вид для узкой колонки: подпись текущего шага и полоса прогресса. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер маркеров и кегль подписей. По умолчанию `md`; берётся из `GrConfigProvider`. |
ariaLabel | string | undefined | undefined | Имя лендмарка `navigation`. Не задано — берётся из локали. |
orientation | GrStepsOrientation | undefined | undefined | Направление ленты. По умолчанию `horizontal`; берётся из `GrConfigProvider`. |
clickable | boolean | undefined | undefined | Переход кликом по шагу. Выключен — лента становится только индикатором. |
linear | boolean | undefined | undefined | Вперёд — не дальше первого непройденного. Назад свободно всегда: вернуться и поправить заполненное — обычный сценарий, а не обход правила. |
beforeLeave | ((from: string, to: string) => boolean | Promise<boolean>) | undefined | undefined | Гейт перехода. Вернул `false` — перехода нет. Сюда потребитель кладёт валидацию шага: `GrSteps` про `GrForm` ничего не знает и знать не должен, а форма уже отдаёт `validate()`/`validateField()`. |
modelValueобязательныйv-model | string | — | `value` текущего шага (`v-model`). |
stepsобязательный | GrStep[] | — | Шаги мастера по порядку: `value`, подпись, описание, иконка, статус, `disabled`. |
Слоты
| Slot | Type | Описание |
|---|---|---|
step |
| Своя разметка пункта **целиком**: маркер, подпись и состояние словом. Корневой тег, `aria-current` и клик остаются за компонентом. Слот обязан сам вывести `stateText` (визуально скрытым): иначе пройденный шаг и шаг с ошибкой звучат одинаково — цвет маркера диктору недоступен. Класс маркера по статусу — `markerClass`. Нужно только дополнить подпись — бери `#label`. |
label | { step: GrStep; index: number; status: GrStepStatus; } | Только текстовый блок шага: подпись, описание, своё рядом с ними — оценка времени, бейдж. Маркер и визуально скрытое состояние остаются за компонентом. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: string): void | — |
change | (event: "change", value: string): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
next | () => Promise<boolean> | Перейти на следующий шаг, минуя выключенные; спрашивает `beforeLeave`. `true` — переход состоялся. |
back | () => Promise<boolean> | Вернуться на предыдущий шаг, минуя выключенные; спрашивает `beforeLeave`. `true` — переход состоялся. |
goTo | (value: string) => Promise<boolean> | Перейти на шаг по `value`. Ограничение `linear` не действует, `beforeLeave` — действует; `true` — переход состоялся. |
isFirst | boolean | Перед текущим нет доступных шагов — кнопка «Назад» не нужна. |
isLast | boolean | После текущего нет доступных шагов — пора показывать «Готово». |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrStepsConfigurableProps, GrStepsSize, GrStepsOrientation } from '@feugene/granularity/components/GrSteps'Файлdist/types/components/GrSteps/config.d.ts
const grStepsConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSteps">;Файлdist/types/components/GrSteps/defaults.d.ts
Пропы GrSteps, настраиваемые глобально через componentDefaults.
Оформление и правило перехода — то, что приложение задаёт один раз на все
мастера; текущий шаг относится к экземпляру и сюда не входит.
interface GrStepsConfigurableProps {
size: GrStepsSize;
orientation: GrStepsOrientation;
variant: GrStepsVariant;
linear: boolean;
clickable: boolean;
}Файлdist/types/components/GrSteps/grStepsStyles.d.ts
type GrStepsSize = GrComponentSize;type GrStepsOrientation = 'horizontal' | 'vertical';type GrStepsVariant = 'steps' | 'compact';Файлdist/types/components/GrSteps/safelist.d.ts
const grStepsSafelist: string[];Файлdist/types/components/GrSteps/stepsModel.d.ts
Состояние шага.
error не выводится из позиции — его ставит приложение, когда шаг пройден,
но не прошёл проверку. Без него мастер не умеет сказать «на втором шаге
остались ошибки», ради чего гейт перехода и заводится.
type GrStepStatus = 'complete' | 'current' | 'upcoming' | 'error';type GrStep = {
value: string;
label: string;
/** Вторая строка под подписью: что от пользователя ждут на этом шаге. */
description?: string;
/**
* Иконка вместо номера — декоративна. Vue-компонент либо класс иконки
* (`i-lucide-*` — правило для класса заводит приложение: фабрике движка или
* провайдером, см. `docs/installation.md#иконки`).
*/
icon?: string | Component;
/** Явный статус. Не задан — выводится из позиции относительно текущего шага. */
status?: GrStepStatus;
disabled?: boolean;
};Файлdist/types/components/GrSteps/GrSteps.vue.d.ts
Индикатор шагов многошагового мастера.
По семантике это **навигация**, а не составной виджет выбора: <nav> со
списком, где пройденный шаг — кнопка со своей остановкой Tab, текущий —
aria-current="step", будущий — вне таб-порядка. Той же формы держатся
GrBreadcrumbs и GrBottomNav; role="tab" здесь неприменим — tablist без
tabpanel это сломанный паттерн, а панель шага никакой роли не несёт.
Контент шага компонент не рисует: его ставит потребитель обычным v-if.
interface GrStepsProps {
/** `value` текущего шага (`v-model`). */
modelValue: string;
/** Шаги мастера по порядку: `value`, подпись, описание, иконка, статус, `disabled`. */
steps: GrStep[];
/** Направление ленты. По умолчанию `horizontal`; берётся из `GrConfigProvider`. */
orientation?: GrStepsOrientation;
/** Размер маркеров и кегль подписей. По умолчанию `md`; берётся из `GrConfigProvider`. */
size?: GrStepsSize;
/** Компактный вид для узкой колонки: подпись текущего шага и полоса прогресса. */
variant?: GrStepsVariant;
/**
* Вперёд — не дальше первого непройденного. Назад свободно всегда: вернуться
* и поправить заполненное — обычный сценарий, а не обход правила.
*/
linear?: boolean;
/** Переход кликом по шагу. Выключен — лента становится только индикатором. */
clickable?: boolean;
/** Имя лендмарка `navigation`. Не задано — берётся из локали. */
ariaLabel?: string;
/**
* Гейт перехода. Вернул `false` — перехода нет.
*
* Сюда потребитель кладёт валидацию шага: `GrSteps` про `GrForm` ничего не
* знает и знать не должен, а форма уже отдаёт `validate()`/`validateField()`.
*/
beforeLeave?: (from: string, to: string) => boolean | Promise<boolean>;interface GrStepsEmits {
/** Перешли на другой шаг (`v-model`) — кликом или методом, после одобрения `beforeLeave`. */
(e: 'update:modelValue', value: string): void;
/** Шаг сменился — тот же payload, что у `update:modelValue`. */
(e: 'change', value: string): void;
}Файлdist/types/components/GrSteps/index.d.ts
type GrStepsInstance = ComponentExposed<typeof GrStepsComponent>;Доступность
Паттерн APG
—
| Клавиша | Действие |
|---|---|
| — | Своего кольца стрелок нет: это навигация, а не составной виджет выбора. Каждый доступный шаг — своя остановка Tab |
| — | Enter/Space активируют, недоступный и выключенный из обхода выпадают. Тем же живут GrBreadcrumbs и GrBottomNav; кольцо здесь ходило бы в основном по недоступному |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Это навигация, а не вкладки
Корень — <nav> со списком <ol>, и ролей на пунктах нет вовсе. role="tab"
здесь неприменим: tablist без tabpanel — сломанный паттерн, а панель шага
никакой роли не несёт, это обычная разметка приложения.
Отсюда и клавиатура: каждый доступный шаг — своя остановка Tab, стрелки не
задействованы. Правило «составной виджет — одна остановка» относится к виджетам,
выбирающим значение (GrTabs, GrSegmented); шаги ближе к
GrBreadcrumbs и GrBottomNav, где
каждый пункт табируется отдельно. Практическое соображение то же: будущие шаги
недоступны, и кольцо стрелок ходило бы в основном по выключенному.
Три состояния дают три разные разметки:
| Шаг | Тег | Метка |
|---|---|---|
| пройденный, доступный | <button> | — |
| текущий | <span> | aria-current="step" |
| будущий или выключенный | <span> вне таб-порядка | aria-disabled у выключенного |
Контент шага компонент не рисует
Панели у мастера нет и не будет. У вкладок она оправдана role="tabpanel" и
связкой aria-labelledby, а здесь содержимое шага — обычный v-if в разметке
потребителя, и отдельный компонент добавил бы вторую сущность с синхронизируемым
idBase ради нуля семантики.
<GrSteps v-model="step" :steps="steps" />
<GrForm v-if="step === 'delivery'" :model="form" :rules="rules">
…
</GrForm>
<GrForm v-else-if="step === 'payment'" :model="form" :rules="rules">
…
</GrForm>Валидация шага — через beforeLeave, а не через знание о форме
GrSteps про GrForm ничего не знает: он не читает её контекст
и не лезет в её поля. Проверку ставит приложение — в гейт, который блокирует
переход:
async function validateStep(from: string, to: string): Promise<boolean> {
// Назад пускаем всегда: правка заполненного не должна упираться в валидацию.
if (stepIndex(to) < stepIndex(from))
return true
return formRef.value!.validate()
}Через гейт идут клик по шагу, next(), back() и goTo() — всё, что
инициирует сам компонент. Прямая смена v-model снаружи через него не идёт:
это уже решение приложения, и перехватывать его было бы враньём.
Статус error ставится снаружи
Три статуса выводятся из позиции: до текущего — complete, текущий — current,
после — upcoming. Четвёртый, error, вывести неоткуда: ошибка живёт в форме, а
не в порядке шагов. Ставьте его сами, когда шаг пройден, но не сошёлся, — иначе
мастер не умеет сказать «на втором шаге остались ошибки».
Шаг с ошибкой не считается краем пройденного: при linear дальше него не пустит.
linear ограничивает только движение вперёд
Назад можно всегда — вернуться и поправить заполненное это правка, а не обход
правила. Вперёд — не дальше первого непройденного, но уже пройденные шаги
впереди остаются достижимыми: пометьте их status: 'complete', и пользователь
сможет прыгнуть обратно на тот шаг, откуда вернулся, не проходя всё заново.
Компактный вариант для узкой колонки
Семь шагов не помещаются в боковую панель и не помещаются на телефон.
variant="compact" показывает подпись текущего шага, счётчик и полосу вместо
ленты. Полоса декоративна (aria-hidden): то же самое уже сказано текстом рядом
и скрытым живым регионом, а второй progressbar в дереве заставил бы диктора
прочитать прогресс дважды.
Свой текст рядом с подписью — #label
#label заполняет только текстовый блок шага: подпись, описание и своё рядом
с ними — оценку времени, бейдж. Маркер (номер, галочка, иконка) и визуально
скрытое состояние словом («— has errors», «— complete») остаются за
компонентом.
<template>
<GrSteps v-model="step" :steps="steps" orientation="vertical">
<template #label="{ step: item }">
<span>{{ item.label }}</span>
<small class="text-[var(--gr-muted-fg)]">{{ item.estimate }}</small>
</template>
</GrSteps>
</template>#step заменяет пункт целиком — и маркер, и состояние. Тогда их выводит
слот: класс маркера по статусу приходит пропом markerClass, состояние словом
— stateText, и его обязательно печатать визуально скрытым. Без него
пройденный шаг и шаг с ошибкой звучат одинаково: цвет маркера диктору
недоступен.
Границы
- своих кнопок «Назад» и «Далее» у компонента нет. Их ставит приложение из
GrButtonи зовётnext()/back()через ref: у мастера они живут в подвале, вместе с «Отменить» и «Сохранить черновик», и рисовать их внутри индикатора значило бы диктовать раскладку страницы; - компонент не хранит данные шагов — модель формы целиком ваша;
- горизонтальная лента не прокручивается. Много шагов в узком месте — это
variant="compact", а не скроллер, из которого половина этапов не видна.
Пропы этого примера