Files
Rental-operations-platform/src/components/overlays/Dialog.tsx
T
2026-06-25 19:06:59 -04:00

100 lines
2.7 KiB
TypeScript

'use client';
import { Button } from '@/components/actions/Button';
import { useScrollLock } from '@/components/foundation/client-hooks';
import { classNames } from '@/lib/components/classNames';
import { useEffect, useId, useRef, type ReactNode, type RefObject } from 'react';
import styles from './Dialog.module.css';
interface DialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
children: ReactNode;
closeLabel: string;
initialFocusRef?: RefObject<HTMLElement | null>;
returnFocusRef?: RefObject<HTMLElement | null>;
variant?: 'modal' | 'drawer';
dismissible?: boolean;
className?: string;
}
export function Dialog({
open,
onOpenChange,
title,
description,
children,
closeLabel,
initialFocusRef,
returnFocusRef,
variant = 'modal',
dismissible = true,
className,
}: DialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);
const titleId = useId();
const descriptionId = useId();
useScrollLock(open);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open && !dialog.open) {
dialog.showModal();
window.requestAnimationFrame(() => (initialFocusRef?.current ?? closeRef.current)?.focus());
} else if (!open && dialog.open) {
dialog.close();
}
}, [initialFocusRef, open]);
const close = () => {
dialogRef.current?.close();
onOpenChange(false);
window.requestAnimationFrame(() => returnFocusRef?.current?.focus());
};
return (
<dialog
ref={dialogRef}
className={classNames(styles.dialog, styles[variant], className)}
aria-labelledby={titleId}
aria-describedby={description ? descriptionId : undefined}
onCancel={(event) => {
if (!dismissible) {
event.preventDefault();
return;
}
event.preventDefault();
close();
}}
onClose={() => onOpenChange(false)}
onClick={(event) => {
if (dismissible && event.target === dialogRef.current) close();
}}
>
<div className={styles.panel}>
<header className={styles.header}>
<div>
<h2 id={titleId}>{title}</h2>
{description ? <p id={descriptionId}>{description}</p> : null}
</div>
{dismissible ? (
<Button
ref={closeRef}
intent="ghost"
size="small"
icon="close"
aria-label={closeLabel}
onClick={close}
/>
) : null}
</header>
<div className={styles.content}>{children}</div>
</div>
</dialog>
);
}