import { useEffect, useState } from 'react'; import type { ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { XCloseIcon } from '@/components/icons'; import { useFocusTrap } from '@/hooks/useFocusTrap'; import { Sheet } from './Sheet'; /** Ширина, с которой нижний шит перестаёт быть уместным. */ const DESKTOP_QUERY = '(min-width: 640px)'; function useIsDesktop(): boolean { const [isDesktop, setIsDesktop] = useState( () => typeof window !== 'undefined' && window.matchMedia(DESKTOP_QUERY).matches, ); useEffect(() => { const query = window.matchMedia(DESKTOP_QUERY); const onChange = (event: MediaQueryListEvent) => setIsDesktop(event.matches); query.addEventListener('change', onChange); setIsDesktop(query.matches); return () => query.removeEventListener('change', onChange); }, []); return isDesktop; } interface ResponsiveSheetProps { isOpen: boolean; onClose: () => void; title: string; children: ReactNode; } /** * Нижний шит на телефоне, обычное окно на десктопе. * * Шит — мобильная идиома: ручка перетаскивания мышью бессмысленна, а * прижатая к нижнему краю плашка на широком экране оставляет центр пустым * и обрезает нижние скругления. На десктопе то же содержимое показывается * центрированным окном с ловушкой фокуса и закрытием по Escape. */ export function ResponsiveSheet({ isOpen, onClose, title, children }: ResponsiveSheetProps) { const { t } = useTranslation(); const isDesktop = useIsDesktop(); // Ловушка нужна только своей ветке: у Sheet она уже своя. const dialogRef = useFocusTrap(isOpen && isDesktop, { onEscape: onClose }); if (!isOpen) return null; if (!isDesktop) { return ( {children} ); } return createPortal(

{title}

{children}
, document.body, ); }