close nested modals one at a time on escape via shared useModalEscape hook

This commit is contained in:
shatfield4
2026-06-11 17:16:31 -07:00
parent de7a1d64ac
commit d31140c605
3 changed files with 51 additions and 20 deletions

View File

@@ -15,6 +15,7 @@ import Modal, {
ModalPrimaryButton,
} from "@/components/lib/Modal";
import { EmbeddingProgressProvider } from "@/EmbeddingProgressContext";
import { useModalEscape } from "@/hooks/useModalEscape";
const noop = () => {};
const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
@@ -145,17 +146,7 @@ export function useManageWorkspaceModal() {
setShowing(false);
}
useEffect(() => {
function onEscape(event) {
if (!showing || event.key !== "Escape") return;
setShowing(false);
}
document.addEventListener("keydown", onEscape);
return () => {
document.removeEventListener("keydown", onEscape);
};
}, [showing]);
useModalEscape(showing, hideModal);
return { showing, showModal, hideModal };
}

View File

@@ -1,6 +1,7 @@
import { Children, useEffect } from "react";
import { Children } from "react";
import { createPortal } from "react-dom";
import { X } from "@phosphor-icons/react";
import { useModalEscape } from "@/hooks/useModalEscape";
/** @type {Record<string, string>} max-width per size, matched to the Figma modal frames */
const SIZE_CLASSES = {
@@ -41,14 +42,7 @@ export default function Modal({
closeOnEsc = true,
noPortal = false,
}) {
useEffect(() => {
if (!isOpen || !closeOnEsc || !onClose) return;
function handleKeyDown(event) {
if (event.key === "Escape") onClose();
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, closeOnEsc, onClose]);
useModalEscape(isOpen && closeOnEsc, onClose);
if (!isOpen) return null;

View File

@@ -0,0 +1,46 @@
import { useEffect, useRef } from "react";
/**
* Shared stack of open modals that close on Escape. Only the top-most (most
* recently opened) modal handles Escape, so stacked/nested modals close one at
* a time from the top down instead of all at once.
* @type {{ onClose: () => void }[]}
*/
const escapeStack = [];
function handleEscape(event) {
if (event.key !== "Escape") return;
const top = escapeStack[escapeStack.length - 1];
top?.onClose?.();
}
/**
* Close a modal on Escape while respecting nesting. While `active`, the modal
* joins a shared stack and only fires `onClose` when it is the top-most open
* modal. A single window listener is shared across all modals.
*
* @param {boolean} active - Whether the modal is open and should close on Escape
* @param {() => void} [onClose] - Called when Escape closes this modal
*/
export function useModalEscape(active, onClose) {
// Stable entry so re-renders (e.g. a new inline `onClose`) update the handler
// in place rather than reordering the stack.
const entryRef = useRef({ onClose });
useEffect(() => {
entryRef.current.onClose = onClose;
}, [onClose]);
useEffect(() => {
if (!active) return;
const entry = entryRef.current;
escapeStack.push(entry);
if (escapeStack.length === 1)
window.addEventListener("keydown", handleEscape);
return () => {
const idx = escapeStack.indexOf(entry);
if (idx !== -1) escapeStack.splice(idx, 1);
if (escapeStack.length === 0)
window.removeEventListener("keydown", handleEscape);
};
}, [active]);
}