/** * Доступное модальное окно с поддержкой ARIA и фокус-ловкой */ 'use client'; import { ReactNode, useEffect, useRef, useId } from 'react'; import { getModalAriaProps } from '@/lib/accessibility/aria'; import { createFocusTrap, createEscapeHandler } from '@/lib/accessibility/keyboard'; interface AccessibleModalProps { isOpen: boolean; onClose: () => void; title: string; description?: string; children: ReactNode; size?: 'small' | 'medium' | 'large'; } export default function AccessibleModal({ isOpen, onClose, title, description, children, size = 'medium', }: AccessibleModalProps) { const modalRef = useRef(null); const titleId = useId(); const descriptionId = useId(); const ariaProps = getModalAriaProps({ titleId: `modal-title-${titleId}`, descriptionId: description ? `modal-description-${descriptionId}` : undefined, }); useEffect(() => { if (!isOpen || !modalRef.current) { return; } // Фокус-ловка const cleanupFocusTrap = createFocusTrap(modalRef.current, onClose); // Обработка Escape const handleEscape = createEscapeHandler(onClose); document.addEventListener('keydown', handleEscape); // Блокировка скролла body document.body.style.overflow = 'hidden'; // Фокус на модальное окно modalRef.current.focus(); return () => { cleanupFocusTrap(); document.removeEventListener('keydown', handleEscape); document.body.style.overflow = ''; }; }, [isOpen, onClose]); useEffect(() => { if (isOpen) { // Сохраняем фокус на элементе, который открыл модальное окно const activeElement = document.activeElement as HTMLElement; return () => { // Возвращаем фокус при закрытии activeElement?.focus(); }; } return undefined; }, [isOpen]); if (!isOpen) { return null; } const sizeStyles = { small: { maxWidth: '400px' }, medium: { maxWidth: '600px' }, large: { maxWidth: '800px' }, }; return (
{ if (e.target === e.currentTarget) { onClose(); } }} >
e.stopPropagation()} >

{title}

{description && (

{description}

)}
{children}
); }