57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { X } from 'lucide-react'
|
|
import styles from './Modal.module.css'
|
|
|
|
interface ModalProps {
|
|
open: boolean
|
|
title: string
|
|
children: React.ReactNode
|
|
onClose: () => void
|
|
wide?: boolean
|
|
/** Extra-wide dialog for dense forms (e.g. invoice editor). */
|
|
xl?: boolean
|
|
}
|
|
|
|
export function Modal({ open, title, children, onClose, wide, xl }: ModalProps) {
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose()
|
|
}
|
|
const prevOverflow = document.body.style.overflow
|
|
document.body.style.overflow = 'hidden'
|
|
document.addEventListener('keydown', onKey)
|
|
return () => {
|
|
document.body.style.overflow = prevOverflow
|
|
document.removeEventListener('keydown', onKey)
|
|
}
|
|
}, [open, onClose])
|
|
|
|
if (!open) return null
|
|
|
|
const sizeClass = xl ? styles.modalXl : wide ? styles.modalWide : ''
|
|
|
|
return createPortal(
|
|
<div
|
|
className={styles.overlay}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
onMouseDown={(e) => {
|
|
if (e.target === e.currentTarget) onClose()
|
|
}}
|
|
>
|
|
<div className={`${styles.modal} ${sizeClass}`} onMouseDown={(e) => e.stopPropagation()}>
|
|
<div className={styles.header}>
|
|
<h3 className={styles.title}>{title}</h3>
|
|
<button type="button" className={styles.closeBtn} onClick={onClose} aria-label="Close">
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
<div className={styles.body}>{children}</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
}
|