Implement auto collapse on window resize behaviour

This commit is contained in:
R Midhun Suresh 2026-03-29 19:40:51 +05:30
parent 77c389a29e
commit bacda61eea
No known key found for this signature in database
3 changed files with 194 additions and 0 deletions

View File

@ -0,0 +1,97 @@
/*
* 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 { throttle } from "lodash";
import UIStore, { UI_EVENTS } from "../../../../stores/UIStore";
import { BaseCollapseBehaviour } from "./BaseCollapseBehaviour";
import type { CollapseHandler } from "../CollapseHandler";
/**
* The viewport width below which the left panel will be auto-collapsed.
*/
const AUTO_COLLAPSE_WIDTH = 768;
/**
* Implements auto-collapse logic that collapses and expands the left panel when the
* app window is resized.
*/
export class CollapseOnWindowResizeBehaviour extends BaseCollapseBehaviour {
/**
* If this boolean is true, we won't auto collapse the panel when the
* window is resized to be smaller than AUTO_COLLAPSE_WIDTH.
*
* If the panel is auto-collapsed and then the user manually expands the
* panel, we want to make sure that further window resizing does not collapse
* the panel.
*/
private disableAutoCollapse = false;
public constructor(collapseHandler: CollapseHandler) {
super(collapseHandler);
UIStore.instance.on(UI_EVENTS.WidthIncreased, this.onWindowWidthIncreased);
UIStore.instance.on(UI_EVENTS.WidthDecreased, this.onWindowWidthDecreased);
}
private onWindowWidthDecreased = throttle((currentWindowWidth: number): void => {
// If the panel is already collapsed, we have nothing else left to do.
if (this.collapseHandler.isAutoCollapsed || this.collapseHandler.panelHandle?.isCollapsed()) return;
// We were already auto-collapsed and the user has manually resized the panel.
// Don't auto-collapse again.
if (this.disableAutoCollapse) return;
if (currentWindowWidth <= AUTO_COLLAPSE_WIDTH) {
this.collapseHandler.collapse();
console.log("\t collapsed panel");
}
}, 50);
public onLeftPanelResized = (): void => {
if (this.collapseHandler.isAutoCollapsed) {
// Track that the user has manually resized the auto-collapsed panel.
this.disableAutoCollapse = true;
}
};
private onWindowWidthIncreased = throttle((currentWindowWidth: number): void => {
if (currentWindowWidth > AUTO_COLLAPSE_WIDTH) {
// Reset the flag when we cross the collapse width boundary.
this.disableAutoCollapse = false;
// If the panel isn't already collapsed, we don't need to expand the panel.
if (!this.collapseHandler.isAutoCollapsed) return;
// As the window is resized, react-resizable-panels is also resizing the panels.
// We'll expand the panel after a second to avoid racing with the library logic.
window.setTimeout(() => {
// this.disableAutoCollapse = false;
this.collapseHandler.expand();
}, 1000);
}
}, 50);
/**
* Whether the window is currently being resized.
*/
public get shouldIgnoreResize(): boolean {
// When the window is resized, the panel is resized in various ways.
// These transient changes should not be persisted in settings.
// So early return if that is the case.
return UIStore.instance.isWindowBeingResized;
}
/**
* Remove's any event listeners used by this class.
*/
public dispose = (): void => {
UIStore.instance.off(UI_EVENTS.WidthIncreased, this.onWindowWidthIncreased);
UIStore.instance.off(UI_EVENTS.WidthDecreased, this.onWindowWidthDecreased);
};
public static shouldStartCollapsed(): boolean {
return UIStore.instance.windowWidth <= AUTO_COLLAPSE_WIDTH;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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 UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
import { CollapseOnWindowResizeBehaviour } from "../../../../../src/viewmodels/structures/auto-collapse/behaviours/CollapseOnWindowResizeBehaviour";
import type { CollapseHandler } from "../../../../../src/viewmodels/structures/auto-collapse/CollapseHandler";
import { MockCollapseHandler } from "../mocks";
jest.useFakeTimers();
describe("CollapseOnWindowResizeBehaviour", () => {
it("Should collapse/expand the panel when the window is resized", () => {
const collapseHandler = new MockCollapseHandler() as unknown as CollapseHandler;
new CollapseOnWindowResizeBehaviour(collapseHandler);
// Making the window smaller should collapse the panel.
UIStore.instance.emit(UI_EVENTS.WidthDecreased, 750);
expect(collapseHandler.collapse).toHaveBeenCalledTimes(1);
// Making the window larger should expand the panel.
UIStore.instance.emit(UI_EVENTS.WidthIncreased, 950);
jest.runAllTimers();
expect(collapseHandler.expand).toHaveBeenCalledTimes(1);
});
it("should set shouldIgnoreResize to true when window is being resized", () => {
const collapseHandler = new MockCollapseHandler() as unknown as CollapseHandler;
const behaviour = new CollapseOnWindowResizeBehaviour(collapseHandler);
expect(behaviour.shouldIgnoreResize).toBe(false);
// When the window is being resized, this behaviour should tell the AutoCollapser to
// ignore any panel resize events.
UIStore.instance.isWindowBeingResized = true;
expect(behaviour.shouldIgnoreResize).toBe(true);
});
it("should not auto-collapse panel when user has manually resized the panel", () => {
const collapseHandler = new MockCollapseHandler();
const behaviour = new CollapseOnWindowResizeBehaviour(collapseHandler as unknown as CollapseHandler);
// Let's make the window smaller so that the panel is auto-collapsed.
UIStore.instance.emit(UI_EVENTS.WidthDecreased, 750);
expect(collapseHandler.collapse).toHaveBeenCalledTimes(1);
collapseHandler.collapse.mockClear();
// Let's say that the user now manually expands the auto-collapsed panel.
behaviour.onLeftPanelResized();
// Let's say that the window now became even smaller
UIStore.instance.emit(UI_EVENTS.WidthDecreased, 500);
// The panel should not be auto-collapsed again
expect(collapseHandler.collapse).not.toHaveBeenCalled();
});
it("should return correct shouldStartCollapsed", () => {
const collapseHandler = new MockCollapseHandler();
new CollapseOnWindowResizeBehaviour(collapseHandler as unknown as CollapseHandler);
// When the window is smaller than 768px, start collapsed.
UIStore.instance.windowWidth = 750;
expect(CollapseOnWindowResizeBehaviour.shouldStartCollapsed()).toBe(true);
// When the window is larger than 768px, start expanded.
UIStore.instance.windowWidth = 900;
expect(CollapseOnWindowResizeBehaviour.shouldStartCollapsed()).toBe(false);
});
});

View File

@ -0,0 +1,34 @@
/*
* 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.
*/
export class MockCollapseHandler {
public collapse = jest.fn().mockImplementation(() => {
this.isAutoCollapsed = true;
});
public expand = jest.fn().mockImplementation(() => {
this.isAutoCollapsed = false;
});
public isAutoCollapsed = false;
public panelHandle = new MockPanelHandle();
}
export class MockPanelHandle {
private _isCollapsed = false;
public inPixels = 100;
public isCollapsed = jest.fn().mockImplementation(() => {
return this._isCollapsed;
});
public collapse = jest.fn().mockImplementation(() => {
this._isCollapsed = true;
});
public resize = jest.fn().mockImplementation(() => {
this._isCollapsed = false;
});
getSize = jest.fn().mockReturnValue({
inPixels: this.inPixels,
});
}