Skip to content

Commit 2abd534

Browse files
authored
New room list: add search section (#29251)
* feat(new room list): move `RoomListView` to its own folder and add styling * feat(new room list): add search section * test(new room list): add tests for `RoomListSearch` * test(new room list): add tests for `RoomListView` * test(e2e): add method to close notification toast to `ElementAppPage` * test(e2e): add tests for the search section * test(e2e): add tests for the room list view * refactor: use Flex component * fix: loop icon size in search button * refactor: remove `focus_room_filter` listener
1 parent 85f80b1 commit 2abd534

File tree

18 files changed

+609
-21
lines changed

18 files changed

+609
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 { type Page } from "@playwright/test";
9+
10+
import { test, expect } from "../../../element-web-test";
11+
12+
test.describe("Search section of the room list", () => {
13+
test.use({
14+
labsFlags: ["feature_new_room_list"],
15+
});
16+
17+
/**
18+
* Get the search section of the room list
19+
* @param page
20+
*/
21+
function getSearchSection(page: Page) {
22+
return page.getByRole("search");
23+
}
24+
25+
test.beforeEach(async ({ page, app, user }) => {
26+
// The notification toast is displayed above the search section
27+
await app.closeNotificationToast();
28+
});
29+
30+
test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => {
31+
const searchSection = getSearchSection(page);
32+
// exact=false to ignore the shortcut which is related to the OS
33+
await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible();
34+
await expect(searchSection).toMatchScreenshot("search-section.png");
35+
});
36+
37+
test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => {
38+
const searchSection = getSearchSection(page);
39+
await searchSection.getByRole("button", { name: "Search", exact: false }).click();
40+
// The spotlight should be displayed
41+
await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible();
42+
});
43+
44+
test("should open the room directory when the search button is clicked", async ({ page, app, user }) => {
45+
const searchSection = getSearchSection(page);
46+
await searchSection.getByRole("button", { name: "Explore rooms" }).click();
47+
const dialog = page.getByRole("dialog", { name: "Search Dialog" });
48+
// The room directory should be displayed
49+
await expect(dialog).toBeVisible();
50+
// The public room filter should be displayed
51+
await expect(dialog.getByText("Public rooms")).toBeVisible();
52+
});
53+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 { type Page } from "@playwright/test";
9+
10+
import { test, expect } from "../../../element-web-test";
11+
12+
test.describe("Search section of the room list", () => {
13+
test.use({
14+
labsFlags: ["feature_new_room_list"],
15+
});
16+
17+
/**
18+
* Get the room list view
19+
* @param page
20+
*/
21+
function getRoomListView(page: Page) {
22+
return page.getByTestId("room-list-view");
23+
}
24+
25+
test.beforeEach(async ({ page, app, user }) => {
26+
// The notification toast is displayed above the search section
27+
await app.closeNotificationToast();
28+
});
29+
30+
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
31+
const roomListView = getRoomListView(page);
32+
await expect(roomListView).toMatchScreenshot("room-list-view.png");
33+
});
34+
});

playwright/e2e/settings/security-user-settings-tab.spec.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,9 @@ test.describe("Security user settings tab", () => {
2525
},
2626
});
2727

28-
test.beforeEach(async ({ page, user }) => {
28+
test.beforeEach(async ({ page, app, user }) => {
2929
// Dismiss "Notification" toast
30-
await page
31-
.locator(".mx_Toast_toast", { hasText: "Notifications" })
32-
.getByRole("button", { name: "Dismiss" })
33-
.click();
34-
30+
await app.closeNotificationToast();
3531
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
3632
});
3733

playwright/pages/ElementAppPage.ts

+11
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,15 @@ export class ElementAppPage {
202202
}
203203
return this.page.locator(`id=${labelledById ?? describedById}`);
204204
}
205+
206+
/**
207+
* Close the notification toast
208+
*/
209+
public closeNotificationToast(): Promise<void> {
210+
// Dismiss "Notification" toast
211+
return this.page
212+
.locator(".mx_Toast_toast", { hasText: "Notifications" })
213+
.getByRole("button", { name: "Dismiss" })
214+
.click();
215+
}
205216
}

res/css/_components.pcss

