72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
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>
|
|
);
|
|
}
|