import { useEffect, useRef } from 'react'
import type { ReactNode } from 'react'
import { Button } from './Button'
interface ModalProps {
children: ReactNode
isOpen: boolean
onClose: () => void
title?: string
size?: 'small' | 'medium' | 'large' | 'full'
showCloseButton?: boolean
'aria-label'?: string
}
function getSizeClasses(size: ModalProps['size']): string {
switch (size) {
case 'small':
return 'max-w-md'
case 'medium':
return 'max-w-lg'
case 'large':
return 'max-w-xl'
case 'full':
return 'max-w-full mx-4'
default:
return 'max-w-md'
}
}
function CloseButton({ onClose }: { onClose: () => void }): React.ReactElement {
return (
)
}
function ModalHeader({
title,
showCloseButton,
onClose,
}: {
title?: string | undefined
showCloseButton: boolean
onClose: () => void
}): React.ReactElement | null {
if (!title && !showCloseButton) {
return null
}
return (
{title &&
{title}
}
{showCloseButton && }
)
}
function useModalFocus(modalRef: React.RefObject, isOpen: boolean): void {
const previousFocusRef = useRef(null)
useEffect(() => {
if (isOpen) {
const { activeElement } = document
if (activeElement instanceof HTMLElement) {
previousFocusRef.current = activeElement
}
const firstFocusable = modalRef.current?.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
if (firstFocusable instanceof HTMLElement) {
firstFocusable.focus()
}
} else {
previousFocusRef.current?.focus()
}
}, [isOpen, modalRef])
}
function useModalKeyboard(isOpen: boolean, onClose: () => void): void {
useEffect(() => {
const handleEscape = (e: KeyboardEvent): void => {
if (e.key === 'Escape' && isOpen) {
onClose()
}
}
if (isOpen) {
document.addEventListener('keydown', handleEscape)
document.body.style.overflow = 'hidden'
}
return () => {
document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = ''
}
}, [isOpen, onClose])
}
function ModalOverlay({
onClose,
ariaLabel,
children,
}: {
onClose: () => void
ariaLabel: string | undefined
children: ReactNode
}): React.ReactElement {
return (
{
if (e.target === e.currentTarget) {
onClose()
}
}}
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
>
{children}
)
}
function ModalContent({
modalRef,
sizeClasses,
title,
showCloseButton,
onClose,
children,
}: {
modalRef: React.RefObject
sizeClasses: string
title?: string | undefined
showCloseButton: boolean
onClose: () => void
children: ReactNode
}): React.ReactElement {
return (
e.stopPropagation()}
>
{children}
)
}
export function Modal({
children,
isOpen,
onClose,
title,
size = 'medium',
showCloseButton = true,
'aria-label': ariaLabel,
}: ModalProps): React.ReactElement | null {
const modalRef = useRef(null)
useModalFocus(modalRef, isOpen)
useModalKeyboard(isOpen, onClose)
if (!isOpen) {
return null
}
const sizeClasses = getSizeClasses(size)
return (
{children}
)
}