diff --git a/apps/client/src/features/base/components/grid/grid-header-cell.tsx b/apps/client/src/features/base/components/grid/grid-header-cell.tsx index 9fe7bebe..0d2f8d28 100644 --- a/apps/client/src/features/base/components/grid/grid-header-cell.tsx +++ b/apps/client/src/features/base/components/grid/grid-header-cell.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useRef } from "react"; +import { memo, useCallback, useEffect, useRef } from "react"; import { Header, flexRender } from "@tanstack/react-table"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; @@ -106,6 +106,21 @@ export const GridHeaderCell = memo(function GridHeaderCell({ setActivePropertyMenu(null); }, [setActivePropertyMenu]); + // Mantine's built-in `closeOnEscape` only fires when focus is inside the + // dropdown, but opening the property menu (clicking the header) leaves + // focus on the header itself. Add a document-level ESC handler that + // closes the menu when it's open and not dirty. + useEffect(() => { + if (!menuOpened) return; + const handler = (e: KeyboardEvent) => { + if (e.key !== "Escape") return; + if (propertyMenuDirty) return; + handleMenuClose(); + }; + document.addEventListener("keydown", handler); + return () => document.removeEventListener("keydown", handler); + }, [menuOpened, propertyMenuDirty, handleMenuClose]); + const TypeIcon = property ? typeIcons[property.type] : undefined; const sortableStyle = transform