Skip to content

Commit 20d8abf

Browse files
authored
New room list: add primary filters (#29481)
* feat(room filter): add component for the primary filters * feat(room filter): add filter component to room list view * test(room filter): add tests to primary filters * test: update snapshots * test(e2e): update snapshots * test(e2e): add tests for primary filters * refactor: change aria-label of primary filters
1 parent fda6581 commit 20d8abf

File tree

14 files changed

+354
-2
lines changed

14 files changed

+354
-2
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { expect, test } from "../../../element-web-test";
9+
import type { Page } from "@playwright/test";
10+
11+
test.describe("Room list filters and sort", () => {
12+
test.use({
13+
displayName: "Alice",
14+
botCreateOpts: {
15+
displayName: "BotBob",
16+
autoAcceptInvites: true,
17+
},
18+
labsFlags: ["feature_new_room_list"],
19+
});
20+
21+
/**
22+
* Get the room list
23+
* @param page
24+
*/
25+
function getRoomList(page: Page) {
26+
return page.getByTestId("room-list");
27+
}
28+
29+
function getPrimaryFilters(page: Page) {
30+
return page.getByRole("listbox", { name: "Room list filters" });
31+
}
32+
33+
test.beforeEach(async ({ page, app, bot, user }) => {
34+
// The notification toast is displayed above the search section
35+
await app.closeNotificationToast();
36+
37+
await app.client.createRoom({ name: "empty room" });
38+
39+
const unReadDmId = await bot.createRoom({
40+
name: "unread dm",
41+
invite: [user.userId],
42+
is_direct: true,
43+
});
44+
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
45+
46+
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
47+
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
48+
await bot.joinRoom(unReadRoomId);
49+
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
50+
51+
const favouriteId = await app.client.createRoom({ name: "favourite room" });
52+
await app.client.evaluate(async (client, favouriteId) => {
53+
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
54+
}, favouriteId);
55+
});
56+
57+
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
58+
const roomList = getRoomList(page);
59+
const primaryFilters = getPrimaryFilters(page);
60+
61+
const allFilters = await primaryFilters.locator("option").all();
62+
for (const filter of allFilters) {
63+
expect(await filter.getAttribute("aria-selected")).toBe("false");
64+
}
65+
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
66+
67+
await primaryFilters.getByRole("option", { name: "Unread" }).click();
68+
// only one room should be visible
69+
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
70+
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
71+
expect(await roomList.locator("role=gridcell").count()).toBe(2);
72+
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
73+
74+
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
75+
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
76+
expect(await roomList.locator("role=gridcell").count()).toBe(1);
77+
78+
await primaryFilters.getByRole("option", { name: "People" }).click();
79+
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
80+
expect(await roomList.locator("role=gridcell").count()).toBe(1);
81+
82+
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
83+
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
84+
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
85+
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
86+
expect(await roomList.locator("role=gridcell").count()).toBe(3);
87+
});
88+
});
Loading
Loading

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@
273273
@import "./views/rooms/RoomListPanel/_RoomListCell.pcss";
274274
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
275275
@import "./views/rooms/RoomListPanel/_RoomListPanel.pcss";
276+
@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss";
276277
@import "./views/rooms/RoomListPanel/_RoomListSearch.pcss";
277278
@import "./views/rooms/_AppsDrawer.pcss";
278279
@import "./views/rooms/_Autocomplete.pcss";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
.mx_RoomListPrimaryFilters {
9+
margin: unset;
10+
list-style-type: none;
11+
padding: var(--cpd-space-2x) var(--cpd-space-3x);
12+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { type JSX } from "react";
9+
import { ChatFilter } from "@vector-im/compound-web";
10+
11+
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
12+
import { Flex } from "../../../utils/Flex";
13+
import { _t } from "../../../../languageHandler";
14+
15+
interface RoomListPrimaryFiltersProps {
16+
/**
17+
* The view model for the room list
18+
*/
19+
vm: RoomListViewState;
20+
}
21+
22+
/**
23+
* The primary filters for the room list
24+
*/
25+
export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX.Element {
26+
return (
27+
<Flex
28+
as="ul"
29+
role="listbox"
30+
aria-label={_t("room_list|primary_filters")}
31+
className="mx_RoomListPrimaryFilters"
32+
align="center"
33+
gap="var(--cpd-space-2x)"
34+
wrap="wrap"
35+
>
36+
{vm.primaryFilters.map((filter) => (
37+
<li role="option" aria-selected={filter.active} key={filter.name}>
38+
<ChatFilter selected={filter.active} onClick={filter.toggle}>
39+
{filter.name}
40+
</ChatFilter>
41+
</li>
42+
))}
43+
</Flex>
44+
);
45+
}

src/components/views/rooms/RoomListPanel/RoomListView.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ import React, { type JSX } from "react";
99

1010
import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel";
1111
import { RoomList } from "./RoomList";
12+
import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
1213

1314
/**
1415
* Host the room list and the (future) room filters
1516
*/
1617
export function RoomListView(): JSX.Element {
1718
const vm = useRoomListViewModel();
18-
// Room filters will be added soon
19-
return <RoomList vm={vm} />;
19+
return (
20+
<>
21+
<RoomListPrimaryFilters vm={vm} />
22+
<RoomList vm={vm} />
23+
</>
24+
);
2025
}

0 commit comments

Comments
 (0)