// app/components/ui/Modal.tsx import React, { useEffect, useRef } from 'react'; import ReactDOM from 'react-dom'; import modalStyles from '~/styles/components/modal.css?url'; // 导出样式 export function links() { return [{ rel: 'stylesheet', href: modalStyles }]; } // 模态框尺寸 export type ModalSize = 'small' | 'medium' | 'large' | 'full'; interface ModalProps { isOpen: boolean; onClose: () => void; title: React.ReactNode; children: React.ReactNode; footer?: React.ReactNode; width?: number | string; size?: ModalSize; className?: string; closeOnEsc?: boolean; closeOnBackdropClick?: boolean; } export function Modal({ isOpen, onClose, title, children, footer, width, size = 'medium', className = '', closeOnEsc = true, closeOnBackdropClick = true }: ModalProps) { // 引用模态框内容元素 const contentRef = useRef(null); // 保存之前的活动元素,以便关闭时恢复焦点 const previousActiveElement = useRef(null); // 自动聚焦第一个可聚焦元素并保存先前的焦点元素 useEffect(() => { if (isOpen) { previousActiveElement.current = document.activeElement as HTMLElement; if (contentRef.current) { const focusableElements = contentRef.current.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length > 0) { (focusableElements[0] as HTMLElement).focus(); } else { contentRef.current.focus(); } } // 当模态框打开时禁止背景滚动 document.body.style.overflow = 'hidden'; } else if (previousActiveElement.current) { // 当模态框关闭时,恢复之前的焦点 previousActiveElement.current.focus(); // 恢复背景滚动 document.body.style.overflow = ''; } // 清理函数 return () => { document.body.style.overflow = ''; }; }, [isOpen]); // 处理背景点击事件 const handleBackdropClick = () => { if (closeOnBackdropClick) { onClose(); } }; // 获取尺寸样式类 const sizeClass = width ? '' : `modal-${size}`; // 计算宽度样式 const widthStyle = width ? { width: typeof width === 'number' ? `${width}px` : width, maxWidth: typeof width === 'number' ? `${width}px` : width } : {}; // 使用useEffect添加键盘事件监听器 useEffect(() => { const handleEscKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && closeOnEsc) { onClose(); } }; document.addEventListener('keydown', handleEscKey); return () => { document.removeEventListener('keydown', handleEscKey); }; }, [closeOnEsc, onClose]); if (!isOpen) return null; const modalNode = ( ); return ReactDOM.createPortal(modalNode, document.body); }