Room list: fix room options remaining on room item after mouse leaving (#31414)
* refactor(room list): remove extra component button and `setMenuOpen` callback * fix(room list): replace js focus and hover handling by CSS to fix focus decoration Closes https://github.com/element-hq/element-web/issues/31365 * test(room list): update jest test * refactor(room list): remove irrelevant comment * test(room list): update screenshots
This commit is contained in:
@@ -19,10 +19,6 @@ interface RoomListItemContextMenuViewProps {
|
||||
* The room to display the menu for.
|
||||
*/
|
||||
room: Room;
|
||||
/**
|
||||
* Set the menu open state.
|
||||
*/
|
||||
setMenuOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,7 +26,6 @@ interface RoomListItemContextMenuViewProps {
|
||||
*/
|
||||
export function RoomListItemContextMenuView({
|
||||
room,
|
||||
setMenuOpen,
|
||||
children,
|
||||
}: PropsWithChildren<RoomListItemContextMenuViewProps>): JSX.Element {
|
||||
const vm = useRoomListItemMenuViewModel(room);
|
||||
@@ -42,7 +37,6 @@ export function RoomListItemContextMenuView({
|
||||
// To not mess with the roving tab index of the button
|
||||
hasAccessibleAlternative={true}
|
||||
trigger={children}
|
||||
onOpenChange={setMenuOpen}
|
||||
>
|
||||
<MoreOptionContent vm={vm} />
|
||||
</ContextMenu>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ComponentProps, type JSX, type Ref, useState } from "react";
|
||||
import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem, Tooltip } from "@vector-im/compound-web";
|
||||
import React, { type JSX, useState } from "react";
|
||||
import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem } from "@vector-im/compound-web";
|
||||
import MarkAsReadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-read";
|
||||
import MarkAsUnreadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-unread";
|
||||
import FavouriteIcon from "@vector-im/compound-design-tokens/assets/web/icons/favourite";
|
||||
@@ -20,6 +20,7 @@ import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/ic
|
||||
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Flex } from "@element-hq/web-shared-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import {
|
||||
@@ -29,26 +30,27 @@ import {
|
||||
import { RoomNotifState } from "../../../../RoomNotifs";
|
||||
|
||||
interface RoomListItemMenuViewProps {
|
||||
/**
|
||||
* Additional class name for the root element.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* 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 menu.
|
||||
*/
|
||||
export function RoomListItemMenuView({ room, setMenuOpen }: RoomListItemMenuViewProps): JSX.Element {
|
||||
export function RoomListItemMenuView({ room, className }: RoomListItemMenuViewProps): JSX.Element {
|
||||
const vm = useRoomListItemMenuViewModel(room);
|
||||
|
||||
return (
|
||||
<Flex className="mx_RoomListItemMenuView" align="center" gap="var(--cpd-space-1x)">
|
||||
{vm.showMoreOptionsMenu && <MoreOptionsMenu setMenuOpen={setMenuOpen} vm={vm} />}
|
||||
{vm.showNotificationMenu && <NotificationMenu setMenuOpen={setMenuOpen} vm={vm} />}
|
||||
<Flex className={classNames("mx_RoomListItemMenuView", className)} align="center" gap="var(--cpd-space-1x)">
|
||||
{vm.showMoreOptionsMenu && <MoreOptionsMenu vm={vm} />}
|
||||
{vm.showNotificationMenu && <NotificationMenu vm={vm} />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -58,30 +60,30 @@ interface MoreOptionsMenuProps {
|
||||
* The view model state for the menu.
|
||||
*/
|
||||
vm: RoomListItemMenuViewState;
|
||||
/**
|
||||
* Set the menu open state.
|
||||
* @param isOpen
|
||||
*/
|
||||
setMenuOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The more options menu for the room list item.
|
||||
*/
|
||||
function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element {
|
||||
function MoreOptionsMenu({ vm }: MoreOptionsMenuProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
onOpenChange={setOpen}
|
||||
title={_t("room_list|room|more_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<MoreOptionsButton size="24px" />}
|
||||
trigger={
|
||||
<IconButton
|
||||
tooltip={_t("room_list|room|more_options")}
|
||||
aria-label={_t("room_list|room|more_options")}
|
||||
size="24px"
|
||||
>
|
||||
<OverflowIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<MoreOptionContent vm={vm} />
|
||||
</Menu>
|
||||
@@ -164,55 +166,37 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
interface MoreOptionsButtonProps extends ComponentProps<typeof IconButton> {
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to trigger the more options menu.
|
||||
*/
|
||||
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}>
|
||||
<OverflowIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
interface NotificationMenuProps {
|
||||
/**
|
||||
* The view model state for the menu.
|
||||
*/
|
||||
vm: RoomListItemMenuViewState;
|
||||
/**
|
||||
* Set the menu open state.
|
||||
* @param isOpen
|
||||
*/
|
||||
setMenuOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Element {
|
||||
function NotificationMenu({ vm }: NotificationMenuProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const checkComponent = <CheckIcon width="24px" height="24px" color="var(--cpd-color-icon-primary)" />;
|
||||
|
||||
return (
|
||||
<div
|
||||
// We don't want keyboard navigation events to bubble up to the ListView changing the focused item
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={_t("room_list|notification_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={
|
||||
<IconButton
|
||||
size="24px"
|
||||
tooltip={_t("room_list|notification_options")}
|
||||
aria-label={_t("room_list|notification_options")}
|
||||
>
|
||||
{vm.isNotificationMute ? <NotificationOffIcon /> : <NotificationIcon />}
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
title={_t("room_list|notification_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<NotificationButton isRoomMuted={vm.isNotificationMute} size="24px" />}
|
||||
<div
|
||||
// We don't want keyboard navigation events to bubble up to the ListView changing the focused item
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessage}
|
||||
@@ -250,32 +234,7 @@ function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Eleme
|
||||
>
|
||||
{vm.isNotificationMute && checkComponent}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
interface NotificationButtonProps extends ComponentProps<typeof IconButton> {
|
||||
/**
|
||||
* Whether the room is muted.
|
||||
*/
|
||||
isRoomMuted: boolean;
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to trigger the notification menu.
|
||||
*/
|
||||
const NotificationButton = function MoreOptionsButton({
|
||||
isRoomMuted,
|
||||
ref,
|
||||
...props
|
||||
}: NotificationButtonProps): JSX.Element {
|
||||
return (
|
||||
<Tooltip label={_t("room_list|notification_options")}>
|
||||
<IconButton aria-label={_t("room_list|notification_options")} {...props} ref={ref}>
|
||||
{isRoomMuted ? <NotificationOffIcon /> : <NotificationIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { type JSX, memo, useEffect, useRef } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { Flex } from "@element-hq/web-shared-components";
|
||||
@@ -57,18 +57,6 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
}: RoomListItemViewProps): JSX.Element {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const vm = useRoomListItemViewModel(room);
|
||||
const [isHover, setHover] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
// The compound menu in RoomListItemMenuView needs to be rendered when the hover menu is shown
|
||||
// Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned
|
||||
const showHoverDecoration = isMenuOpen || isFocused || isHover;
|
||||
const showHoverMenu = showHoverDecoration && vm.showHoverMenu;
|
||||
|
||||
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);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
@@ -81,8 +69,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
as="button"
|
||||
ref={ref}
|
||||
className={classNames("mx_RoomListItemView", {
|
||||
mx_RoomListItemView_hover: showHoverDecoration,
|
||||
mx_RoomListItemView_menu_open: showHoverMenu,
|
||||
mx_RoomListItemView_has_menu: vm.showHoverMenu,
|
||||
mx_RoomListItemView_selected: isSelected,
|
||||
mx_RoomListItemView_bold: vm.isBold,
|
||||
})}
|
||||
@@ -96,8 +83,6 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
aria-label={vm.a11yLabel}
|
||||
onClick={() => vm.openRoom()}
|
||||
onFocus={(e: React.FocusEvent<HTMLButtonElement>) => onFocus(room, e)}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
tabIndex={isFocused ? 0 : -1}
|
||||
{...props}
|
||||
>
|
||||
@@ -119,44 +104,21 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showHoverMenu ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
|
||||
{vm.showHoverMenu && <RoomListItemMenuView className="mx_RoomListItemView_menu" room={room} />}
|
||||
|
||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||
{vm.showNotificationDecoration && (
|
||||
<NotificationDecoration
|
||||
className="mx_RoomListItemView_notificationDecoration"
|
||||
notificationState={vm.notificationState}
|
||||
aria-hidden={true}
|
||||
callType={vm.callType}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||
{vm.showNotificationDecoration && (
|
||||
<NotificationDecoration
|
||||
notificationState={vm.notificationState}
|
||||
aria-hidden={true}
|
||||
callType={vm.callType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
// Rendering multiple context menus can causes crashes in radix upstream,
|
||||
// See https://github.com/radix-ui/primitives/issues/2717.
|
||||
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>
|
||||
);
|
||||
return <RoomListItemContextMenuView room={room}>{content}</RoomListItemContextMenuView>;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user