'use client'; import React, { useEffect, useRef, type ReactNode, type KeyboardEvent as ReactKeyboardEvent, } from 'react'; import { Box } from './Box'; import { Text } from './Text'; import { Heading } from './Heading'; import { Button } from './Button'; interface ModalProps { title: string; description?: string; children?: ReactNode; primaryActionLabel?: string; secondaryActionLabel?: string; onPrimaryAction?: () => void | Promise; onSecondaryAction?: () => void; onOpenChange?: (open: boolean) => void; isOpen: boolean; } export function Modal({ title, description, children, primaryActionLabel, secondaryActionLabel, onPrimaryAction, onSecondaryAction, onOpenChange, isOpen, }: ModalProps) { const dialogRef = useRef(null); const previouslyFocusedElementRef = useRef(null); useEffect(() => { if (isOpen) { previouslyFocusedElementRef.current = document.activeElement; const focusable = getFirstFocusable(dialogRef.current); if (focusable) { focusable.focus(); } else if (dialogRef.current) { dialogRef.current.focus(); } return; } if (!isOpen && previouslyFocusedElementRef.current instanceof HTMLElement) { previouslyFocusedElementRef.current.focus(); } }, [isOpen]); const handleKeyDown = (event: ReactKeyboardEvent) => { if (event.key === 'Escape') { if (onOpenChange) { onOpenChange(false); } return; } if (event.key === 'Tab') { const focusable = getFocusableElements(dialogRef.current); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1] ?? first; if (!first || !last) { return; } if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } else if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } } }; const handleBackdropClick = (event: React.MouseEvent) => { if (event.target === event.currentTarget && onOpenChange) { onOpenChange(false); } }; if (!isOpen) { return null; } return ( {title} {description && ( {description} )} {children} {(primaryActionLabel || secondaryActionLabel) && ( {secondaryActionLabel && ( )} {primaryActionLabel && ( )} )} ); } function getFocusableElements(root: HTMLElement | null): HTMLElement[] { if (!root) return []; const selectors = [ 'a[href]', 'button:not([disabled])', 'textarea:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', '[tabindex]:not([tabindex="-1"])', ]; const nodes = Array.from( root.querySelectorAll(selectors.join(',')), ); return nodes.filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')); } function getFirstFocusable(root: HTMLElement | null): HTMLElement | null { const elements = getFocusableElements(root); return elements[0] ?? null; }