New room list: add context menu to room list item (#29952)

* chore: update compound-web

* chore: remove unused export

* feat: export content of more option menu

* feat: add context menu

* feat: add `showContextMenu` to vm

* feat: use context menu in new room list

* test: add tests for room list item

* test: fix room list test

* test: add `showContextMenu` test for `useRoomListItemViewModel`

* test: add e2e test for context menu

* chore: update compound

* test: update snapshots and e2e test

* fix: avoid icon blinking when we reopen the context menu

* test: add test for menu closing

* doc: remove useless tsdoc param

* chore: update `@vector-im/compound-web`

* refactor: remove manual focus

* test(e2e): fix focus after closing notification menu

* doc: remove useless jobs
This commit is contained in:
Florian Duros
2025-06-24 11:50:27 +02:00
committed by GitHub
parent 52f836a0dd
commit f707bb410e
11 changed files with 169 additions and 32 deletions

View File

@@ -30,6 +30,10 @@ export interface RoomListItemViewState {
* The name of the room.
*/
name: string;
/**
* Whether the context menu should be shown.
*/
showContextMenu: boolean;
/**
* Whether the hover menu should be shown.
*/
@@ -105,12 +109,12 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
setNotificationValues(getNotificationValues(notificationState));
}, [notificationState]);
// We don't want to show the hover menu if
// We don't want to show the menus if
// - there is an invitation for this room
// - the user doesn't have access to both notification and more options menus
// - the user doesn't have access to notification and more options menus
const showContextMenu = !invited && hasAccessToOptionsMenu(room);
const showHoverMenu =
!invited &&
(hasAccessToOptionsMenu(room) || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived));
!invited && (showContextMenu || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived));
const messagePreview = useRoomMessagePreview(room);
@@ -137,6 +141,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
return {
name,
notificationState,
showContextMenu,
showHoverMenu,
openRoom,
a11yLabel,

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Room } from "matrix-js-sdk/src/matrix";
import { type JSX, type PropsWithChildren } from "react";
import { ContextMenu } from "@vector-im/compound-web";
import React from "react";
import { _t } from "../../../../languageHandler";
import { MoreOptionContent } from "./RoomListItemMenuView";
import { useRoomListItemMenuViewModel } from "../../../viewmodels/roomlist/RoomListItemMenuViewModel";
interface RoomListItemContextMenuViewProps {
/**
* The room to display the menu for.
*/
room: Room;
/**
* Set the menu open state.
*/
setMenuOpen: (isOpen: boolean) => void;
}
/**
* A view for the room list item context menu.
*/
export function RoomListItemContextMenuView({
room,
setMenuOpen,
children,
}: PropsWithChildren<RoomListItemContextMenuViewProps>): JSX.Element {
const vm = useRoomListItemMenuViewModel(room);
return (
<ContextMenu
title={_t("room_list|room|more_options")}
showTitle={false}
// To not mess with the roving tab index of the button
hasAccessibleAlternative={true}
trigger={children}
onOpenChange={setMenuOpen}
>
<MoreOptionContent vm={vm} />
</ContextMenu>
);
}

View File

@@ -35,7 +35,6 @@ interface RoomListItemMenuViewProps {
room: Room;
/**
* Set the menu open state.
* @param isOpen
*/
setMenuOpen: (isOpen: boolean) => void;
}
@@ -84,6 +83,21 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element
align="start"
trigger={<MoreOptionsButton size="24px" />}
>
<MoreOptionContent vm={vm} />
</Menu>
);
}
interface MoreOptionContentProps {
/**
* The view model state for the menu.
*/
vm: RoomListItemMenuViewState;
}
export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
return (
<>
{vm.canMarkAsRead && (
<MenuItem
Icon={MarkAsReadIcon}
@@ -143,7 +157,7 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element
onClick={(evt) => evt.stopPropagation()}
hideChevron={true}
/>
</Menu>
</>
);
}
@@ -154,7 +168,7 @@ interface MoreOptionsButtonProps extends ComponentProps<typeof IconButton> {
/**
* A button to trigger the more options menu.
*/
export const MoreOptionsButton = function MoreOptionsButton(props: MoreOptionsButtonProps): JSX.Element {
const MoreOptionsButton = function MoreOptionsButton(props: MoreOptionsButtonProps): JSX.Element {
return (
<Tooltip label={_t("room_list|room|more_options")}>
<IconButton aria-label={_t("room_list|room|more_options")} {...props}>
@@ -244,7 +258,7 @@ interface NotificationButtonProps extends ComponentProps<typeof IconButton> {
/**
* A button to trigger the notification menu.
*/
export const NotificationButton = function MoreOptionsButton({
const NotificationButton = function MoreOptionsButton({
isRoomMuted,
ref,
...props

View File

@@ -15,6 +15,7 @@ import { RoomListItemMenuView } from "./RoomListItemMenuView";
import { NotificationDecoration } from "../NotificationDecoration";
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView";
interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement> {
/**
@@ -47,7 +48,13 @@ export const RoomListItemView = memo(function RoomListItemView({
const showHoverDecoration = isMenuOpen || isHover;
const showHoverMenu = showHoverDecoration && vm.showHoverMenu;
return (
const closeMenu = useCallback(() => {
// To avoid icon blinking when closing the menu, we delay the state update
// Also, let the focus move to the menu trigger before closing the menu
setTimeout(() => setIsMenuOpen(false), 10);
}, []);
const content = (
<button
ref={ref}
className={classNames("mx_RoomListItemView", {
@@ -92,17 +99,7 @@ export const RoomListItemView = memo(function RoomListItemView({
{showHoverMenu ? (
<RoomListItemMenuView
room={room}
setMenuOpen={(isOpen) => {
if (isOpen) {
setIsMenuOpen(isOpen);
} else {
// To avoid icon blinking when closing the menu, we delay the state update
setTimeout(() => setIsMenuOpen(isOpen), 0);
// After closing the menu, we need to set the focus back to the button
// 10ms because the focus moves to the body and we put back the focus on the button
setTimeout(() => buttonRef.current?.focus(), 10);
}
}}
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
/>
) : (
<>
@@ -120,6 +117,24 @@ export const RoomListItemView = memo(function RoomListItemView({
</Flex>
</button>
);
if (!vm.showContextMenu) return content;
return (
<RoomListItemContextMenuView
room={room}
setMenuOpen={(isOpen) => {
if (isOpen) {
// To avoid icon blinking when the context menu is re-opened
setTimeout(() => setIsMenuOpen(true), 0);
} else {
closeMenu();
}
}}
>
{content}
</RoomListItemContextMenuView>
);
});
/**