| 1 | import { useEffect, useId, useLayoutEffect, useRef, type ReactNode } from "react"; |
| 2 | import { createPortal } from "react-dom"; |
| 3 | import { useT } from "../lib/i18n"; |
| 4 | import { ModalCloseButton } from "./ModalCloseButton"; |
| 5 | |
| 6 | /** A settings child dialog that owns focus and Escape without closing settings. */ |
| 7 | export function ProviderDialog({ title, children, onClose }: { title: string; children: ReactNode; onClose: () => void }) { |
| 8 | const id = useId(); |
| 9 | const ref = useRef<HTMLDivElement>(null); |
| 10 | const closeRef = useRef(onClose); |
| 11 | closeRef.current = onClose; |
| 12 | const t = useT(); |
| 13 | useLayoutEffect(() => { |
| 14 | const previous = document.activeElement as HTMLElement | null; |
| 15 | ref.current?.querySelector<HTMLElement>("input:not(:disabled), select:not(:disabled)")?.focus(); |
| 16 | return () => { if (previous?.isConnected) previous.focus(); }; |
| 17 | }, []); |
| 18 | useEffect(() => { |
| 19 | const keydown = (event: KeyboardEvent) => { |
| 20 | if (event.key === "Escape") { |
| 21 | event.preventDefault(); event.stopImmediatePropagation(); closeRef.current(); |
| 22 | } |
| 23 | if (event.key === "Tab") { |
| 24 | const elements = Array.from(ref.current?.querySelectorAll<HTMLElement>("button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), summary") ?? []); |
| 25 | const first = elements[0], last = elements[elements.length - 1]; |
| 26 | if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } |
| 27 | else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } |
| 28 | } |
| 29 | }; |
| 30 | window.addEventListener("keydown", keydown, true); |
| 31 | return () => window.removeEventListener("keydown", keydown, true); |
| 32 | }, []); |
| 33 | return createPortal(<div className="modal-backdrop provider-dialog-backdrop" data-app-overlay="" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}> |
| 34 | <div ref={ref} className="modal provider-dialog" role="dialog" aria-modal="true" aria-labelledby={id}> |
| 35 | <header><h3 id={id}>{title}</h3><ModalCloseButton label={t("common.close")} onClick={onClose} /></header> |
| 36 | {children} |
| 37 | </div> |
| 38 | </div>, document.body); |
| 39 | } |
| 40 |