Skip to content
Merged
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
11 changes: 7 additions & 4 deletions docs/components/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
<LivePlayground :code="`
() => {
return (
<Space direction='vertical' size={8}>
<div className='flex flex-col gap-3'>
<Progress percent={30} />
<Progress percent={70} status='success' />
<Progress percent={50} status='exception' />
</Space>
<Progress percent={70} status='success' format={(value) => value === 100 ? '完成' : value + '%'} />
<Progress percent={50} status='exception' format={(value) => value + ' tasks'} aria-valuetext='50 tasks completed' />
</div>
)
}
`" />
Expand All @@ -20,13 +20,16 @@

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

`format` 会自定义可见进度文本;当 `format` 返回字符串或数字时,该内容也会作为默认 `aria-valuetext`。如果格式化结果是复杂节点,或需要让屏幕阅读器读取更完整的上下文,请显式传入 `aria-valuetext`。

## API

| 属性 | 说明 | 类型 | 默认值 |
| --------------- | ------------------------------ | -------------------------------------- | ------------- |
| percent | 百分比 | `number` | - |
| status | 状态 | `'normal' \| 'success' \| 'exception'` | `'normal'` |
| showInfo | 显示百分比文字 | `boolean` | `true` |
| format | 自定义进度文本 | `(percent: number) => ReactNode` | - |
| aria-label | 进度条可访问名称 | `string` | `'Progress'` |
| aria-labelledby | 使用外部标签命名进度条 | `string` | - |
| aria-valuetext | 自定义屏幕阅读器读取的进度文本 | `string` | `${percent}%` |
27 changes: 26 additions & 1 deletion packages/ui/src/components/data/Progress/Progress.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ describe('Progress', () => {
expect(screen.getByText('100%')).toBeInTheDocument()
})

it('uses custom formatted text for visible and accessible value text', () => {
render(<Progress percent={75} format={(value) => `${value} days`} />)

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

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

it('keeps explicit value text when custom formatted content is visual only', () => {
render(
<Progress
percent={75}
format={(value) => <span>{value} days</span>}
aria-valuetext="75 days remaining"
/>,
)

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

expect(progress).toHaveAttribute('aria-valuetext', '75 days remaining')
expect(screen.getByText('75 days')).toBeInTheDocument()
})

it('supports external labelling and custom value text', () => {
render(
<div>
Expand All @@ -44,4 +69,4 @@ describe('Progress', () => {
expect(progress).toHaveAttribute('aria-valuetext', '64 percent complete')
expect(screen.queryByText('64%')).not.toBeInTheDocument()
})
})
})
15 changes: 9 additions & 6 deletions packages/ui/src/components/data/Progress/Progress.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { forwardRef, type HTMLAttributes } from 'react'
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
import { cn } from '../../../utils/cn'

export interface ProgressProps extends HTMLAttributes<HTMLDivElement> {
percent: number
status?: 'normal' | 'success' | 'exception'
showInfo?: boolean
format?: (percent: number) => ReactNode
}

export const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progress(
Expand All @@ -13,6 +14,7 @@ export const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progr
percent,
status = 'normal',
showInfo = true,
format,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'aria-valuetext': ariaValueText,
Expand All @@ -22,6 +24,9 @@ export const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progr
) {
const safePercent = Math.max(0, Math.min(100, percent))
const accessibleLabel = ariaLabel ?? (ariaLabelledBy ? undefined : 'Progress')
const info = format ? format(safePercent) : `${safePercent}%`
const formattedValueText =
typeof info === 'string' || typeof info === 'number' ? String(info) : `${safePercent}%`

return (
<div
Expand All @@ -34,7 +39,7 @@ export const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progr
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={safePercent}
aria-valuetext={ariaValueText ?? `${safePercent}%`}
aria-valuetext={ariaValueText ?? formattedValueText}
>
<div className="h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div
Expand All @@ -46,9 +51,7 @@ 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">{info}</div> : null}
</div>
)
})
})
Loading