GrSteps

  • ядро
  • Навигация
  • API · 9 props
  • ⌨ клавиши

Берут, когда оформление заказа или регистрация.

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

Обзор

Когда брать

  • оформление заказа или регистрация — несколько экранов с формой, между которыми надо ходить и видеть, сколько осталось;
  • импорт данных — загрузка, сопоставление колонок, предпросмотр, запуск: каждый этап зависит от предыдущего;
  • длинная форма, разбитая на этапы — когда одна страница на сорок полей пугает, а GrFormSection уже не спасает;
  • процесс с проверкой на каждом шаге — beforeLeave не пустит дальше, пока текущий шаг не сойдётся.

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

НужноБерите
Переключить панели одного экрана, порядок не важенGrTabs
Показать историю событий, а не пройти процессGrTimeline
Показать, где пользователь в иерархии страницGrBreadcrumbs
Долю выполненного числом, без этаповGrProgressBar / GrProgressCircle
Сгруппировать поля одной формы заголовкамиGrFormSection

Варианты все тона и виды — настоящим компонентом

variant

  • steps
  • compact

Размеры

  • xs
  • sm
  • md
  • lg

Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.

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

Сцена

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

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

Примеры 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

PropTypeпо умолчаниюОписание
variantGrStepsVariant | undefinedundefinedКомпактный вид для узкой колонки: подпись текущего шага и полоса прогресса.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedРазмер маркеров и кегль подписей. По умолчанию `md`; берётся из `GrConfigProvider`.
ariaLabelstring | undefinedundefinedИмя лендмарка `navigation`. Не задано — берётся из локали.
orientationGrStepsOrientation | undefinedundefinedНаправление ленты. По умолчанию `horizontal`; берётся из `GrConfigProvider`.
clickableboolean | undefinedundefinedПереход кликом по шагу. Выключен — лента становится только индикатором.
linearboolean | undefinedundefinedВперёд — не дальше первого непройденного. Назад свободно всегда: вернуться и поправить заполненное — обычный сценарий, а не обход правила.
beforeLeave((from: string, to: string) => boolean | Promise<boolean>) | undefinedundefinedГейт перехода. Вернул `false` — перехода нет. Сюда потребитель кладёт валидацию шага: `GrSteps` про `GrForm` ничего не знает и знать не должен, а форма уже отдаёт `validate()`/`validateField()`.
modelValueобязательныйv-modelstring—`value` текущего шага (`v-model`).
stepsобязательныйGrStep[]—Шаги мастера по порядку: `value`, подпись, описание, иконка, статус, `disabled`.

Слоты

SlotTypeОписание
step
{ step: GrStep; index: number; status: GrStepStatus; enterab{ step: GrStep; index: number; status: GrStepStatus; enterable: boolean; markerClass: string; stateText: string; }
Своя разметка пункта **целиком**: маркер, подпись и состояние словом. Корневой тег, `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` — переход состоялся.
isFirstbooleanПеред текущим нет доступных шагов — кнопка «Назад» не нужна.
isLastbooleanПосле текущего нет доступных шагов — пора показывать «Готово».

Типы

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

Импорт
import type { GrStepsConfigurableProps, GrStepsSize, GrStepsOrientation } from '@feugene/granularity/components/GrSteps'

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

grStepsConfigconst
const grStepsConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSteps">;

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

GrStepsConfigurablePropsinterface

Пропы GrSteps, настраиваемые глобально через componentDefaults. Оформление и правило перехода — то, что приложение задаёт один раз на все мастера; текущий шаг относится к экземпляру и сюда не входит.

interface GrStepsConfigurableProps {
    size: GrStepsSize;
    orientation: GrStepsOrientation;
    variant: GrStepsVariant;
    linear: boolean;
    clickable: boolean;
}

Файлdist/types/components/GrSteps/grStepsStyles.d.ts

GrStepsSizetype
type GrStepsSize = GrComponentSize;
GrStepsOrientationtype
type GrStepsOrientation = 'horizontal' | 'vertical';
GrStepsVarianttype
type GrStepsVariant = 'steps' | 'compact';

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

grStepsSafelistconst
const grStepsSafelist: string[];

Файлdist/types/components/GrSteps/stepsModel.d.ts

GrStepStatustype

Состояние шага. error не выводится из позиции — его ставит приложение, когда шаг пройден, но не прошёл проверку. Без него мастер не умеет сказать «на втором шаге остались ошибки», ради чего гейт перехода и заводится.

type GrStepStatus = 'complete' | 'current' | 'upcoming' | 'error';
GrSteptype
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

GrStepsPropsinterface

Индикатор шагов многошагового мастера. По семантике это **навигация**, а не составной виджет выбора: <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>;
GrStepsEmitsinterface
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

GrStepsInstancetype
type GrStepsInstance = ComponentExposed<typeof GrStepsComponent>;

Доступность

Паттерн APG

—

КлавишаДействие
—Своего кольца стрелок нет: это навигация, а не составной виджет выбора. Каждый доступный шаг — своя остановка Tab
—Enter/Space активируют, недоступный и выключенный из обхода выпадают. Тем же живут GrBreadcrumbs и GrBottomNav; кольцо здесь ходило бы в основном по недоступному

Использование из прозы библиотеки

Это навигация, а не вкладки

Корень — <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 ради нуля семантики.

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

ts
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») остаются за компонентом.

vue
<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", а не скроллер, из которого половина этапов не видна.