diff --git a/src/components/Modal/Modal.test.tsx b/src/components/Modal/Modal.test.tsx index 7a423235e..c673eda58 100644 --- a/src/components/Modal/Modal.test.tsx +++ b/src/components/Modal/Modal.test.tsx @@ -4,6 +4,7 @@ import React, { FC, useEffect, useState } from "react"; import Button from "components/Button"; import Input from "components/Input"; +import ContextualMenu from "components/ContextualMenu"; import Modal from "./Modal"; describe("Modal ", () => { @@ -223,6 +224,41 @@ describe("Modal ", () => { expect(input).toHaveValue("delete item1"); }); + it("should allow Escape to close a ContextualMenu inside the modal before closing the modal", async () => { + const handleCloseModal = jest.fn(); + const handleMenuToggle = jest.fn(); + + render( + + + , + ); + + // The contextual menu dropdown should be open (aria-hidden="false") + const dropdown = document.querySelector( + '.p-contextual-menu__dropdown[aria-hidden="false"]', + ); + expect(dropdown).toBeInTheDocument(); + + // Press Escape — should close the menu, not the modal + await userEvent.keyboard("{Escape}"); + + // The modal close handler should NOT have been called + expect(handleCloseModal).not.toHaveBeenCalled(); + + // The dropdown should now be closed (no longer present with aria-hidden="false") + expect( + document.querySelector( + '.p-contextual-menu__dropdown[aria-hidden="false"]', + ), + ).not.toBeInTheDocument(); + }); + it("updates focusable elements when an initially disabled button becomes enabled", async () => { const user = userEvent.setup(); diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 0294a101e..7ca08f566 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -91,6 +91,19 @@ export const Modal = ({ const handleEscKey = ( event: KeyboardEvent | React.KeyboardEvent, ) => { + // If there is an open contextual menu dropdown inside (or alongside) this + // modal, let the Escape event propagate so that the menu's own keydown + // handler can close it first. The modal should only intercept Escape when + // no child overlay is open. This fixes the bug where a ContextualMenu + // rendered inside a Modal cannot be closed with the Escape key because + // stopImmediatePropagation() was called unconditionally, preventing the + // menu's document-level keydown listener from ever firing. + const hasOpenDropdown = !!document.querySelector( + '.p-contextual-menu__dropdown[aria-hidden="false"]', + ); + if (hasOpenDropdown) { + return; + } if ("nativeEvent" in event && event.nativeEvent.stopImmediatePropagation) { event.nativeEvent.stopImmediatePropagation(); } else if ("stopImmediatePropagation" in event) {