veza/apps/web/src/components/ui/modal.tsx
2025-12-03 22:56:50 +01:00

116 lines
2.9 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { FocusTrap } from './focus-trap';
import { X } from 'lucide-react';
import { Button } from './button';
import { cn } from '@/lib/utils';
export interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
title?: string;
closeOnOverlayClick?: boolean;
closeOnEscape?: boolean;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
className?: string;
}
const sizeClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
full: 'max-w-full mx-4',
};
/**
* Composant Modal réutilisable avec overlay, fermeture, et gestion du focus.
*/
export function Modal({
open,
onClose,
children,
title,
closeOnOverlayClick = true,
closeOnEscape = true,
size = 'md',
className,
}: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
// Empêcher le scroll du body quand le modal est ouvert
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = '';
};
}
}, [open]);
// Gérer le clic sur l'overlay
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (closeOnOverlayClick && e.target === e.currentTarget) {
onClose();
}
};
// Ne pas rendre le modal s'il n'est pas ouvert
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
onClick={handleOverlayClick}
role="dialog"
aria-modal="true"
aria-labelledby={title ? 'modal-title' : undefined}
>
{/* Overlay */}
<div className="fixed inset-0 bg-black/50 transition-opacity" />
{/* Modal Content */}
<FocusTrap active={open} onEscape={closeOnEscape ? onClose : undefined}>
<div
ref={modalRef}
className={cn(
'relative z-50 w-full bg-background rounded-lg shadow-xl',
'transform transition-all',
sizeClasses[size],
className
)}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
{(title || closeOnEscape) && (
<div className="flex items-center justify-between p-6 border-b">
{title && (
<h2
id="modal-title"
className="text-2xl font-semibold leading-none tracking-tight"
>
{title}
</h2>
)}
{closeOnEscape && (
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="ml-auto"
aria-label="Fermer"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
)}
{/* Content */}
<div className="p-6">{children}</div>
</div>
</FocusTrap>
</div>
);
}