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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user