mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-10 14:46:25 +02:00
Implement auto collapse on window resize behaviour
This commit is contained in:
parent
77c389a29e
commit
bacda61eea
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
34
apps/web/test/viewmodels/structures/auto-collapse/mocks.ts
Normal file
34
apps/web/test/viewmodels/structures/auto-collapse/mocks.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user