From ebe7d5191f6aae70e3d6217d98da3cd8736e847f Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Sun, 29 Mar 2026 19:33:15 +0530 Subject: [PATCH] Add functionality to UIStore - Make it possible to query if the window is currently being resized - Emit WidthIncreased/WidthDecreased events --- apps/web/src/stores/UIStore.ts | 42 ++++++++++- .../test/unit-tests/stores/UIStore-test.ts | 73 +++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 apps/web/test/unit-tests/stores/UIStore-test.ts diff --git a/apps/web/src/stores/UIStore.ts b/apps/web/src/stores/UIStore.ts index 0792bb206d..e3e9558bbe 100644 --- a/apps/web/src/stores/UIStore.ts +++ b/apps/web/src/stores/UIStore.ts @@ -10,6 +10,8 @@ import EventEmitter from "events"; export enum UI_EVENTS { Resize = "resize", + WidthIncreased = "width-increased", + WidthDecreased = "width-decreased", } export default class UIStore extends EventEmitter { @@ -19,9 +21,14 @@ export default class UIStore extends EventEmitter { private uiElementDimensions = new Map(); private trackedUiElements = new Map(); + private timeoutId: number = 0; public windowWidth: number; public windowHeight: number; + /** + * Whether the window is currently being resized. + */ + public isWindowBeingResized: boolean = false; public constructor() { super(); @@ -38,6 +45,7 @@ export default class UIStore extends EventEmitter { public static get instance(): UIStore { if (!UIStore._instance) { UIStore._instance = new UIStore(); + window.mxUIStore = UIStore._instance; } return UIStore._instance; } @@ -81,7 +89,12 @@ export default class UIStore extends EventEmitter { const windowEntry = entries.find((entry) => entry.target === document.body); if (windowEntry) { - this.windowWidth = windowEntry.contentRect.width; + this.setWindowAsBeingResized(); + + const currentWidth = windowEntry.contentRect.width; + this.emitWidthChangeEvents(currentWidth); + + this.windowWidth = currentWidth; this.windowHeight = windowEntry.contentRect.height; } @@ -95,6 +108,29 @@ export default class UIStore extends EventEmitter { this.emit(UI_EVENTS.Resize, entries); }; -} -window.mxUIStore = UIStore.instance; + /** + * Emit any necessary {@link UI_EVENTS.WidthIncreased} or {@link UI_EVENTS.WidthDecreased} events. + * @param currentWidth The current width of {@link window} + */ + private emitWidthChangeEvents = (currentWidth: number): void => { + if (currentWidth > this.windowWidth) this.emit(UI_EVENTS.WidthIncreased, currentWidth); + if (currentWidth < this.windowWidth) this.emit(UI_EVENTS.WidthDecreased, currentWidth); + }; + + /** + * Update {@link UIStore#isWindowBeingResized}. + */ + private setWindowAsBeingResized = (): void => { + // Window is being resized, so set to true. + this.isWindowBeingResized = true; + // Reset any previous timeout. + window.clearTimeout(this.timeoutId); + // Set to false after a second. + // If the window continues to be resized, this method will be called + // again and this setTimeout will be cancelled. + this.timeoutId = window.setTimeout(() => { + this.isWindowBeingResized = false; + }, 1000); + }; +} diff --git a/apps/web/test/unit-tests/stores/UIStore-test.ts b/apps/web/test/unit-tests/stores/UIStore-test.ts new file mode 100644 index 0000000000..0daee1b383 --- /dev/null +++ b/apps/web/test/unit-tests/stores/UIStore-test.ts @@ -0,0 +1,73 @@ +/* + * 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"; + +jest.useFakeTimers(); + +describe("UIStore", () => { + class MockResizeObserver { + public static instance: ResizeObserver; + public static callback: ResizeObserverCallback; + + public constructor(callback: ResizeObserverCallback) { + MockResizeObserver.callback = callback; + MockResizeObserver.instance = this; + } + + public static changeWidth = (width: number): void => { + const entry = { + target: document.body, + contentRect: { + width, + height: 1000, + }, + } as unknown as ResizeObserverEntry; + MockResizeObserver.callback([entry], MockResizeObserver.instance); + }; + + public observe = jest.fn(); + public unobserve = jest.fn(); + public disconnect = jest.fn(); + } + globalThis.ResizeObserver = MockResizeObserver; + + it("should emit events on width increase/decrease", () => { + // eslint-disable-next-line no-restricted-properties + window.innerWidth = 500; + const store = UIStore.instance; + + const onDecrease = jest.fn(); + store.on(UI_EVENTS.WidthDecreased, onDecrease); + MockResizeObserver.changeWidth(200); + expect(onDecrease).toHaveBeenCalledWith(200); + + const onIncrease = jest.fn(); + store.on(UI_EVENTS.WidthIncreased, onIncrease); + MockResizeObserver.changeWidth(700); + expect(onIncrease).toHaveBeenCalledWith(700); + + UIStore.destroy(); + }); + + it("should set isWindowBeingResized on resize", () => { + // eslint-disable-next-line no-restricted-properties + window.innerWidth = 500; + const store = UIStore.instance; + + // No resize yet, so expect isWindowBeingResized to be false + expect(store.isWindowBeingResized).toBe(false); + + // When resizing the window, expect isWindowBeingResized to be true + MockResizeObserver.changeWidth(200); + expect(store.isWindowBeingResized).toBe(true); + + // After a second, isWindowBeingResized should again become false + jest.runAllTimers(); + expect(store.isWindowBeingResized).toBe(false); + }); +});