Files
lynkeduppro-crm/vendor/photo-gallery-sdk/src/components/Modal.tsx
T

63 lines
1.5 KiB
TypeScript

'use client';
import { type ReactNode, useEffect, useRef, useSyncExternalStore } from 'react';
import { useFocusTrap } from '../hooks/useFocusTrap';
// Module-level modal emitter (mirrors the context-menu host pattern).
let current: ReactNode | null = null;
const listeners = new Set<() => void>();
const emit = () => listeners.forEach((l) => l());
export function openModal(node: ReactNode) {
current = node;
emit();
}
export function closeModal() {
current = null;
emit();
}
/** Whether a modal is currently mounted. Read by global Escape handling so the
* modal (which owns Escape) is never fought over by another handler. */
export function isModalOpen(): boolean {
return current !== null;
}
function subscribe(cb: () => void) {
listeners.add(cb);
return () => listeners.delete(cb);
}
export function ModalHost() {
const node = useSyncExternalStore(
subscribe,
() => current,
() => null,
);
const ref = useRef<HTMLDivElement>(null);
useFocusTrap(ref, Boolean(node), closeModal);
useEffect(() => {
if (!node) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') closeModal();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [node]);
if (!node) return null;
return (
<div
ref={ref}
className="apg-modal__backdrop"
onMouseDown={(e) => {
if (e.target === e.currentTarget) closeModal();
}}
>
{node}
</div>
);
}