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:
co-authored by
Andy Balaam
parent
89261bbe00
commit
99e6ede9f1
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 {
|
||||
BaseViewModel,
|
||||
type LeftResizablePanelViewActions,
|
||||
type SeparatorViewActions,
|
||||
type PanelSize,
|
||||
type PanelImperativeHandle,
|
||||
type GroupViewActions,
|
||||
type ResizerViewSnapshot,
|
||||
} from "@element-hq/web-shared-components";
|
||||
import { debounce } from "lodash";
|
||||
import whatInput from "what-input";
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
|
||||
function getInitialState(): ResizerViewSnapshot {
|
||||
if (SettingsStore.getValue("RoomList.isPanelCollapsed")) {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
initialSize: 0,
|
||||
isFocusedViaKeyboard: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
isCollapsed: false,
|
||||
initialSize: SettingsStore.getValue("RoomList.panelSize") ?? undefined,
|
||||
isFocusedViaKeyboard: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewmodel that drives the resizable left panel.
|
||||
*/
|
||||
export class ResizerViewModel
|
||||
extends BaseViewModel<ResizerViewSnapshot, void>
|
||||
implements SeparatorViewActions, LeftResizablePanelViewActions, GroupViewActions
|
||||
{
|
||||
/**
|
||||
* This object gives us access to the API methods of react-resizable-panels library.
|
||||
*/
|
||||
private panelHandle?: PanelImperativeHandle;
|
||||
|
||||
public constructor() {
|
||||
super(undefined, getInitialState());
|
||||
}
|
||||
|
||||
public onLeftPanelResize = debounce((panelSize: PanelSize): void => {
|
||||
const newSize = panelSize.inPixels;
|
||||
this.snapshot.merge({ isCollapsed: newSize === 0 });
|
||||
}, 50);
|
||||
|
||||
public onLeftPanelResized = (newSize: number): void => {
|
||||
const isCollapsed = newSize === 0;
|
||||
// Store the size if the panel isn't collapsed.
|
||||
if (!isCollapsed) {
|
||||
SettingsStore.setValue("RoomList.panelSize", null, SettingLevel.DEVICE, newSize);
|
||||
}
|
||||
// Store whether the panel was collapsed.
|
||||
// This is stored separately instead of being inferred from the stored panel size so that
|
||||
// the panel can be restored to its last known non-zero width even after app reload, which
|
||||
// we wouldn't be able to do if we stored panelSize as zero.
|
||||
SettingsStore.setValue("RoomList.isPanelCollapsed", null, SettingLevel.DEVICE, isCollapsed);
|
||||
};
|
||||
|
||||
public setPanelHandle = (handle: PanelImperativeHandle): void => {
|
||||
this.panelHandle = handle;
|
||||
};
|
||||
|
||||
public onSeparatorClick = (): void => {
|
||||
if (this.panelHandle?.isCollapsed()) {
|
||||
this.panelHandle.resize(`100%`);
|
||||
}
|
||||
};
|
||||
|
||||
public onFocus = (): void => {
|
||||
/**
|
||||
* The intention here is to make the separator visible when it is focused by keyboard
|
||||
* navigation i.e tabbing through the app.
|
||||
*
|
||||
* There's a good reason to take this approach instead of just relying on the focus-visible
|
||||
* selector:
|
||||
* When exactly an element gets focus-visible is determined by browser heuristics and usually
|
||||
* interacting with the mouse will not give an element focus-visible.
|
||||
* However with this separator on chrome, mouse interaction occasionally gives it focus-visible.
|
||||
* The leads to flakey separator behaviour.
|
||||
*/
|
||||
const currentNavigation = whatInput.ask();
|
||||
if (currentNavigation === "keyboard") {
|
||||
this.snapshot.merge({ isFocusedViaKeyboard: true });
|
||||
}
|
||||
};
|
||||
|
||||
public onBlur = (): void => {
|
||||
if (this.getSnapshot().isFocusedViaKeyboard) this.snapshot.merge({ isFocusedViaKeyboard: false });
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user