+2
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@
269269
@import "./views/right_panel/_VerificationPanel.pcss";
270270
@import "./views/right_panel/_WidgetCard.pcss";
271271
@import "./views/room_settings/_AliasSettings.pcss";
272+
@import "./views/rooms/RoomListView/_RoomListSearch.pcss";
273+
@import "./views/rooms/RoomListView/_RoomListView.pcss";
272274
@import "./views/rooms/_AppsDrawer.pcss";
273275
@import "./views/rooms/_Autocomplete.pcss";
274276
@import "./views/rooms/_AuxPanel.pcss";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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_RoomListSearch {
9+
/* From figma, this should be aligned with the room header */
10+
height: 64px;
11+
box-sizing: border-box;
12+
border-bottom: 1px solid var(--cpd-color-bg-subtle-primary);
13+
padding: 0 var(--cpd-space-3x);
14+
15+
svg {
16+
fill: var(--cpd-color-icon-secondary);
17+
}
18+
19+
.mx_RoomListSearch_search {
20+
/* The search button should take all the remaining space */
21+
flex: 1;
22+
font: var(--cpd-font-body-md-regular);
23+
color: var(--cpd-color-text-secondary);
24+
25+
span {
26+
flex: 1;
27+
28+
kbd {
29+
font-family: inherit;
30+
}
31+
}
32+
}
33+
34+
.mx_RoomListSearch_explore:hover {
35+
svg {
36+
fill: var(--cpd-color-icon-primary);
37+
}
38+
}
39+
}
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_RoomListView {
9+
background-color: var(--cpd-color-bg-canvas-default);
10+
height: 100%;
11+
border-right: 1px solid var(--cpd-color-bg-subtle-primary);
12+
}

src/components/structures/LeftPanel.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
390390
return (
391391
<div className={containerClasses}>
392392
<div className="mx_LeftPanel_roomListContainer">
393-
<RoomListView />
393+
<RoomListView activeSpace={this.state.activeSpace} />
394394
</div>
395395
</div>
396396
);

src/components/views/rooms/RoomListView.tsx

-14
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 { Button } from "@vector-im/compound-web";
10+
import ExploreIcon from "@vector-im/compound-design-tokens/assets/web/icons/explore";
11+
import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/search";
12+
13+
import { IS_MAC, Key } from "../../../../Keyboard";
14+
import { _t } from "../../../../languageHandler";
15+
import { ALTERNATE_KEY_NAME } from "../../../../accessibility/KeyboardShortcuts";
16+
import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
17+
import { UIComponent } from "../../../../settings/UIFeature";
18+
import { MetaSpace } from "../../../../stores/spaces";
19+
import { Action } from "../../../../dispatcher/actions";
20+
import PosthogTrackers from "../../../../PosthogTrackers";
21+
import defaultDispatcher from "../../../../dispatcher/dispatcher";
22+
import { Flex } from "../../../utils/Flex";
23+
24+
type RoomListSearchProps = {
25+
/**
26+
* Current active space
27+
* The explore button is only displayed in the Home meta space
28+
*/
29+
activeSpace: string;
30+
};
31+
32+
/**
33+
* A search component to be displayed at the top of the room list
34+
* The `Explore` button is displayed only in the Home meta space and when UIComponent.ExploreRooms is enabled.
35+
*/
36+
export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Element {
37+
const displayExploreButton = activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms);
38+
39+
return (
40+
<Flex className="mx_RoomListSearch" role="search" gap="var(--cpd-space-2x)" align="center">
41+
<Button
42+
className="mx_RoomListSearch_search"
43+
kind="secondary"
44+
size="sm"
45+
Icon={SearchIcon}
46+
onClick={() => defaultDispatcher.fire(Action.OpenSpotlight)}
47+
>
48+
<Flex as="span" justify="space-between">
49+
{_t("action|search")}
50+
<kbd>{IS_MAC ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K"}</kbd>
51+
</Flex>
52+
</Button>
53+
{displayExploreButton && (
54+
<Button
55+
className="mx_RoomListSearch_explore"
56+
kind="secondary"
57+
size="sm"
58+
Icon={ExploreIcon}
59+
iconOnly={true}
60+
aria-label={_t("action|explore_rooms")}
61+
onClick={(ev) => {
62+
defaultDispatcher.fire(Action.ViewRoomDirectory);
63+
PosthogTrackers.trackInteraction("WebLeftPanelExploreRoomsButton", ev);
64+
}}
65+
/>
66+
)}
67+
</Flex>
68+
);
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 from "react";
9+
10+
import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
11+
import { UIComponent } from "../../../../settings/UIFeature";
12+
import { RoomListSearch } from "./RoomListSearch";
13+
14+
type RoomListViewProps = {
15+
/**
16+
* Current active space
17+
* See {@link RoomListSearch}
18+
*/
19+
activeSpace: string;
20+
};
21+
22+
/**
23+
* A view component for the room list.
24+
*/
25+
export const RoomListView: React.FC<RoomListViewProps> = ({ activeSpace }) => {
26+
const displayRoomSearch = shouldShowComponent(UIComponent.FilterContainer);
27+
28+
return (
29+
<div className="mx_RoomListView" data-testid="room-list-view">
30+
{displayRoomSearch && <RoomListSearch activeSpace={activeSpace} />}
31+
</div>
32+
);
33+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
export { RoomListView } from "./RoomListView";

0 commit comments

Comments
 (0)