diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx index 60643a44419..3abe2018266 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx @@ -72,19 +72,26 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element const [open, setOpen] = useState(false); return ( - { - setOpen(isOpen); - setMenuOpen(isOpen); +
{ + // Prevent opening duplicate more options menu via right-click + if (open) e.stopPropagation(); }} - title={_t("room_list|room|more_options")} - showTitle={false} - align="start" - trigger={} > - -
+ { + setOpen(isOpen); + setMenuOpen(isOpen); + }} + title={_t("room_list|room|more_options")} + showTitle={false} + align="start" + trigger={} + > + + + ); } @@ -202,6 +209,16 @@ function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Eleme
e.stopPropagation()} + onContextMenu={() => { + if (open) { + // Close notification menu and allow context menu to open via event bubbling + // setTimeout ensures the menu closes after the current event completes bubbling + setTimeout(() => { + setOpen(false); + setMenuOpen(false); + }, 0); + } + }} > ", () => { await user.click(screen.getByRole("menuitem", { name: "Mute room" })); expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute); }); + + it("should prevent context menu event bubbling when right-clicking on open more options menu", async () => { + const user = userEvent.setup(); + const { container } = renderMenu(); + + await user.click(screen.getByRole("button", { name: "More Options" })); + await screen.findByRole("menuitem", { name: "Mark as read" }); + + const contextMenuHandler = jest.fn(); + container.addEventListener("contextmenu", contextMenuHandler); + + const openMenu = screen.getByRole("menu", { name: "More Options" }); + const menuWrapper = openMenu.parentElement; + const contextMenuEvent = new MouseEvent("contextmenu", { bubbles: true, cancelable: true }); + menuWrapper?.dispatchEvent(contextMenuEvent); + + expect(contextMenuHandler).not.toHaveBeenCalled(); + container.removeEventListener("contextmenu", contextMenuHandler); + }); + + it("should close notification menu when right-clicking on open notification menu", async () => { + const user = userEvent.setup(); + const setMenuOpen = jest.fn(); + renderMenu(setMenuOpen); + + await user.click(screen.getByRole("button", { name: "Notification options" })); + await screen.findByRole("menuitem", { name: "Match default settings" }); + + setMenuOpen.mockClear(); + + const openNotificationMenu = screen.getByRole("menu", { name: "Notification options" }); + const notificationMenuWrapper = openNotificationMenu.parentElement; // This should be the div with onContextMenu + const contextMenuEvent = new MouseEvent("contextmenu", { bubbles: true, cancelable: true }); + notificationMenuWrapper?.dispatchEvent(contextMenuEvent); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(setMenuOpen).toHaveBeenCalledWith(false); + }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx index b72bcc4156d..d4f16109710 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx @@ -215,4 +215,31 @@ describe("", () => { // But the room item itself should still be rendered expect(button).toBeInTheDocument(); }); + + test("should hide hover menu when context menu opens", async () => { + const user = userEvent.setup(); + + mocked(useRoomListItemViewModel).mockReturnValue({ + ...defaultValue, + showContextMenu: true, + showHoverMenu: true, + }); + + renderRoomListItem(); + + const button = screen.getByRole("option", { name: `Open room ${room.name}` }); + + // First hover to show hover menu + await user.hover(button); + await waitFor(() => expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument()); + + // Then right-click to open context menu + await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]); + + // Context menu should be open + await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument()); + + // Hover menu (More Options button) should be hidden when context menu is open + expect(screen.queryByRole("button", { name: "More Options" })).toBeNull(); + }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap index 6ebd76efef4..078950c3606 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap @@ -4,6 +4,7 @@ exports[` should render a room list 1`] = `
should render the more options menu 1`] = ` class="flex mx_RoomListItemMenuView" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;" > -
- + + + +
+ +
- + + + + + +