46 lines
1.8 KiB
TypeScript
46 lines
1.8 KiB
TypeScript
import { useEffect, useId, useRef, type ReactNode } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { X } from 'lucide-react';
|
|
|
|
interface ModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
closeDisabled?: boolean;
|
|
}
|
|
|
|
export const Modal = ({ isOpen, onClose, title, children, footer, closeDisabled = false }: ModalProps) => {
|
|
const ref = useRef<HTMLDialogElement>(null);
|
|
const titleId = useId();
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
const dialog = ref.current;
|
|
const previousFocus = document.activeElement as HTMLElement | null;
|
|
const overflow = document.body.style.overflow;
|
|
dialog?.showModal();
|
|
document.body.style.overflow = 'hidden';
|
|
return () => {
|
|
dialog?.close();
|
|
document.body.style.overflow = overflow;
|
|
if (previousFocus?.isConnected) previousFocus.focus();
|
|
};
|
|
}, [isOpen]);
|
|
|
|
return createPortal(
|
|
<dialog ref={ref} aria-labelledby={titleId} aria-modal="true" className="app-dialog"
|
|
onCancel={e => { e.preventDefault(); if (!closeDisabled) onClose(); }}
|
|
onClick={e => { if (e.target === e.currentTarget && !closeDisabled) onClose(); }}>
|
|
<div className="dialog-panel">
|
|
<div className="flex shrink-0 items-center justify-between gap-4 border-b border-[#e4e2e4] px-5 py-4">
|
|
<h2 id={titleId} className="text-xl font-semibold">{title}</h2>
|
|
<button type="button" onClick={onClose} disabled={closeDisabled} aria-label="Закрыть окно" className="icon-button"><X size={20} /></button>
|
|
</div>
|
|
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">{children}</div>
|
|
{footer && <div className="dialog-footer flex shrink-0 justify-end gap-3 border-t border-[#e4e2e4] bg-white px-5 py-4">{footer}</div>}
|
|
</div>
|
|
</dialog>, document.body,
|
|
);
|
|
};
|