feat(stepper): implement multi-step wizard for appointment confirmation process
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import Stepper from './Stepper';
|
||||
|
||||
const STEPS = [
|
||||
{ key: 'cost', title: 'بیمه و هزینه' },
|
||||
{ key: 'payment', title: 'پرداخت' },
|
||||
{ key: 'review', title: 'تأیید' },
|
||||
];
|
||||
|
||||
const items = () => screen.getAllByRole('listitem');
|
||||
|
||||
describe('Stepper', () => {
|
||||
it('همهٔ مراحل را با شمارهٔ خودشان نشان میدهد', () => {
|
||||
render(<Stepper steps={STEPS} current="payment" ariaLabel="مراحل" />);
|
||||
|
||||
expect(items()).toHaveLength(3);
|
||||
expect(items()[0].textContent).toBe('1بیمه و هزینه');
|
||||
expect(items()[2].textContent).toBe('3تأیید');
|
||||
});
|
||||
|
||||
it('مرحلهٔ جاری با aria-current اعلام میشود', () => {
|
||||
render(<Stepper steps={STEPS} current="payment" ariaLabel="مراحل" />);
|
||||
|
||||
// فقط یکی؛ وگرنه صفحهخوان دو «مرحلهٔ فعلی» میخواند.
|
||||
const marked = items().filter(li => li.getAttribute('aria-current') === 'step');
|
||||
expect(marked).toHaveLength(1);
|
||||
expect(marked[0].textContent).toBe('2پرداخت');
|
||||
});
|
||||
|
||||
it('نوار با برچسبِ دادهشده قابل پیدا شدن است', () => {
|
||||
render(<Stepper steps={STEPS} current="cost" ariaLabel="مراحل قطعی کردن نوبت" />);
|
||||
|
||||
expect(screen.getByRole('list', { name: 'مراحل قطعی کردن نوبت' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('مرحلهٔ ناشناخته هیچکدام را فعال نمیکند و خطا نمیدهد', () => {
|
||||
render(<Stepper steps={STEPS} current="unknown" ariaLabel="مراحل" />);
|
||||
|
||||
expect(items().filter(li => li.getAttribute('aria-current') === 'step')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
export interface Step {
|
||||
key: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* نوار مراحلِ یک فرمِ چندمرحلهای — «کجای کارم و چقدر مانده».
|
||||
*
|
||||
* فرمهای بلندِ مودال (ثبت نوبت، قطعی کردن نوبت) همهٔ تصمیمها را با هم نشان میدادند
|
||||
* و از ارتفاع صفحه بلندتر میشدند. حالا هر بار یک مرحله دیده میشود و این نوار تنها
|
||||
* چیزی است که کلِ مسیر را میگوید.
|
||||
*
|
||||
* فقط نمایش است: پیمایش با دکمههای فوتر انجام میشود، نه با کلیک روی مراحل — پرشِ
|
||||
* دلخواه یعنی رسیدن به مرحلهای که پیشنیازش پر نشده.
|
||||
*/
|
||||
export default function Stepper({
|
||||
steps, current, ariaLabel,
|
||||
}: { steps: Step[]; current: string; ariaLabel: string }) {
|
||||
const activeIdx = steps.findIndex(s => s.key === current);
|
||||
|
||||
return (
|
||||
<ol
|
||||
aria-label={ariaLabel}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, listStyle: 'none',
|
||||
margin: '0 0 18px', padding: 0,
|
||||
}}
|
||||
>
|
||||
{steps.map((step, idx) => {
|
||||
const done = idx < activeIdx;
|
||||
const active = idx === activeIdx;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={step.key}
|
||||
aria-current={active ? 'step' : undefined}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
flex: idx === steps.length - 1 ? '0 0 auto' : 1,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 999,
|
||||
flexShrink: 0, fontSize: 11.5, fontWeight: 700,
|
||||
background: active || done ? 'var(--primary)' : 'var(--surface-3)',
|
||||
color: active || done ? 'var(--on-primary)' : 'var(--text-3)',
|
||||
}}>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 12.5, whiteSpace: 'nowrap',
|
||||
fontWeight: active ? 700 : 500,
|
||||
color: active ? 'var(--text)' : 'var(--text-3)',
|
||||
}}>
|
||||
{step.title}
|
||||
</span>
|
||||
{idx < steps.length - 1 && (
|
||||
<span style={{
|
||||
flex: 1, height: 1, minWidth: 12,
|
||||
background: done ? 'var(--primary)' : 'var(--border)',
|
||||
}} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user