Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions docs/components/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@
}
`" />

## 可访问性

Progress 默认渲染为 `role="progressbar"`,并根据 `percent` 同步 `aria-valuemin`、`aria-valuemax`、`aria-valuenow` 和 `aria-valuetext`。当页面中有多个进度条时,建议通过 `aria-label` 或 `aria-labelledby` 提供可区分名称。

## API

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| percent | 百分比 | `number` | - |
| status | 状态 | `'normal' \| 'success' \| 'exception'` | `'normal'` |
| showInfo | 显示百分比文字 | `boolean` | `true` |
| 属性 | 说明 | 类型 | 默认值 |
| --------------- | ------------------------------ | -------------------------------------- | ------------- |
| percent | 百分比 | `number` | - |
| status | 状态 | `'normal' \| 'success' \| 'exception'` | `'normal'` |
| showInfo | 显示百分比文字 | `boolean` | `true` |
| aria-label | 进度条可访问名称 | `string` | `'Progress'` |
| aria-labelledby | 使用外部标签命名进度条 | `string` | - |
| aria-valuetext | 自定义屏幕阅读器读取的进度文本 | `string` | `${percent}%` |
47 changes: 47 additions & 0 deletions packages/ui/src/components/data/Progress/Progress.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'

import { Progress } from './Progress'

describe('Progress', () => {
it('exposes determinate progressbar semantics from percent', () => {
render(<Progress percent={42} aria-label="Upload progress" />)

const progress = screen.getByRole('progressbar', { name: 'Upload progress' })

expect(progress).toHaveAttribute('aria-valuemin', '0')
expect(progress).toHaveAttribute('aria-valuemax', '100')
expect(progress).toHaveAttribute('aria-valuenow', '42')
expect(progress).toHaveAttribute('aria-valuetext', '42%')
})

it('keeps progressbar values in sync with the clamped visual percentage', () => {
render(<Progress percent={120} />)

const progress = screen.getByRole('progressbar', { name: 'Progress' })

expect(progress).toHaveAttribute('aria-valuenow', '100')
expect(progress).toHaveAttribute('aria-valuetext', '100%')
expect(screen.getByText('100%')).toBeInTheDocument()
})

it('supports external labelling and custom value text', () => {
render(
<div>
<span id="sync-progress-label">Sync progress</span>
<Progress
percent={64}
aria-labelledby="sync-progress-label"
aria-valuetext="64 percent complete"
showInfo={false}
/>
</div>,
)

const progress = screen.getByRole('progressbar', { name: 'Sync progress' })

expect(progress).toHaveAttribute('aria-valuenow', '64')
expect(progress).toHaveAttribute('aria-valuetext', '64 percent complete')
expect(screen.queryByText('64%')).not.toBeInTheDocument()
})
})
29 changes: 26 additions & 3 deletions packages/ui/src/components/data/Progress/Progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,34 @@ export interface ProgressProps extends HTMLAttributes<HTMLDivElement> {
}

export const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progress(
{ className, percent, status = 'normal', showInfo = true, ...props },
{
className,
percent,
status = 'normal',
showInfo = true,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'aria-valuetext': ariaValueText,
...props
},
ref,
) {
const safePercent = Math.max(0, Math.min(100, percent))
const accessibleLabel = ariaLabel ?? (ariaLabelledBy ? undefined : 'Progress')

return (
<div ref={ref} className={cn('w-full', className)} {...props}>
<div
ref={ref}
className={cn('w-full', className)}
{...props}
role="progressbar"
aria-label={accessibleLabel}
aria-labelledby={ariaLabelledBy}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={safePercent}
aria-valuetext={ariaValueText ?? `${safePercent}%`}
>
<div className="h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div
className={cn('h-full transition-all', {
Expand All @@ -25,7 +46,9 @@ export const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progr
style={{ width: `${safePercent}%` }}
/>
</div>
{showInfo ? <div className="mt-1 text-right text-xs text-slate-500">{safePercent}%</div> : null}
{showInfo ? (
<div className="mt-1 text-right text-xs text-slate-500">{safePercent}%</div>
) : null}
</div>
)
})
Loading