Room list: add collapse/expand all sections (#33318)

* chore: update compound design tokens

* feat(sc): add collapse/expand button to room list header

* feat: add new events to broadcast section state

* feat(vm): add expand/collpase event to room list events

* test: add e2e tests

* chore: fix company name in copyright

* chore: use two differant actions for collapse/expand

* Update apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* test: fix existing tests

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Florian Duros
2026-04-30 14:32:43 +00:00
committed by GitHub
co-authored by Michael Telatynski
parent a7ab72af11
commit c2e5aa7adc
16 changed files with 392 additions and 14 deletions
@@ -100,6 +100,7 @@
},
"appearance": "Appearance",
"chat_moved": "Chat moved",
"collapse_all_sections": "Collapse all sections",
"collapse_filters": "Collapse filter list",
"empty": {
"no_chats": "No chats yet",
@@ -118,6 +119,7 @@
"show_activity": "See all activity",
"show_chats": "Show all chats"
},
"expand_all_sections": "Expand all sections",
"expand_filters": "Expand filter list",
"filters": {
"favourite": "Favourites",
@@ -31,6 +31,7 @@ const RoomListHeaderViewWrapperImpl = ({
sort,
toggleMessagePreview,
createSection,
collapseOrExpandSections,
...rest
}: RoomListHeaderProps): JSX.Element => {
const vm = useMockedViewModel(rest, {
@@ -44,6 +45,7 @@ const RoomListHeaderViewWrapperImpl = ({
openSpacePreferences,
toggleMessagePreview,
createSection,
collapseOrExpandSections,
});
return <RoomListHeaderView vm={vm} />;
};
@@ -65,6 +67,7 @@ const meta = {
openSpacePreferences: fn(),
toggleMessagePreview: fn(),
createSection: fn(),
collapseOrExpandSections: fn(),
},
parameters: {
design: {
@@ -109,3 +112,9 @@ export const PlusIcon: Story = {
useComposeIcon: false,
},
};
export const CollapseSections: Story = {
args: {
collapseSections: "collapse",
},
};
@@ -12,7 +12,7 @@ import React from "react";
import * as stories from "./RoomListHeaderView.stories";
const { Default, NoComposeMenu, NoSpaceMenu } = composeStories(stories);
const { Default, NoComposeMenu, NoSpaceMenu, CollapseSections } = composeStories(stories);
describe("RoomListHeaderView", () => {
it("renders the default state", () => {
@@ -29,4 +29,11 @@ describe("RoomListHeaderView", () => {
const { container } = render(<NoSpaceMenu />);
expect(container).toMatchSnapshot();
});
it("should bind the collapse all sections action", () => {
const { getByRole } = render(<CollapseSections />);
const collapseButton = getByRole("button", { name: "Collapse all sections" });
collapseButton.click();
expect(CollapseSections.args?.collapseOrExpandSections).toHaveBeenCalled();
});
});
@@ -9,6 +9,7 @@ import React, { type JSX } from "react";
import { IconButton, H1 } from "@vector-im/compound-web";
import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose";
import PlusIcon from "@vector-im/compound-design-tokens/assets/web/icons/plus";
import CollapseAllIcon from "@vector-im/compound-design-tokens/assets/web/icons/collapse-all";
import { type ViewModel, useViewModel } from "../../core/viewmodel";
import { Flex } from "../../core/utils/Flex";
@@ -21,6 +22,11 @@ import styles from "./RoomListHeaderView.module.css";
*/
export type SortOption = "recent" | "alphabetical" | "unread-first";
/**
* The available options for collapsing sections in the room list.
*/
export type CollapseSectionsOption = "collapse" | "expand";
export interface RoomListHeaderViewSnapshot {
/**
* The title of the room list
@@ -68,6 +74,12 @@ export interface RoomListHeaderViewSnapshot {
* Whether to use the compose icon instead of the create icon.
*/
useComposeIcon: boolean;
/**
* If "collapse", an icon to collapse all sections is shown.
* If "expand", an icon to expand all sections is shown.
* If undefined, no icon are shown.
*/
collapseSections?: CollapseSectionsOption;
}
export interface RoomListHeaderViewActions {
@@ -111,6 +123,10 @@ export interface RoomListHeaderViewActions {
* Create a new section in the room list.
*/
createSection: () => void;
/**
* Collapse or expand all sections in the room list depending on the current state.
*/
collapseOrExpandSections: () => void;
}
/**
@@ -136,7 +152,7 @@ interface RoomListHeaderViewProps {
*/
export function RoomListHeaderView({ vm }: Readonly<RoomListHeaderViewProps>): JSX.Element {
const { translate: _t } = useI18n();
const { title, displaySpaceMenu, displayComposeMenu, useComposeIcon } = useViewModel(vm);
const { title, displaySpaceMenu, displayComposeMenu, useComposeIcon, collapseSections } = useViewModel(vm);
return (
<Flex
@@ -155,6 +171,20 @@ export function RoomListHeaderView({ vm }: Readonly<RoomListHeaderViewProps>): J
</Flex>
<Flex align="center" gap="var(--cpd-space-2x)">
<OptionMenuView vm={vm} />
{collapseSections && (
<IconButton
size="28px"
style={{ padding: "4px" }}
onClick={() => vm.collapseOrExpandSections()}
tooltip={
collapseSections === "collapse"
? _t("room_list|collapse_all_sections")
: _t("room_list|expand_all_sections")
}
>
<CollapseAllIcon color="var(--cpd-color-icon-secondary)" aria-hidden />
</IconButton>
)}
{/* If we don't display the compose menu, it means that the user can only send DM */}
{displayComposeMenu ? (
@@ -10,5 +10,6 @@ export type {
RoomListHeaderViewSnapshot,
RoomListHeaderViewActions,
SortOption,
CollapseSectionsOption,
} from "./RoomListHeaderView";
export { RoomListHeaderView } from "./RoomListHeaderView";
@@ -24,6 +24,7 @@ export class MockedViewModel extends MockViewModel<RoomListHeaderViewSnapshot> i
public openSpacePreferences = vi.fn<() => void>();
public toggleMessagePreview = vi.fn<() => void>();
public createSection = vi.fn<() => void>();
public collapseOrExpandSections = vi.fn<() => void>();
}
export { defaultSnapshot } from "./default-snapshot";