Implement collapsible panels for the new room list (#32742)

* Add `react-resizable-panels` library

* Implement a custom SeparatorView

* Add a `LeftResizablePanelView`

* Add a custom `GroupView`

* Export everything from shared-components

* Make it possible to track width/collapse state through settings

* Add a view model to drive the views

* Render views without disrupting the old room list

* Fix lint error

* Disable user interaction on collapsed panel

* Prevent widgets fron hijacking pointer events

* Expand to full width on separator click

* Separator should be shown when focused via keyboard

* Update tests

* Use data-attribute for hover

* Write stories for SeperatorView

* Write vite tests for SeparatorView

* Write tests for LeftResizablePanelView

* More tests

* Fix lint errors

* Fix flakey border on the roomlst

* Fix storybook axe violation

* Update snapshots

* Fix playwright tests

* Fix sonarcloud issues

* Use translated string

* Add better js-doc comments

* Rename `ResizerSnapshot` to `ResizerViewSnapshot`

* Externalize react-resizable-panels

* Link figma designs to stories

* Write playwright tests

* Update screenshots

* Fix lint errors

* Update more screenshots

* Update more screenshots

* Fix flaky toast test

* Update apps/web/playwright/e2e/crypto/toasts.spec.ts

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>

* Fix indentation

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
R Midhun Suresh
2026-03-23 13:33:32 +00:00
committed by GitHub
co-authored by Andy Balaam
parent 89261bbe00
commit 99e6ede9f1
197 changed files with 1592 additions and 40 deletions
@@ -22,6 +22,7 @@ import {
} from "matrix-js-sdk/src/matrix";
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import classNames from "classnames";
import { GroupView, SeparatorView, Panel, LeftResizablePanelView } from "@element-hq/web-shared-components";
import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard";
import PageTypes from "../../PageTypes";
@@ -70,6 +71,7 @@ import { MatrixClientContextProvider } from "./MatrixClientContextProvider";
import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation";
import { ModuleApi } from "../../modules/Api.ts";
import { SDKContext } from "../../contexts/SDKContext.ts";
import { ResizerViewModel } from "../../viewmodels/structures/ResizerViewModel.ts";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -136,6 +138,8 @@ class LoggedInView extends React.Component<IProps, IState> {
protected timezoneProfileUpdateRef?: string[];
protected resizer?: Resizer<ICollapseConfig, CollapseItem>;
private resizerViewModel?: ResizerViewModel;
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
@@ -198,6 +202,8 @@ class LoggedInView extends React.Component<IProps, IState> {
OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
this.refreshBackgroundImage();
this.resizerViewModel = new ResizerViewModel();
}
/**
@@ -258,6 +264,7 @@ class LoggedInView extends React.Component<IProps, IState> {
SettingsStore.unwatchSetting(this.backgroundImageWatcherRef);
this.timezoneProfileUpdateRef?.forEach((s) => SettingsStore.unwatchSetting(s));
this.resizer?.detach();
this.resizerViewModel?.dispose();
}
private onCallState = (): void => {
@@ -760,6 +767,58 @@ class LoggedInView extends React.Component<IProps, IState> {
});
const shouldUseMinimizedUI = !useNewRoomList && this.props.collapseLhs;
const leftPanel = (
<div className="mx_LeftPanel_outerWrapper">
<LeftPanelLiveShareWarning isMinimized={shouldUseMinimizedUI || false} />
<div className={leftPanelWrapperClasses}>
{!useNewRoomList && (
<BackdropPanel blurMultiplier={0.5} backgroundImage={this.state.backgroundImage} />
)}
{!useNewRoomList && <SpacePanel />}
{!useNewRoomList && <BackdropPanel backgroundImage={this.state.backgroundImage} />}
{!moduleRenderer && (
<div
className="mx_LeftPanel_wrapper--user"
ref={this._resizeContainer}
data-collapsed={shouldUseMinimizedUI ? true : undefined}
>
<LeftPanel
pageType={this.props.page_type as PageTypes}
isMinimized={shouldUseMinimizedUI || false}
resizeNotifier={this.context.resizeNotifier}
/>
</div>
)}
</div>
</div>
);
const roomView = <div className="mx_RoomView_wrapper">{pageElement}</div>;
const content =
useNewRoomList && this.resizerViewModel ? (
<GroupView vm={this.resizerViewModel}>
<SpacePanel />
<LeftResizablePanelView
vm={this.resizerViewModel}
className="mx_LeftPanel_panel"
minSize="200px"
maxSize="370px"
defaultSize="370px"
>
{leftPanel}
</LeftResizablePanelView>
<SeparatorView className="mx_Separator" vm={this.resizerViewModel} />
<Panel className="mx_LeftPanel_panel">{roomView}</Panel>
</GroupView>
) : (
<>
{leftPanel}
{!moduleRenderer && <ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />}
{roomView}
</>
);
return (
<MatrixClientContextProvider client={this._matrixClient}>
<div
@@ -769,33 +828,7 @@ class LoggedInView extends React.Component<IProps, IState> {
aria-hidden={this.props.hideToSRUsers}
>
<ToastContainer />
<div className={bodyClasses}>
<div className="mx_LeftPanel_outerWrapper">
<LeftPanelLiveShareWarning isMinimized={shouldUseMinimizedUI || false} />
<div className={leftPanelWrapperClasses}>
{!useNewRoomList && (
<BackdropPanel blurMultiplier={0.5} backgroundImage={this.state.backgroundImage} />
)}
<SpacePanel />
{!useNewRoomList && <BackdropPanel backgroundImage={this.state.backgroundImage} />}
{!moduleRenderer && (
<div
className="mx_LeftPanel_wrapper--user"
ref={this._resizeContainer}
data-collapsed={shouldUseMinimizedUI ? true : undefined}
>
<LeftPanel
pageType={this.props.page_type as PageTypes}
isMinimized={shouldUseMinimizedUI || false}
resizeNotifier={this.context.resizeNotifier}
/>
</div>
)}
</div>
</div>
{!moduleRenderer && <ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />}
<div className="mx_RoomView_wrapper">{pageElement}</div>
</div>
<div className={bodyClasses}>{content}</div>
</div>
<PipContainer />
<NonUrgentToastContainer />