New room list: add selection decoration (#29531)

* fix(room list): remove 1px extra padding

* feat(room list): add selection decoration to room list item and scroll list to this element

* test(room list item): add is selected test

* test(room list): update snapshot

* test(e2e): add test to keep the room list item visible

* test(e2e): update snapshots
This commit is contained in:
Florian Duros 2025-03-19 09:39:12 +01:00 committed by GitHub
parent 35aed69604
commit 3587161a2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 106 additions and 12 deletions

View File

@ -77,4 +77,20 @@ test.describe("Room list", () => {
await page.getByRole("menuitem", { name: "leave room" }).click();
await expect(roomItem).not.toBeVisible();
});
test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
const filters = page.getByRole("listbox", { name: "Room list filters" });
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -7,9 +7,4 @@
.mx_RoomList {
height: 100%;
.mx_RoomList_List {
/* Avoid when on hover, the background color to be on top of the right border */
padding-right: 1px;
}
}

View File

@ -47,3 +47,7 @@
.mx_RoomListItemView_menu_open {
background-color: var(--cpd-color-bg-action-secondary-hovered);
}
.mx_RoomListItemView_selected {
background-color: var(--cpd-color-bg-action-secondary-pressed);
}

View File

@ -22,10 +22,12 @@ interface RoomListProps {
/**
* A virtualized list of rooms.
*/
export function RoomList({ vm: { rooms } }: RoomListProps): JSX.Element {
export function RoomList({ vm: { rooms, activeIndex } }: RoomListProps): JSX.Element {
const roomRendererMemoized = useCallback(
({ key, index, style }: ListRowProps) => <RoomListItemView room={rooms[index]} key={key} style={style} />,
[rooms],
({ key, index, style }: ListRowProps) => (
<RoomListItemView room={rooms[index]} key={key} style={style} isSelected={activeIndex === index} />
),
[rooms, activeIndex],
);
// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
@ -41,6 +43,7 @@ export function RoomList({ vm: { rooms } }: RoomListProps): JSX.Element {
rowHeight={48}
height={height}
width={width}
scrollToIndex={activeIndex}
/>
)}
</AutoSizer>

View File

@ -20,12 +20,16 @@ interface RoomListItemViewPropsProps extends React.HTMLAttributes<HTMLButtonElem
* The room to display
*/
room: Room;
/**
* Whether the room is selected
*/
isSelected: boolean;
}
/**
* An item in the room list
*/
export function RoomListItemView({ room, ...props }: RoomListItemViewPropsProps): JSX.Element {
export function RoomListItemView({ room, isSelected, ...props }: RoomListItemViewPropsProps): JSX.Element {
const vm = useRoomListItemViewModel(room);
const [isHover, setIsHover] = useState(false);
@ -38,8 +42,10 @@ export function RoomListItemView({ room, ...props }: RoomListItemViewPropsProps)
<button
className={classNames("mx_RoomListItemView", {
mx_RoomListItemView_menu_open: showHoverDecoration,
mx_RoomListItemView_selected: isSelected,
})}
type="button"
aria-selected={isSelected}
aria-label={_t("room_list|room|open_room", { roomName: room.name })}
onClick={() => vm.openRoom()}
onMouseOver={() => setIsHover(true)}

View File

@ -42,13 +42,13 @@ describe("<RoomListItemView />", () => {
test("should render a room item", () => {
const onClick = jest.fn();
const { asFragment } = render(<RoomListItemView room={room} onClick={onClick} />);
const { asFragment } = render(<RoomListItemView room={room} onClick={onClick} isSelected={false} />);
expect(asFragment()).toMatchSnapshot();
});
test("should call openRoom when clicked", async () => {
const user = userEvent.setup();
render(<RoomListItemView room={room} />);
render(<RoomListItemView room={room} isSelected={false} />);
await user.click(screen.getByRole("button", { name: `Open room ${room.name}` }));
expect(defaultValue.openRoom).toHaveBeenCalled();
@ -58,11 +58,20 @@ describe("<RoomListItemView />", () => {
mocked(useRoomListItemViewModel).mockReturnValue({ ...defaultValue, showHoverMenu: true });
const user = userEvent.setup();
render(<RoomListItemView room={room} />, withClientContextRenderOptions(matrixClient));
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
expect(screen.queryByRole("button", { name: "More Options" })).toBeNull();
await user.hover(listItem);
await waitFor(() => expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument());
});
test("should be selected if isSelected=true", async () => {
const { asFragment } = render(<RoomListItemView room={room} isSelected={true} />);
expect(screen.queryByRole("button", { name: `Open room ${room.name}` })).toHaveAttribute(
"aria-selected",
"true",
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -24,6 +24,7 @@ exports[`<RoomList /> should render a room list 1`] = `
>
<button
aria-label="Open room room0"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 0px; width: 100%;"
@ -70,6 +71,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room1"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 48px; width: 100%;"
@ -116,6 +118,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room2"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 96px; width: 100%;"
@ -162,6 +165,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room3"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 144px; width: 100%;"
@ -208,6 +212,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room4"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 192px; width: 100%;"
@ -254,6 +259,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room5"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 240px; width: 100%;"
@ -300,6 +306,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room6"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 288px; width: 100%;"
@ -346,6 +353,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room7"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 336px; width: 100%;"
@ -392,6 +400,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room8"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 384px; width: 100%;"
@ -438,6 +447,7 @@ exports[`<RoomList /> should render a room list 1`] = `
</button>
<button
aria-label="Open room room9"
aria-selected="false"
class="mx_RoomListItemView"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 432px; width: 100%;"

View File

@ -1,9 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
<DocumentFragment>
<button
aria-label="Open room room1"
aria-selected="true"
class="mx_RoomListItemView mx_RoomListItemView_selected"
type="button"
>
<div
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<span
title="room1"
>
room1
</span>
</div>
</div>
</button>
</DocumentFragment>
`;
exports[`<RoomListItemView /> should render a room item 1`] = `
<DocumentFragment>
<button
aria-label="Open room room1"
aria-selected="false"
class="mx_RoomListItemView"
type="button"
>