164 lines
4.0 KiB
TypeScript
164 lines
4.0 KiB
TypeScript
import clsx from 'clsx';
|
|
import { Check } from 'lucide-react';
|
|
|
|
import { Icon } from '@@/Icon';
|
|
|
|
export interface StepData {
|
|
label: string;
|
|
}
|
|
|
|
interface Props {
|
|
step: StepData;
|
|
index: number;
|
|
currentStepIndex: number;
|
|
isLast: boolean;
|
|
onStepClick?: (stepIndex: number) => void;
|
|
}
|
|
|
|
type StepStateLabel = 'completed' | 'current' | 'upcoming';
|
|
|
|
interface StepState {
|
|
isActive: boolean;
|
|
isCompleted: boolean;
|
|
isClickable?: boolean;
|
|
}
|
|
|
|
export function Step({
|
|
step,
|
|
index,
|
|
currentStepIndex,
|
|
isLast,
|
|
onStepClick,
|
|
}: Props) {
|
|
const isActive = index === currentStepIndex;
|
|
const isCompleted = index < currentStepIndex;
|
|
const isClickable = !!onStepClick && isCompleted;
|
|
const displayNumber = index + 1;
|
|
const stepState = getStepState({ isActive, isCompleted });
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => isClickable && onStepClick?.(index)}
|
|
disabled={!isClickable}
|
|
className={getButtonClasses({
|
|
isActive,
|
|
isCompleted,
|
|
isClickable,
|
|
})}
|
|
aria-label={`Step ${displayNumber}: ${step.label}, ${stepState}`}
|
|
aria-current={isActive ? 'step' : undefined}
|
|
data-cy={`stepper-step-${index}`}
|
|
data-step-state={stepState}
|
|
>
|
|
<span className={getBadgeClasses({ isCompleted })} aria-hidden="true">
|
|
{isCompleted ? <Icon icon={Check} size="xs" /> : displayNumber}
|
|
</span>
|
|
<span className="whitespace-nowrap text-sm">{step.label}</span>
|
|
</button>
|
|
|
|
{!isLast && (
|
|
<div
|
|
className={getConnectorClasses({ isCompleted })}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getStepState({
|
|
isActive,
|
|
isCompleted,
|
|
}: Pick<StepState, 'isActive' | 'isCompleted'>): StepStateLabel {
|
|
if (isCompleted) {
|
|
return 'completed';
|
|
}
|
|
if (isActive) {
|
|
return 'current';
|
|
}
|
|
return 'upcoming';
|
|
}
|
|
|
|
function getButtonClasses({ isActive, isCompleted, isClickable }: StepState) {
|
|
return clsx(
|
|
'flex items-center gap-2 rounded-lg border border-solid px-3 py-2 transition-all text-inherit font-medium',
|
|
getButtonStateClasses({ isActive, isCompleted }),
|
|
isClickable
|
|
? clsx(
|
|
'cursor-pointer hover:border-blue-6 hover:bg-blue-3',
|
|
'th-dark:hover:border-blue-5 th-dark:hover:bg-blue-9/30',
|
|
'th-highcontrast:hover:border-blue-4 th-highcontrast:hover:bg-blue-9/40'
|
|
)
|
|
: 'cursor-default'
|
|
);
|
|
}
|
|
|
|
function getButtonStateClasses({
|
|
isActive,
|
|
isCompleted,
|
|
}: Omit<StepState, 'isClickable'>) {
|
|
if (isActive) {
|
|
return clsx(
|
|
'border-blue-6 bg-blue-2',
|
|
'th-dark:border-blue-7 th-dark:bg-blue-10',
|
|
'th-highcontrast:border-blue-7 th-highcontrast:bg-blue-10'
|
|
);
|
|
}
|
|
|
|
if (isCompleted) {
|
|
return clsx(
|
|
'border-graphite-700 bg-graphite-700/5',
|
|
'th-dark:border-gray-warm-5 th-dark:bg-gray-warm-9',
|
|
'th-highcontrast:border-gray-4 th-highcontrast:bg-gray-9'
|
|
);
|
|
}
|
|
|
|
return clsx(
|
|
'border-gray-5 bg-white',
|
|
'th-dark:border-gray-warm-7 th-dark:bg-gray-iron-10',
|
|
'th-highcontrast:border-gray-2 th-highcontrast:bg-black'
|
|
);
|
|
}
|
|
|
|
function getBadgeClasses({ isCompleted }: Pick<StepState, 'isCompleted'>) {
|
|
const base = 'flex h-6 w-6 items-center justify-center rounded-full text-xs';
|
|
|
|
if (isCompleted) {
|
|
return clsx(
|
|
base,
|
|
'bg-graphite-700 text-mist-100',
|
|
'th-dark:bg-gray-warm-5 th-dark:text-gray-iron-10',
|
|
'th-highcontrast:bg-gray-4 th-highcontrast:text-black'
|
|
);
|
|
}
|
|
|
|
return clsx(
|
|
base,
|
|
'bg-gray-4 text-gray-7',
|
|
'th-dark:bg-gray-warm-7 th-dark:text-white',
|
|
'th-highcontrast:bg-gray-8 th-highcontrast:text-white'
|
|
);
|
|
}
|
|
|
|
function getConnectorClasses({ isCompleted }: Pick<StepState, 'isCompleted'>) {
|
|
const base = 'h-0.5 w-8 transition-colors';
|
|
|
|
if (isCompleted) {
|
|
return clsx(
|
|
base,
|
|
'bg-graphite-700',
|
|
'th-dark:bg-gray-warm-5',
|
|
'th-highcontrast:bg-gray-4'
|
|
);
|
|
}
|
|
|
|
return clsx(
|
|
base,
|
|
'bg-gray-5',
|
|
'th-dark:bg-gray-warm-7',
|
|
'th-highcontrast:bg-gray-6'
|
|
);
|
|
}
|