mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-04 19:56:45 +02:00
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:
parent
a7ab72af11
commit
c2e5aa7adc
@ -258,6 +258,47 @@ test.describe("Room list custom sections", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Collapse and expand all sections", () => {
|
||||
test("should collapse all sections when 'Collapse all sections' button is clicked", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
await createCustomSection(page, "Work");
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
const header = getRoomListHeader(page);
|
||||
|
||||
await expect(getSectionHeader(page, "Chats")).toBeVisible();
|
||||
await expect(getSectionHeader(page, "Work")).toBeVisible();
|
||||
|
||||
const collapseButton = header.getByRole("button", { name: "Collapse all sections" });
|
||||
await expect(collapseButton).toBeVisible();
|
||||
|
||||
await expect(roomList.getByRole("row", { name: "Open room my room" })).toBeVisible();
|
||||
|
||||
await collapseButton.click();
|
||||
|
||||
await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "false");
|
||||
});
|
||||
|
||||
test("should expand all sections when 'Expand all sections' button is clicked", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
await createCustomSection(page, "Work");
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
const header = getRoomListHeader(page);
|
||||
|
||||
await expect(getSectionHeader(page, "Chats")).toBeVisible();
|
||||
|
||||
await header.getByRole("button", { name: "Collapse all sections" }).click();
|
||||
await expect(roomList.getByRole("row", { name: "Open room my room" })).not.toBeVisible();
|
||||
|
||||
await header.getByRole("button", { name: "Expand all sections" }).click();
|
||||
|
||||
await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Adding a room to a custom section", () => {
|
||||
test("should add a room to a custom section via the More Options menu", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
|
||||
@ -403,4 +403,20 @@ export enum Action {
|
||||
* or keyboard event).
|
||||
*/
|
||||
UserActivity = "user_activity",
|
||||
|
||||
/**
|
||||
* Fired to request collapsing all room list sections.
|
||||
*/
|
||||
RoomListCollapseAllSections = "room_list_collapse_all_sections",
|
||||
|
||||
/**
|
||||
* Fired to request expanding all room list sections.
|
||||
*/
|
||||
RoomListExpandAllSections = "room_list_expand_all_sections",
|
||||
|
||||
/**
|
||||
* Fired to report the collapse state of a given room list section.
|
||||
* Payload: {@link RoomListSectionsCollapseStateChangedPayload}
|
||||
*/
|
||||
RoomListSectionsCollapseStateChanged = "room_list_sections_collapse_state_changed",
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type CollapseSectionsOption } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type ActionPayload } from "../payloads";
|
||||
import { type Action } from "../actions";
|
||||
|
||||
export interface RoomListSectionsCollapseStateChangedPayload extends ActionPayload {
|
||||
action: Action.RoomListSectionsCollapseStateChanged;
|
||||
/**
|
||||
* The new collapse state for the room list sections.
|
||||
* If undefined, the feature is disabled.
|
||||
*/
|
||||
collapseSections?: CollapseSectionsOption;
|
||||
}
|
||||
@ -26,6 +26,7 @@ import {
|
||||
showSpaceSettings,
|
||||
} from "../../utils/space";
|
||||
import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import type { RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
||||
import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters";
|
||||
@ -77,6 +78,10 @@ export class RoomListHeaderViewModel
|
||||
if (this.activeSpace) {
|
||||
this.disposables.trackListener(this.activeSpace, RoomEvent.Name, this.onSpaceNameChange);
|
||||
}
|
||||
|
||||
// Listen for section collapse state changes from RoomListViewModel
|
||||
const dispatcherRef = defaultDispatcher.register(this.onDispatch);
|
||||
this.disposables.track(() => defaultDispatcher.unregister(dispatcherRef));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -203,6 +208,23 @@ export class RoomListHeaderViewModel
|
||||
public createSection = (): void => {
|
||||
RoomListStoreV3.instance.createSection();
|
||||
};
|
||||
|
||||
public collapseOrExpandSections = (): void => {
|
||||
const action =
|
||||
this.snapshot.current.collapseSections === "expand"
|
||||
? Action.RoomListExpandAllSections
|
||||
: Action.RoomListCollapseAllSections;
|
||||
defaultDispatcher.fire(action);
|
||||
};
|
||||
|
||||
private readonly onDispatch = (payload: { action: string }): void => {
|
||||
if (payload.action === Action.RoomListSectionsCollapseStateChanged) {
|
||||
const { collapseSections } = payload as RoomListSectionsCollapseStateChangedPayload;
|
||||
this.snapshot.merge({
|
||||
collapseSections: collapseSections && (collapseSections === "collapse" ? "expand" : "collapse"),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get the initial snapshot for the RoomListHeaderViewModel.
|
||||
|
||||
@ -21,6 +21,7 @@ import { Action } from "../../dispatcher/actions";
|
||||
import dispatcher from "../../dispatcher/dispatcher";
|
||||
import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { type RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import RoomListStoreV3, {
|
||||
CHATS_TAG,
|
||||
@ -325,9 +326,24 @@ export class RoomListViewModel
|
||||
// Handle keyboard navigation shortcuts (Alt+ArrowUp/Down)
|
||||
// This was previously handled by useRoomListNavigation hook
|
||||
this.handleViewRoomDelta(payload as ViewRoomDeltaPayload);
|
||||
} else if (payload.action === Action.RoomListCollapseAllSections) {
|
||||
this.onCollapseAllSections(false);
|
||||
} else if (payload.action === Action.RoomListExpandAllSections) {
|
||||
this.onCollapseAllSections(true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the collapse or expansion of all sections in the room list.
|
||||
* @param expand - Whether to expand or collapse all sections
|
||||
*/
|
||||
private onCollapseAllSections(expand: boolean): void {
|
||||
for (const sectionHeaderVM of this.roomSectionHeaderViewModels.values()) {
|
||||
sectionHeaderVM.isExpanded = expand;
|
||||
}
|
||||
this.updateRoomListData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard navigation shortcuts (Alt+ArrowUp/Down) to move between rooms.
|
||||
* Supports both regular navigation and unread-only navigation.
|
||||
@ -581,6 +597,32 @@ export class RoomListViewModel
|
||||
sections: keepIfSame(previousSections, viewSections),
|
||||
isFlatList,
|
||||
});
|
||||
|
||||
this.notifyCollapseState(isFlatList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the dispatcher about the current collapse state of the room list sections.
|
||||
* @param isFlatList - Whether the room list is currently displayed as a flat list
|
||||
*/
|
||||
private notifyCollapseState(isFlatList: boolean): void {
|
||||
// Hide collapse/expand all button if sections are disabled or if it's a flat list
|
||||
if (!SettingsStore.getValue("feature_room_list_sections") || isFlatList) {
|
||||
dispatcher.dispatch<RoomListSectionsCollapseStateChangedPayload>({
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if all sections are currently collapsed
|
||||
const allCollapsed = this.snapshot.current.sections.every(
|
||||
({ id }) => !(this.roomSectionHeaderViewModels.get(id)?.isExpanded ?? true),
|
||||
);
|
||||
dispatcher.dispatch<RoomListSectionsCollapseStateChangedPayload>({
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: allCollapsed ? "collapse" : "expand",
|
||||
});
|
||||
}
|
||||
|
||||
public createChatRoom = (): void => {
|
||||
|
||||
@ -323,6 +323,89 @@ describe("RoomListHeaderViewModel", () => {
|
||||
expect(createSectionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("collapseOrExpandSections", () => {
|
||||
it("should dispatch RoomListCollapseAllSections when collapseSections is not 'expand'", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
vm.collapseOrExpandSections();
|
||||
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.RoomListCollapseAllSections);
|
||||
});
|
||||
|
||||
it("should dispatch RoomListExpandAllSections when collapseSections is 'expand'", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
// Drive the VM into the "expand" state by simulating all sections collapsed
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "collapse",
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(vm.getSnapshot().collapseSections).toBe("expand");
|
||||
vm.collapseOrExpandSections();
|
||||
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.RoomListExpandAllSections);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RoomListSectionsCollapseStateChanged handling", () => {
|
||||
it("should set collapseSections to 'expand' when collapseSections is collapse", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "collapse",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().collapseSections).toBe("expand");
|
||||
});
|
||||
|
||||
it("should set collapseSections to 'collapse' when collapseSections is expand", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "expand",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().collapseSections).toBe("collapse");
|
||||
});
|
||||
|
||||
it("should set collapseSections to undefined when collapseSections is undefined", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
// First drive it into a non-undefined state
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "collapse",
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(vm.getSnapshot().collapseSections).toBe("expand");
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: undefined,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().collapseSections).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should toggle message preview from enabled to disabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.showMessagePreview") return true;
|
||||
|
||||
@ -465,6 +465,20 @@ describe("RoomListViewModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifyCollapseState", () => {
|
||||
it("should dispatch collapseSections=undefined when feature_room_list_sections is disabled", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Keyboard navigation (ViewRoomDelta)", () => {
|
||||
beforeEach(() => {
|
||||
// stubClient sets up MatrixClientPeg which is needed when ViewRoom action is dispatched
|
||||
@ -971,6 +985,96 @@ describe("RoomListViewModel", () => {
|
||||
expect(favSection!.roomIds).toEqual(["!fav1:server"]);
|
||||
});
|
||||
|
||||
describe("Collapse/expand all sections", () => {
|
||||
it("should collapse all sections when Action.RoomListCollapseAllSections is dispatched", async () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
const chatsHeader = viewModel.getSectionHeaderViewModel(CHATS_TAG);
|
||||
expect(favHeader.isExpanded).toBe(true);
|
||||
|
||||
dispatcher.dispatch({ action: Action.RoomListCollapseAllSections });
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
expect(favHeader.isExpanded).toBe(false);
|
||||
expect(chatsHeader.isExpanded).toBe(false);
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.sections.find((s) => s.id === DefaultTagID.Favourite)!.roomIds).toEqual([]);
|
||||
expect(snapshot.sections.find((s) => s.id === CHATS_TAG)!.roomIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("should expand all sections when Action.RoomListExpandAllSections is dispatched", async () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Collapse first
|
||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
favHeader.onClick();
|
||||
expect(favHeader.isExpanded).toBe(false);
|
||||
|
||||
dispatcher.dispatch({ action: Action.RoomListExpandAllSections });
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
expect(favHeader.isExpanded).toBe(true);
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.sections.find((s) => s.id === DefaultTagID.Favourite)!.roomIds).toEqual([
|
||||
"!fav1:server",
|
||||
"!fav2:server",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifyCollapseState", () => {
|
||||
it("should dispatch collapseSections=expand when all sections are expanded (default)", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "expand",
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch collapseSection=collapse when all sections are collapsed", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Collapse all sections
|
||||
viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite).isExpanded = false;
|
||||
viewModel.getSectionHeaderViewModel(CHATS_TAG).isExpanded = false;
|
||||
viewModel.getSectionHeaderViewModel(DefaultTagID.LowPriority).isExpanded = false;
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: "collapse",
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch collapseSection=undefined when it is a flat list", () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [] },
|
||||
{ tag: CHATS_TAG, rooms: [regularRoom1] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [] },
|
||||
],
|
||||
});
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.RoomListSectionsCollapseStateChanged,
|
||||
collapseSections: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should apply sticky room within the correct section", async () => {
|
||||
stubClient();
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@ -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";
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -13,8 +13,8 @@ catalogs:
|
||||
specifier: 1.59.1
|
||||
version: 1.59.1
|
||||
'@vector-im/compound-design-tokens':
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0
|
||||
specifier: 10.1.1
|
||||
version: 10.1.1
|
||||
'@vector-im/compound-web':
|
||||
specifier: 9.2.1
|
||||
version: 9.2.1
|
||||
@ -374,10 +374,10 @@ importers:
|
||||
version: 1.0.2
|
||||
'@vector-im/compound-design-tokens':
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.0(@types/react@19.2.14)(react@19.2.5)
|
||||
version: 10.1.1(@types/react@19.2.14)(react@19.2.5)
|
||||
'@vector-im/compound-web':
|
||||
specifier: 'catalog:'
|
||||
version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@vector-im/matrix-wysiwyg':
|
||||
specifier: 2.40.0
|
||||
version: 2.40.0(patch_hash=7bdf6150f2905bc2f055a6bcaa7b9d78fa7ffde82e800bcc454ac7b0096bd65e)(react@19.2.5)
|
||||
@ -1043,7 +1043,7 @@ importers:
|
||||
version: 1.16.0
|
||||
'@vector-im/compound-design-tokens':
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.0(@types/react@19.2.14)(react@19.2.5)
|
||||
version: 10.1.1(@types/react@19.2.14)(react@19.2.5)
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
@ -1158,7 +1158,7 @@ importers:
|
||||
version: 8.58.2(eslint@8.57.1)(typescript@6.0.3)
|
||||
'@vector-im/compound-web':
|
||||
specifier: 'catalog:'
|
||||
version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@vitest/browser-playwright':
|
||||
specifier: ^4.0.17
|
||||
version: 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@18.19.130)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.10))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.5)
|
||||
@ -5904,8 +5904,8 @@ packages:
|
||||
'@upsetjs/venn.js@2.0.0':
|
||||
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
|
||||
|
||||
'@vector-im/compound-design-tokens@10.1.0':
|
||||
resolution: {integrity: sha512-o+7DGx+NygpT2NPE1Jo//7NZDuyjzRH06eRchS0ZlkJicKx/impEmShmHU/XiE4P84BIFOo9eZ1Ws+rAym6Tuw==}
|
||||
'@vector-im/compound-design-tokens@10.1.1':
|
||||
resolution: {integrity: sha512-f2rdTilbPeOjrX7Mh9iTPcp5VergY7JLLWzKVjwMvpT0wtoFKwn59D1hwX2QInpiG70QTCxEdQFYLxQKvJQ74Q==}
|
||||
peerDependencies:
|
||||
'@types/react': ^19.2.10
|
||||
react: ^17 || ^18 || ^19.0.0
|
||||
@ -18677,12 +18677,12 @@ snapshots:
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
'@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5)':
|
||||
'@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5)':
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
react: 19.2.5
|
||||
|
||||
'@vector-im/compound-web@9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
'@vector-im/compound-web@9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.27.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@fontsource/inconsolata': 5.2.8
|
||||
@ -18693,7 +18693,7 @@ snapshots:
|
||||
'@radix-ui/react-progress': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@radix-ui/react-separator': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5)
|
||||
'@vector-im/compound-design-tokens': 10.1.0(@types/react@19.2.14)(react@19.2.5)
|
||||
'@vector-im/compound-design-tokens': 10.1.1(@types/react@19.2.14)(react@19.2.5)
|
||||
classnames: 2.5.1
|
||||
react: 19.2.5
|
||||
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
|
||||
@ -16,7 +16,7 @@ catalog:
|
||||
"@playwright/test": 1.59.1
|
||||
"playwright-core": 1.59.1
|
||||
# Compound
|
||||
"@vector-im/compound-design-tokens": 10.1.0
|
||||
"@vector-im/compound-design-tokens": 10.1.1
|
||||
"@vector-im/compound-web": 9.2.1
|
||||
# i18n
|
||||
matrix-web-i18n: 3.6.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user