Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/components/Modal/Modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ", () => {
Expand Down Expand Up @@ -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(
<Modal title="Test" close={handleCloseModal}>
<ContextualMenu
toggleLabel="Open menu"
links={[{ children: "Item 1" }]}
onToggleMenu={handleMenuToggle}
visible={true}
/>
</Modal>,
);

// 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();

Expand Down
13 changes: 13 additions & 0 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ export const Modal = ({
const handleEscKey = (
event: KeyboardEvent | React.KeyboardEvent<HTMLDivElement>,
) => {
// 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) {
Expand Down