diff --git a/apps/web/src/viewmodels/structures/auto-collapse/AutoCollapse.ts b/apps/web/src/viewmodels/structures/auto-collapse/AutoCollapse.ts new file mode 100644 index 0000000000..1c499820bd --- /dev/null +++ b/apps/web/src/viewmodels/structures/auto-collapse/AutoCollapse.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"; +import { CollapseHandler } from "./CollapseHandler"; +import type { BaseCollapseBehaviour } from "./behaviours/BaseCollapseBehaviour"; +import { Behaviours } from "./behaviours/behaviours"; + +/** + * This class orchestrates all the auto-collapse behaviours. + */ +export class AutoCollapse { + private readonly behaviours: BaseCollapseBehaviour[] = []; + private readonly collapseHandler: CollapseHandler; + + public constructor(setCollapsed: (collapsed: boolean) => void) { + this.collapseHandler = new CollapseHandler(setCollapsed); + for (const Behaviour of Behaviours) { + this.behaviours.push(new Behaviour(this.collapseHandler)); + } + } + + /** + * When this returns true, any left panel resized events should be ignored. + */ + public get shouldIgnoreResize(): boolean { + return this.behaviours.some((b) => b.shouldIgnoreResize); + } + + /** + * Whether the panel is currently auto-collapsed. + */ + public get isAutoCollapsed(): boolean { + return this.collapseHandler.isAutoCollapsed; + } + + /** + * Returns boolean indicating whether the left panel should be collapsed at app start. + */ + public static shouldStartCollapsed(): boolean { + return Behaviours.some((B) => B.shouldStartCollapsed()); + } + + public dispose = (): void => { + for (const behaviour of this.behaviours) { + behaviour.dispose(); + } + }; + + /** + * 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.collapseHandler.setHandle(handle); + }; + + /** + * Should be called when the left panel is resized. + */ + public onLeftPanelResized = (): void => { + for (const behaviour of this.behaviours) { + behaviour.onLeftPanelResized(); + } + this.collapseHandler.isAutoCollapsed = false; + this.collapseHandler.updateRestoreWidth(); + }; +} diff --git a/apps/web/test/viewmodels/structures/auto-collapse/AutoCollapse-test.ts b/apps/web/test/viewmodels/structures/auto-collapse/AutoCollapse-test.ts new file mode 100644 index 0000000000..73862218b7 --- /dev/null +++ b/apps/web/test/viewmodels/structures/auto-collapse/AutoCollapse-test.ts @@ -0,0 +1,74 @@ +/* + * 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 { AutoCollapse } from "../../../../src/viewmodels/structures/auto-collapse/AutoCollapse"; +import { MockPanelHandle } from "./mocks"; +import type { CollapseHandler } from "../../../../src/viewmodels/structures/auto-collapse/CollapseHandler"; +import { BaseCollapseBehaviour } from "../../../../src/viewmodels/structures/auto-collapse/behaviours/BaseCollapseBehaviour"; + +let instances: BaseCollapseBehaviour[] = []; + +class MockBehaviour extends BaseCollapseBehaviour { + public constructor(collapseHandler: CollapseHandler) { + super(collapseHandler); + instances.push(this); + } + + public onLeftPanelResized = jest.fn(); +} + +class MockBehaviourWithStartCollapsed extends MockBehaviour { + public static shouldStartCollapsed(): boolean { + return true; + } +} + +class MockBehaviourWithIgnoreResize extends MockBehaviour { + public get shouldIgnoreResize(): boolean { + return true; + } +} + +jest.mock("../../../../src/viewmodels/structures/auto-collapse/behaviours/behaviours", () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + get Behaviours() { + return [MockBehaviour, MockBehaviour, MockBehaviourWithIgnoreResize, MockBehaviourWithStartCollapsed]; + }, + }; +}); + +describe("AutoCollapse", () => { + beforeEach(() => { + instances = []; + }); + + it("should call onLeftPanelResized of each behaviour", () => { + const setCollapsed = jest.fn(); + const panelHandle = new MockPanelHandle(); + const autoCollapse = new AutoCollapse(setCollapsed); + autoCollapse.setHandle(panelHandle as unknown as PanelImperativeHandle); + autoCollapse.onLeftPanelResized(); + for (const behaviour of instances) { + expect(behaviour.onLeftPanelResized).toHaveBeenCalledTimes(1); + } + expect(autoCollapse.isAutoCollapsed).toBe(false); + }); + + it("should calculate shouldStartCollapsed correctly", () => { + expect(AutoCollapse.shouldStartCollapsed()).toBe(true); + }); + + it("should calculate shouldIgnoreResize correctly", () => { + const setCollapsed = jest.fn(); + const panelHandle = new MockPanelHandle(); + const autoCollapse = new AutoCollapse(setCollapsed); + autoCollapse.setHandle(panelHandle as unknown as PanelImperativeHandle); + expect(autoCollapse.shouldIgnoreResize).toBe(true); + }); +});