From 8a6ef20865a85118d378945428d2989c1e43d944 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Sun, 29 Mar 2026 19:38:13 +0530 Subject: [PATCH] Add a class to track auto collapse state - An object of this class can be used by all the behaviours to collapse/expand the panel - Can also query whether the panel is already collapsed --- .../auto-collapse/CollapseHandler.ts | 72 +++++++++++++++++++ .../auto-collapse/CollapseHandler-test.ts | 55 ++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 apps/web/src/viewmodels/structures/auto-collapse/CollapseHandler.ts create mode 100644 apps/web/test/viewmodels/structures/auto-collapse/CollapseHandler-test.ts diff --git a/apps/web/src/viewmodels/structures/auto-collapse/CollapseHandler.ts b/apps/web/src/viewmodels/structures/auto-collapse/CollapseHandler.ts new file mode 100644 index 0000000000..936696aed7 --- /dev/null +++ b/apps/web/src/viewmodels/structures/auto-collapse/CollapseHandler.ts @@ -0,0 +1,72 @@ +/* + * 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 { PanelImperativeHandle } from "@element-hq/web-shared-components"; + +/** + * Contains auto-collapsed state and methods to expand/collapse the panel. + * This class is used by the different auto-collapse behaviours. + */ +export class CollapseHandler { + /** + * @param setCollapsed Callback that will be called when the handler changes the collapsed state. + */ + public constructor(private readonly setCollapsed: (collapsed: boolean) => void) {} + + /** + * This gives access to the panel's imperative methods. + */ + public panelHandle?: PanelImperativeHandle; + + /** + * Whether the left-panel is auto-collapsed. + */ + public isAutoCollapsed: boolean = false; + + /** + * Stores the width to which the left-panel should be restored to when the auto-collapsed + * panel is expanded due to the window being resized. + */ + private restoreWidth?: number; + + /** + * Make the panel API from react-resizable-panels available to this class. + * @param handle The panel handle to access react-resizable-panels API. + */ + public setHandle = (handle: PanelImperativeHandle): void => { + this.panelHandle = handle; + this.restoreWidth = this.panelHandle?.getSize().inPixels; + }; + + /** + * Update {@link CollapseHandler#restoreWidth} with the up to date width of the left panel. + */ + public updateRestoreWidth = (): void => { + // Takes a bit of time for the library to update the pixel values. + window.setTimeout(() => { + this.restoreWidth = this.panelHandle?.getSize().inPixels; + }, 500); + }; + + /** + * Collapse the left panel. + */ + public collapse = (): void => { + this.isAutoCollapsed = true; + this.panelHandle?.collapse(); + this.setCollapsed(true); + }; + + /** + * Expand the left panel. + */ + public expand = (): void => { + this.isAutoCollapsed = false; + this.panelHandle?.resize(this.restoreWidth!); + this.setCollapsed(false); + }; +} diff --git a/apps/web/test/viewmodels/structures/auto-collapse/CollapseHandler-test.ts b/apps/web/test/viewmodels/structures/auto-collapse/CollapseHandler-test.ts new file mode 100644 index 0000000000..0c75ef0001 --- /dev/null +++ b/apps/web/test/viewmodels/structures/auto-collapse/CollapseHandler-test.ts @@ -0,0 +1,55 @@ +/* + * 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 { PanelImperativeHandle } from "@element-hq/web-shared-components"; +import { CollapseHandler } from "../../../../src/viewmodels/structures/auto-collapse/CollapseHandler"; +import { MockPanelHandle } from "./mocks"; + +function setup() { + const setCollapsed = jest.fn(); + const panelHandle = new MockPanelHandle(); + const collapseHandler = new CollapseHandler(setCollapsed); + collapseHandler.setHandle(panelHandle as unknown as PanelImperativeHandle); + return { collapseHandler, panelHandle, setCollapsed }; +} + +jest.useFakeTimers(); + +describe("CollapseHandler", () => { + it("should collapse panel on collapse()", () => { + const { collapseHandler, panelHandle, setCollapsed } = setup(); + collapseHandler.collapse(); + expect(setCollapsed).toHaveBeenCalledWith(true); + expect(panelHandle.collapse).toHaveBeenCalledTimes(1); + expect(collapseHandler.isAutoCollapsed).toBe(true); + }); + + it("should expand panel on expand()", () => { + const { collapseHandler, panelHandle, setCollapsed } = setup(); + collapseHandler.expand(); + expect(setCollapsed).toHaveBeenCalledWith(false); + expect(panelHandle.resize).toHaveBeenCalledTimes(1); + expect(collapseHandler.isAutoCollapsed).toBe(false); + }); + + it("should update restoreWidth on updateRestoreWidth()", () => { + const { collapseHandler, panelHandle } = setup(); + collapseHandler.updateRestoreWidth(); + jest.runAllTimers(); + expect(panelHandle.getSize).toHaveBeenCalled(); + }); + + it("should update restoreWidth on setHandle", () => { + const setCollapsed = jest.fn(); + const panelHandle = new MockPanelHandle(); + const collapseHandler = new CollapseHandler(setCollapsed); + expect(collapseHandler.panelHandle).toBeUndefined(); + collapseHandler.setHandle(panelHandle as unknown as PanelImperativeHandle); + expect(collapseHandler.panelHandle).toBe(panelHandle); + expect(panelHandle.getSize).toHaveBeenCalled(); + }); +});