Corp/frontend/src/components/shared/MediaPreviewModal.tsx

124 lines
4.0 KiB
TypeScript

"use client";
import Image from "next/image";
import { Download, X } from "lucide-react";
import { useEffect, useRef, useSyncExternalStore } from "react";
import { createPortal } from "react-dom";
import type { MediaPreview } from "@/lib/media";
type MediaPreviewModalProps = {
preview: MediaPreview;
onClose: () => void;
};
const subscribeToClient = () => () => undefined;
const getClientSnapshot = () => true;
const getServerSnapshot = () => false;
export default function MediaPreviewModal({ preview, onClose }: MediaPreviewModalProps) {
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
const onCloseRef = useRef(onClose);
const canUseDOM = useSyncExternalStore(
subscribeToClient,
getClientSnapshot,
getServerSnapshot,
);
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
useEffect(() => {
const previousOverflow = document.body.style.overflow;
const previousFocus = document.activeElement instanceof HTMLElement
? document.activeElement
: null;
const shouldRestoreFocus = previousFocus?.matches(":focus-visible") ?? false;
document.body.style.overflow = "hidden";
closeButtonRef.current?.focus();
const handleEscape = (event: KeyboardEvent) => {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopImmediatePropagation();
onCloseRef.current();
};
document.addEventListener("keydown", handleEscape, true);
return () => {
document.removeEventListener("keydown", handleEscape, true);
document.body.style.overflow = previousOverflow;
if (shouldRestoreFocus) previousFocus?.focus();
};
}, []);
const downloadUrl = preview.downloadUrl;
if (!canUseDOM) return null;
return createPortal(
<div
data-overlay-root="true"
className="fixed inset-0 z-[300] flex cursor-zoom-out items-center justify-center bg-black/85 p-3 backdrop-blur-[2px] sm:p-5"
onClick={onClose}
role="dialog"
aria-modal="true"
aria-label={`Просмотр ${preview.type === "image" ? "изображения" : "видео"}: ${preview.name}`}
>
<div
className="absolute right-3 top-3 z-10 flex items-center gap-2 sm:right-5 sm:top-5"
onClick={(event) => event.stopPropagation()}
>
{downloadUrl ? (
<a
href={downloadUrl}
download={preview.name}
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
title="Скачать"
aria-label={`Скачать ${preview.name}`}
>
<Download size={18} />
</a>
) : null}
<button
ref={closeButtonRef}
type="button"
onClick={onClose}
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
title="Закрыть"
aria-label="Закрыть предпросмотр"
>
<X size={18} />
</button>
</div>
{preview.type === "image" ? (
<Image
src={preview.src}
alt={preview.name}
width={1600}
height={1200}
unoptimized
draggable={false}
className="h-auto w-auto max-h-[86vh] max-w-[90vw] cursor-zoom-out select-none rounded-lg object-contain shadow-2xl"
/>
) : (
<div
className="max-h-full max-w-[90vw] cursor-default"
onClick={(event) => event.stopPropagation()}
>
<video
controls
autoPlay
playsInline
className="max-h-[86vh] max-w-[90vw] rounded-lg bg-black shadow-2xl"
src={preview.src}
/>
</div>
)}
</div>,
document.body,
);
}