87 lines
2.4 KiB
TypeScript
87 lines
2.4 KiB
TypeScript
import React, { useEffect } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
import { useAnimatedVisibility } from './useAnimatedVisibility.js';
|
||
|
||
type CenteredModalProps = {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
title: React.ReactNode;
|
||
children: React.ReactNode;
|
||
footer?: React.ReactNode;
|
||
maxWidth?: number;
|
||
bodyStyle?: React.CSSProperties;
|
||
closeOnBackdrop?: boolean;
|
||
closeOnEscape?: boolean;
|
||
showCloseButton?: boolean;
|
||
};
|
||
|
||
export default function CenteredModal({
|
||
open,
|
||
onClose,
|
||
title,
|
||
children,
|
||
footer,
|
||
maxWidth = 860,
|
||
bodyStyle,
|
||
closeOnBackdrop = false,
|
||
closeOnEscape = false,
|
||
showCloseButton = true,
|
||
}: CenteredModalProps) {
|
||
const presence = useAnimatedVisibility(open, 220);
|
||
|
||
useEffect(() => {
|
||
if (!open || typeof document === 'undefined') return;
|
||
const previousOverflow = document.body.style.overflow;
|
||
document.body.style.overflow = 'hidden';
|
||
return () => {
|
||
document.body.style.overflow = previousOverflow;
|
||
};
|
||
}, [open]);
|
||
|
||
useEffect(() => {
|
||
if (!open || !closeOnEscape || typeof document === 'undefined') return;
|
||
const handleKeydown = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') onClose();
|
||
};
|
||
document.addEventListener('keydown', handleKeydown);
|
||
return () => {
|
||
document.removeEventListener('keydown', handleKeydown);
|
||
};
|
||
}, [closeOnEscape, open, onClose]);
|
||
|
||
if (!presence.shouldRender) return null;
|
||
|
||
const modal = (
|
||
<div
|
||
className={`modal-backdrop ${presence.isVisible ? '' : 'is-closing'}`.trim()}
|
||
onClick={closeOnBackdrop ? onClose : undefined}
|
||
>
|
||
<div
|
||
className={`modal-content ${presence.isVisible ? '' : 'is-closing'}`.trim()}
|
||
style={{ maxWidth }}
|
||
onClick={(event) => event.stopPropagation()}
|
||
>
|
||
<div className="modal-header">
|
||
<div className="modal-title">{title}</div>
|
||
{showCloseButton ? (
|
||
<button
|
||
type="button"
|
||
className="modal-close-button"
|
||
onClick={onClose}
|
||
aria-label="关闭弹框"
|
||
>
|
||
×
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
<div className="modal-body" style={bodyStyle}>
|
||
{children}
|
||
</div>
|
||
{footer ? <div className="modal-footer">{footer}</div> : null}
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
|
||
}
|