diff --git a/docs/components/steps.md b/docs/components/steps.md index b4e6be1..1320c6a 100644 --- a/docs/components/steps.md +++ b/docs/components/steps.md @@ -16,9 +16,21 @@ } `" /> +## 可访问性 + +组件渲染为有序列表,默认使用 `Steps` 作为列表名称。当前步骤会设置 `aria-current="step"`,完成、当前和待处理状态会提供给屏幕阅读器。 + ## API -| 属性 | 说明 | 类型 | 默认值 | -| --- | --- | --- | --- | -| items | 步骤项 | `StepItem[]` | - | -| current | 当前步骤 | `number` | `0` | +| 属性 | 说明 | 类型 | 默认值 | +| ------- | -------- | ------------ | ------ | +| items | 步骤项 | `StepItem[]` | - | +| current | 当前步骤 | `number` | `0` | + +## StepItem + +| 属性 | 说明 | 类型 | 默认值 | +| ----------- | ------------ | ----------- | ------ | +| key | 步骤唯一标识 | `string` | - | +| title | 步骤标题 | `ReactNode` | - | +| description | 步骤描述 | `ReactNode` | - | diff --git a/packages/ui/src/components/navigation/Steps/Steps.test.tsx b/packages/ui/src/components/navigation/Steps/Steps.test.tsx new file mode 100644 index 0000000..703393e --- /dev/null +++ b/packages/ui/src/components/navigation/Steps/Steps.test.tsx @@ -0,0 +1,47 @@ +import { render, screen, within } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { Steps } from './Steps' + +const items = [ + { key: 'create', title: 'Create', description: 'Start the request' }, + { key: 'review', title: 'Review', description: 'Check details' }, + { key: 'done', title: 'Done', description: 'Complete' }, +] + +describe('Steps', () => { + it('renders a labelled ordered list of steps', () => { + render() + + const list = screen.getByRole('list', { name: 'Steps' }) + const steps = within(list).getAllByRole('listitem') + + expect(steps).toHaveLength(3) + expect(within(steps[0]).getByText('Create')).toBeInTheDocument() + expect(within(steps[1]).getByText('Review')).toBeInTheDocument() + }) + + it('marks the active item as the current step', () => { + render() + + const steps = screen.getAllByRole('listitem') + + expect(steps[0]).not.toHaveAttribute('aria-current') + expect(steps[1]).toHaveAttribute('aria-current', 'step') + expect(steps[2]).not.toHaveAttribute('aria-current') + }) + + it('announces completed, current, and upcoming status text', () => { + render() + + expect(screen.getByText('Completed step:', { selector: '.sr-only' })).toBeInTheDocument() + expect(screen.getByText('Current step:', { selector: '.sr-only' })).toBeInTheDocument() + expect(screen.getByText('Upcoming step:', { selector: '.sr-only' })).toBeInTheDocument() + }) + + it('allows a custom accessible list name', () => { + render() + + expect(screen.getByRole('list', { name: 'Checkout progress' })).toBeInTheDocument() + }) +}) diff --git a/packages/ui/src/components/navigation/Steps/Steps.tsx b/packages/ui/src/components/navigation/Steps/Steps.tsx index c4f54a7..1e219a8 100644 --- a/packages/ui/src/components/navigation/Steps/Steps.tsx +++ b/packages/ui/src/components/navigation/Steps/Steps.tsx @@ -13,26 +13,55 @@ export interface StepsProps extends HTMLAttributes { } export const Steps = forwardRef(function Steps( - { className, items, current = 0, ...props }, + { className, items, current = 0, 'aria-label': ariaLabel = 'Steps', ...props }, ref, ) { return ( -
    +
      {items.map((item, index) => { const isDone = index < current const isActive = index === current + const statusText = isActive ? 'Current step' : isDone ? 'Completed step' : 'Upcoming step' return ( -
    1. -
      +
    2. +
      -
      + {statusText}: +
      {item.title}
      - {item.description ?
      {item.description}
      : null} + {item.description ? ( +
      {item.description}
      + ) : null}
      - {index < items.length - 1 ? : null} + {index < items.length - 1 ? ( +
    3. ) })}