132 lines
2.9 KiB
TypeScript
132 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
import { ComponentProps, useEffect, useRef, useState } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { classNames } from './classNames'
|
|
|
|
export interface ModalWindowProps extends ComponentProps<'div'> {
|
|
disableClosing?: boolean
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
export function ModalWindow({
|
|
children,
|
|
className,
|
|
disableClosing,
|
|
isOpen,
|
|
onClose,
|
|
...otherProps
|
|
}: ModalWindowProps) {
|
|
const [isClient, setIsClient] = useState(false)
|
|
|
|
const [modalState, setModalState] = useState<
|
|
'opening' | 'open' | 'closing' | 'closed'
|
|
>('closed')
|
|
|
|
const windowContentsContainerRef = useRef<HTMLDivElement>(null)
|
|
|
|
function handleTransitionEnd() {
|
|
if (modalState === 'closing') {
|
|
setModalState('closed')
|
|
onClose()
|
|
}
|
|
if (modalState === 'opening') {
|
|
setModalState('open')
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
setModalState('closing')
|
|
}
|
|
|
|
function focusFirstElement() {
|
|
const windowContentsContainer = windowContentsContainerRef.current
|
|
|
|
if (!windowContentsContainer) {
|
|
return
|
|
}
|
|
|
|
const firstFocusableElement = windowContentsContainer.querySelector(
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
) as HTMLElement
|
|
|
|
if (firstFocusableElement) {
|
|
firstFocusableElement.focus()
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
setIsClient(true)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setModalState('opening')
|
|
focusFirstElement()
|
|
} else {
|
|
setModalState('closing')
|
|
}
|
|
}, [isOpen])
|
|
|
|
useEffect(() => {
|
|
if (!disableClosing && isOpen) {
|
|
const handleEscape = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
handleClose()
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleEscape)
|
|
|
|
return () => window.removeEventListener('keydown', handleEscape)
|
|
}
|
|
}, [disableClosing, isOpen, onClose])
|
|
|
|
if (!isClient) {
|
|
return null
|
|
}
|
|
|
|
return createPortal(
|
|
<>
|
|
<div
|
|
className={classNames.backdrop({ modalState })}
|
|
{...(!disableClosing && { onClick: handleClose })}
|
|
/>
|
|
<div
|
|
className={twMerge(classNames.container({ modalState }), className)}
|
|
ref={windowContentsContainerRef}
|
|
onTransitionEnd={handleTransitionEnd}
|
|
{...otherProps}
|
|
>
|
|
{children}
|
|
</div>
|
|
</>,
|
|
document.body,
|
|
)
|
|
}
|
|
|
|
ModalWindow.Title = function ModalWindowTitle({
|
|
children,
|
|
className,
|
|
}: ComponentProps<'header'>) {
|
|
return <div className={twMerge(classNames.header, className)}>{children}</div>
|
|
}
|
|
|
|
ModalWindow.Body = function ModalWindowBody({
|
|
children,
|
|
className,
|
|
}: ComponentProps<'main'>) {
|
|
return <div className={twMerge(classNames.body, className)}>{children}</div>
|
|
}
|
|
|
|
ModalWindow.Buttons = function ModalWindowBody({
|
|
children,
|
|
className,
|
|
}: ComponentProps<'div'>) {
|
|
return (
|
|
<div className={twMerge(classNames.buttons, className)}>{children}</div>
|
|
)
|
|
}
|