mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2026-06-15 23:20:32 +03:00
close nested modals one at a time on escape via shared useModalEscape hook
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
46
frontend/src/hooks/useModalEscape.js
Normal file
46
frontend/src/hooks/useModalEscape.js
Normal 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]);
|
||||
}
|
||||
Reference in New Issue
Block a user