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
14 changes: 10 additions & 4 deletions docs/components/toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@
<LivePlayground :code="`
() => {
return (
<Space>
<Toast open status='success' duration={0}>操作成功</Toast>
<Toast open status='error' duration={0}>操作失败</Toast>
</Space>
<div className='flex flex-col gap-3'>
<Toast open status='success' duration={0} className='static'>操作成功</Toast>
<Toast open status='error' duration={0} className='static'>操作失败</Toast>
</div>
)
}
`" />

## 可访问性

Toast 默认会把 `info`、`success`、`warning` 作为 `role="status"` 的礼貌通知呈现;`error` 默认作为 `role="alert"` 的紧急通知呈现。需要更细粒度控制时,可以显式传入 `role` 与 `aria-live`。

## API

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| open | 是否显示 | `boolean` | `false` |
| duration | 显示时长(ms),0 表示不自动关闭 | `number` | `3000` |
| status | 状态 | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` |
| role | live region 语义,默认 error 为 alert,其余状态为 status | `'status' \| 'alert'` | - |
| aria-live | live region 播报优先级,传入后覆盖默认值 | `'off' \| 'polite' \| 'assertive'` | - |
| onClose | 关闭回调 | `() => void` | - |
24 changes: 24 additions & 0 deletions packages/ui/src/components/feedback/Toast/Toast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ describe('Toast', () => {
expect(toast).toHaveTextContent('Saved successfully')
})

it('renders error updates as alert regions', () => {
render(
<Toast open status="error">
Save failed
</Toast>,
)

const toast = screen.getByRole('alert')
expect(toast).not.toHaveAttribute('aria-live')
expect(toast).toHaveTextContent('Save failed')
})

it('allows callers to override live region urgency', () => {
render(
<Toast open status="warning" role="alert" aria-live="assertive">
Session expiring
</Toast>,
)

const toast = screen.getByRole('alert')
expect(toast).toHaveAttribute('aria-live', 'assertive')
expect(toast).toHaveTextContent('Session expiring')
})

it('uses a 3000ms default auto-close duration', () => {
vi.useFakeTimers()
const onClose = vi.fn()
Expand Down
26 changes: 22 additions & 4 deletions packages/ui/src/components/feedback/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import { forwardRef, type HTMLAttributes, useEffect } from 'react'
import { cn } from '../../../utils/cn'

type ToastStatus = 'info' | 'success' | 'warning' | 'error'
type ToastRole = 'status' | 'alert'

export interface ToastProps extends HTMLAttributes<HTMLDivElement> {
open?: boolean
duration?: number
onClose?: () => void
status?: 'info' | 'success' | 'warning' | 'error'
status?: ToastStatus
/** 轻提示的 live region 语义,默认 error 为 alert,其余状态为 status */
role?: ToastRole
}

export const Toast = forwardRef<HTMLDivElement, ToastProps>(function Toast(
{ className, open = false, duration = 3000, onClose, status = 'info', children, ...props },
{
className,
open = false,
duration = 3000,
onClose,
status = 'info',
role,
'aria-live': ariaLive,
children,
...props
},
ref,
) {
const toastRole = role ?? (status === 'error' ? 'alert' : 'status')
const toastAriaLive = ariaLive ?? (toastRole === 'status' ? 'polite' : undefined)

useEffect(() => {
if (!open || duration <= 0) {
return
Expand All @@ -29,8 +47,8 @@ export const Toast = forwardRef<HTMLDivElement, ToastProps>(function Toast(
return (
<div
ref={ref}
role="status"
aria-live="polite"
role={toastRole}
aria-live={toastAriaLive}
className={cn(
'fixed bottom-4 right-4 z-50 min-w-64 rounded-lg border px-4 py-3 text-sm shadow-lg',
{
Expand Down
Loading