Manage nested dialogs through a central stack
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useId, type ReactNode } from "react";
|
||||
import { useEffect, useId, useRef, type ReactNode } from "react";
|
||||
import { translateReactNode, usePlatformLanguage } from "../i18n/LanguageContext";
|
||||
import { shouldCloseDialogOnBackdrop } from "./dialogInteractions";
|
||||
import {
|
||||
dialogIsTopmost,
|
||||
nextDialogActivationOrder,
|
||||
registerDialog,
|
||||
type DialogStackId
|
||||
} from "./dialogStack";
|
||||
|
||||
export type DialogProps = {
|
||||
open: boolean;
|
||||
@@ -46,6 +53,20 @@ export default function Dialog({
|
||||
}: DialogProps) {
|
||||
const titleId = useId();
|
||||
const canClose = Boolean(onClose) && !closeDisabled;
|
||||
const panelRef = useRef<HTMLElement | null>(null);
|
||||
const stackIdRef = useRef<DialogStackId>(Symbol("govoplan-dialog"));
|
||||
const activationOrderRef = useRef<number | null>(null);
|
||||
const canCloseRef = useRef(canClose);
|
||||
const onCloseRef = useRef(onClose);
|
||||
const restoreFocusRef = useRef<HTMLElement | null>(
|
||||
typeof document !== "undefined" && typeof HTMLElement !== "undefined" && document.activeElement instanceof HTMLElement
|
||||
? document.activeElement
|
||||
: null
|
||||
);
|
||||
canCloseRef.current = canClose;
|
||||
onCloseRef.current = onClose;
|
||||
if (!open) activationOrderRef.current = null;
|
||||
else if (activationOrderRef.current === null) activationOrderRef.current = nextDialogActivationOrder();
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const renderedTitle = translateReactNode(title, translateText);
|
||||
const renderedChildren = translateReactNode(children, translateText);
|
||||
@@ -53,15 +74,36 @@ export default function Dialog({
|
||||
const translatedCloseLabel = translateText(closeLabel);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !canClose) return undefined;
|
||||
if (open || typeof document === "undefined") return undefined;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") onClose?.();
|
||||
const rememberFocusedElement = () => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!(activeElement instanceof HTMLElement)) return;
|
||||
if (panelRef.current?.contains(activeElement)) return;
|
||||
restoreFocusRef.current = activeElement;
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [canClose, onClose, open]);
|
||||
rememberFocusedElement();
|
||||
document.addEventListener("focusin", rememberFocusedElement);
|
||||
return () => document.removeEventListener("focusin", rememberFocusedElement);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || typeof document === "undefined") return undefined;
|
||||
|
||||
const panel = panelRef.current;
|
||||
const activationOrder = activationOrderRef.current;
|
||||
if (!panel || activationOrder === null) return undefined;
|
||||
|
||||
return registerDialog({
|
||||
id: stackIdRef.current,
|
||||
activationOrder,
|
||||
panel,
|
||||
restoreFocus: restoreFocusRef.current,
|
||||
canClose: () => canCloseRef.current,
|
||||
onClose: () => onCloseRef.current?.()
|
||||
}, document.activeElement);
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
@@ -70,13 +112,19 @@ export default function Dialog({
|
||||
className={joinClasses("dialog-backdrop", backdropClassName)}
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (closeOnBackdrop && canClose && event.target === event.currentTarget) onClose?.();
|
||||
if (
|
||||
dialogIsTopmost(stackIdRef.current)
|
||||
&& shouldCloseDialogOnBackdrop(event.target, event.currentTarget, closeOnBackdrop, canClose)
|
||||
) onClose?.();
|
||||
}}
|
||||
>
|
||||
<section
|
||||
ref={panelRef}
|
||||
tabIndex={-1}
|
||||
className={joinClasses("dialog-panel", className)}
|
||||
role={role}
|
||||
aria-modal="true"
|
||||
data-dialog-stack-state="topmost"
|
||||
aria-labelledby={titleId}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user