diff --git a/docs/components/notification.md b/docs/components/notification.md
index e050a7a..abf5cb8 100644
--- a/docs/components/notification.md
+++ b/docs/components/notification.md
@@ -1,20 +1,26 @@
# Notification 通知
-通知提醒框。
+通知提醒框,用于展示不会打断当前流程的状态反馈,或在错误场景中提示需要立即关注的问题。
## 示例
+## 可访问性
+
+`Notification` 会根据类型选择 live region 语义:`error` 默认使用 `role="alert"`,适合需要立即关注的错误;其他类型默认使用 `role="status"` 与 `aria-live="polite"`,避免普通通知打断读屏器当前播报。
+
+如果业务确认某条通知需要更高或更低的播报优先级,可以显式传入 `role` 和 `aria-live` 覆盖默认值。
+
## API
| 属性 | 说明 | 类型 | 默认值 |
@@ -25,3 +31,5 @@
| open | 是否显示;未传时组件内部维护可见状态 | `boolean` | - |
| duration | 自动关闭时长(ms),0 表示不自动关闭 | `number` | `0` |
| onClose | 关闭回调 | `() => void` | - |
+| role | 通知的 live region 语义 | `'status' \| 'alert'` | `error` 为 `'alert'`,其余为 `'status'` |
+| aria-live | live region 播报优先级 | `'polite' \| 'assertive' \| 'off'` | `role='status'` 时为 `'polite'` |
diff --git a/packages/ui/src/components/feedback/Notification/Notification.test.tsx b/packages/ui/src/components/feedback/Notification/Notification.test.tsx
index 2cde4d0..072318e 100644
--- a/packages/ui/src/components/feedback/Notification/Notification.test.tsx
+++ b/packages/ui/src/components/feedback/Notification/Notification.test.tsx
@@ -10,15 +10,33 @@ afterEach(() => {
})
describe('Notification', () => {
- it('renders an alert region with title and description', () => {
+ it('renders non-error notifications as polite status regions', () => {
render()
- const notification = screen.getByRole('alert')
+ const notification = screen.getByRole('status')
+ expect(notification).toHaveAttribute('aria-live', 'polite')
expect(notification).toHaveTextContent('Published')
expect(notification).toHaveTextContent('The package is live.')
expect(screen.getByRole('button', { name: 'Close notification' })).toBeInTheDocument()
})
+ it('renders error notifications as alert regions', () => {
+ render()
+
+ const notification = screen.getByRole('alert')
+ expect(notification).not.toHaveAttribute('aria-live')
+ expect(notification).toHaveTextContent('Failed')
+ expect(notification).toHaveTextContent('Check the build logs.')
+ })
+
+ it('allows callers to override live region urgency', () => {
+ render()
+
+ const notification = screen.getByRole('alert')
+ expect(notification).toHaveAttribute('aria-live', 'assertive')
+ expect(notification).toHaveTextContent('Session expiring')
+ })
+
it('hides when the close button is clicked in uncontrolled mode', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
@@ -28,7 +46,7 @@ describe('Notification', () => {
await user.click(screen.getByRole('button', { name: 'Close notification' }))
expect(onClose).toHaveBeenCalledTimes(1)
- expect(screen.queryByRole('alert')).not.toBeInTheDocument()
+ expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
it('keeps controlled notifications open until open changes', async () => {
@@ -40,7 +58,7 @@ describe('Notification', () => {
await user.click(screen.getByRole('button', { name: 'Close notification' }))
expect(onClose).toHaveBeenCalledTimes(1)
- expect(screen.getByRole('alert')).toHaveTextContent('Controlled')
+ expect(screen.getByRole('status')).toHaveTextContent('Controlled')
})
it('auto-closes uncontrolled notifications after duration', () => {
@@ -52,14 +70,14 @@ describe('Notification', () => {
act(() => {
vi.advanceTimersByTime(4499)
})
- expect(screen.getByRole('alert')).toBeInTheDocument()
+ expect(screen.getByRole('status')).toBeInTheDocument()
expect(onClose).not.toHaveBeenCalled()
act(() => {
vi.advanceTimersByTime(1)
})
expect(onClose).toHaveBeenCalledTimes(1)
- expect(screen.queryByRole('alert')).not.toBeInTheDocument()
+ expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
it('does not auto-close when duration is 0', () => {
@@ -73,6 +91,6 @@ describe('Notification', () => {
})
expect(onClose).not.toHaveBeenCalled()
- expect(screen.getByRole('alert')).toHaveTextContent('Persistent')
+ expect(screen.getByRole('status')).toHaveTextContent('Persistent')
})
})
diff --git a/packages/ui/src/components/feedback/Notification/Notification.tsx b/packages/ui/src/components/feedback/Notification/Notification.tsx
index 6642586..ef3a190 100644
--- a/packages/ui/src/components/feedback/Notification/Notification.tsx
+++ b/packages/ui/src/components/feedback/Notification/Notification.tsx
@@ -1,13 +1,18 @@
import { forwardRef, useCallback, useEffect, useState, type HTMLAttributes } from 'react'
import { cn } from '../../../utils/cn'
+type NotificationType = 'info' | 'success' | 'warning' | 'error'
+type NotificationRole = 'status' | 'alert'
+
export interface NotificationProps extends HTMLAttributes {
- type?: 'info' | 'success' | 'warning' | 'error'
+ type?: NotificationType
title?: string
description?: string
open?: boolean
duration?: number
onClose?: () => void
+ /** 通知的 live region 语义,默认 error 为 alert,其余类型为 status */
+ role?: NotificationRole
}
export const Notification = forwardRef(function Notification(
@@ -19,12 +24,16 @@ export const Notification = forwardRef(functi
open,
duration = 0,
onClose,
+ role,
+ 'aria-live': ariaLive,
...props
},
ref,
) {
const [visible, setVisible] = useState(true)
const mergedOpen = open ?? visible
+ const notificationRole = role ?? (type === 'error' ? 'alert' : 'status')
+ const notificationAriaLive = ariaLive ?? (notificationRole === 'status' ? 'polite' : undefined)
const handleClose = useCallback(() => {
if (open === undefined) {
@@ -49,7 +58,8 @@ export const Notification = forwardRef(functi
return (