/** * Dialog — 令牌驱动的模态对话框(task 18.8,Req 19.1 / 19.5)。 * * 受控显隐(`open` + `onClose`)。可访问性(便于 task 22.x): * - 容器 `role="dialog"` 且 `aria-modal="true"`; * - `aria-labelledby` 关联标题; * - 打开时自动聚焦关闭按钮;按 `Escape` 触发关闭; * - 关闭按钮图标取自单一来源图标集(Req 19.5)。 * * 完整的焦点陷阱(focus trap)留待 task 22.x;此处提供合理的基础行为。 */ import { useEffect, useRef } from 'react'; import type { CSSProperties, ReactNode } from 'react'; import { colorVar, FONT_FAMILY, RADIUS, space, typographyStyle } from './styles.js'; import { Icon } from './Icon.js'; /** `Dialog` 组件属性。 */ export interface DialogProps { /** 是否显示。 */ readonly open: boolean; /** 请求关闭的回调(点击遮罩、关闭按钮或按 Escape 时触发)。 */ readonly onClose: () => void; /** 对话框标题。 */ readonly title: string; /** 对话框主体内容。 */ readonly children: ReactNode; /** 可选底部操作区。 */ readonly footer?: ReactNode; /** 关闭按钮的无障碍标签,默认「关闭」。 */ readonly closeLabel?: string; } /** * 模态对话框。显隐受控,具备基础键盘与焦点行为(Req 19.1)。 */ export function Dialog({ open, onClose, title, children, footer, closeLabel = '关闭', }: DialogProps): JSX.Element | null { const closeButtonRef = useRef(null); const titleId = useRef(`dialog-title-${Math.random().toString(36).slice(2)}`).current; // 打开时聚焦关闭按钮,提供合理的初始焦点。 useEffect(() => { if (open) { closeButtonRef.current?.focus(); } }, [open]); // Escape 关闭。 useEffect(() => { if (!open) { return; } const handler = (event: KeyboardEvent): void => { if (event.key === 'Escape') { onClose(); } }; document.addEventListener('keydown', handler); return () => { document.removeEventListener('keydown', handler); }; }, [open, onClose]); if (!open) { return null; } const overlayStyle: CSSProperties = { position: 'fixed', inset: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: `${space(4)}px`, zIndex: 1000, }; const dialogStyle: CSSProperties = { display: 'flex', flexDirection: 'column', width: '100%', maxWidth: '480px', maxHeight: '100%', backgroundColor: colorVar('color.bg.elevated'), border: `1px solid ${colorVar('color.border.default')}`, borderRadius: `${RADIUS.md}px`, fontFamily: FONT_FAMILY, color: colorVar('color.text.primary'), overflow: 'hidden', }; const headerStyle: CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: `${space(3)}px`, padding: `${space(3)}px ${space(4)}px`, borderBottom: `1px solid ${colorVar('color.border.default')}`, ...typographyStyle('title'), fontWeight: 700, }; const closeButtonStyle: CSSProperties = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: `${space(1)}px`, background: 'transparent', border: 'none', borderRadius: `${RADIUS.sm}px`, cursor: 'pointer', color: colorVar('color.text.secondary'), }; return (
{ if (event.target === event.currentTarget) { onClose(); } }} >

{title}

{children}
{footer !== undefined ? ( ) : null}
); }