feat: Stepper (навигация по шагам, логика в stepperLogic) — 0.5.0
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import styles from './Stepper.module.scss';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { IStepperStep, highestReachableIndex } from './stepperLogic';
|
||||
|
||||
// Логику и тип держим в stepperLogic.ts (чистые, тестируемые), здесь — реэкспорт
|
||||
// для единой точки импорта компонента.
|
||||
export type { IStepperStep } from './stepperLogic';
|
||||
export { highestReachableIndex } from './stepperLogic';
|
||||
|
||||
export interface IStepperProps {
|
||||
steps: IStepperStep[];
|
||||
/** Текущий (активный) шаг. */
|
||||
currentId: string | number;
|
||||
/** Завершённые шаги (показываются галочкой). */
|
||||
completedIds: Array<string | number>;
|
||||
/**
|
||||
* Максимально достигнутый шаг. Кликабельны шаги ДО него включительно —
|
||||
* защита от «прыжка вперёд» на шаг, до которого ещё не дошли.
|
||||
*/
|
||||
highestId: string | number;
|
||||
onStepClick: (id: string | number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Степпер прогресса: кружки с номером/галочкой, подпись, коннекторы между.
|
||||
* Кликабельны только достигнутые шаги (см. highestId). Презентационный и
|
||||
* управляемый: состояние (current/completed/highest) приходит пропсами, клик —
|
||||
* наружу. Шапку формы (номер/статус) держать ОТДЕЛЬНО (FormHeader), не тут.
|
||||
*/
|
||||
export function Stepper({ steps, currentId, completedIds, highestId, onStepClick }: IStepperProps): JSX.Element {
|
||||
const highestIdx = highestReachableIndex(steps, highestId);
|
||||
|
||||
return (
|
||||
<nav className={styles.stepper} aria-label="Прогресс">
|
||||
{steps.map((s, index) => {
|
||||
const isCompleted = completedIds.indexOf(s.id) !== -1;
|
||||
const isCurrent = s.id === currentId;
|
||||
const isNavigable = index <= highestIdx;
|
||||
|
||||
const circleClass = [
|
||||
styles.circle,
|
||||
isCompleted ? styles.completed : '',
|
||||
isCurrent ? styles.current : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<React.Fragment key={s.id}>
|
||||
{index > 0 && <span className={styles.connector} aria-hidden="true" />}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.step}
|
||||
disabled={!isNavigable}
|
||||
aria-current={isCurrent ? 'step' : undefined}
|
||||
onClick={() => onStepClick(s.id)}
|
||||
>
|
||||
<span className={circleClass}>
|
||||
{isCompleted && !isCurrent ? <Icon name="check" size="sm" /> : index + 1}
|
||||
</span>
|
||||
<span className={[styles.label, isCurrent ? styles.labelCurrent : ''].filter(Boolean).join(' ')}>
|
||||
{s.label}
|
||||
</span>
|
||||
</button>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user