From 22fe867b4bdd99d12ca45b910e7e236bf9b640b9 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 12 Mar 2026 20:51:23 -0400 Subject: [PATCH 1/2] Panel imperative API resize() accepts all size units --- .../utils/getImperativePanelMethods.test.ts | 60 +++++++++++++++++-- lib/global/utils/getImperativePanelMethods.ts | 30 ++++------ 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/lib/global/utils/getImperativePanelMethods.test.ts b/lib/global/utils/getImperativePanelMethods.test.ts index 7967d8c25..2d571b907 100644 --- a/lib/global/utils/getImperativePanelMethods.test.ts +++ b/lib/global/utils/getImperativePanelMethods.test.ts @@ -11,6 +11,10 @@ import type { PanelConstraints, PanelImperativeHandle } from "../../components/panel/types"; +import { + mockGetComputedStyle, + setDefaultElementStyle +} from "../../utils/test/mockGetComputedStyle"; import { mountGroup } from "../mountGroup"; import { subscribeToMountedGroup } from "../mutable-state/groups"; import { mockGroup } from "../test/mockGroup"; @@ -294,23 +298,71 @@ describe("getImperativePanelMethods", () => { }); describe("resize", () => { + describe("units", () => { + test("accepts percentage units", () => { + const { panelApis } = init([{}, {}]); + panelApis[0].resize("35%"); + + expect(onLayoutChange).toHaveBeenCalledTimes(1); + expect(onLayoutChange).toHaveBeenCalledWith([35, 65]); + }); + + test("accepts pixel units", () => { + // Computed group size is 1,000 + const { panelApis } = init([{}, {}]); + panelApis[0].resize(400); + + expect(onLayoutChange).toHaveBeenCalledTimes(1); + expect(onLayoutChange).toHaveBeenCalledWith([40, 60]); + }); + + test("accepts rem units", () => { + setDefaultElementStyle({ + fontSize: 16, + writingMode: "" + } as unknown as CSSStyleDeclaration); + const unmockGetComputedStyle = mockGetComputedStyle(); + + try { + const { panelApis } = init([{}, {}]); + panelApis[0].resize("10rem"); + + expect(onLayoutChange).toHaveBeenCalledTimes(1); + expect(onLayoutChange).toHaveBeenCalledWith([16, 84]); + } finally { + unmockGetComputedStyle(); + } + }); + + test("accepts viewport units", () => { + window.innerHeight = 2000; + window.innerWidth = 2000; + + const { panelApis } = init([{}, {}]); + panelApis[0].resize("15vw"); + + expect(onLayoutChange).toHaveBeenCalledTimes(1); + expect(onLayoutChange).toHaveBeenCalledWith([30, 70]); + }); + }); + test("ignores a no-op size update", () => { const { panelApis } = init([{ defaultSize: 10 }, {}]); - panelApis[0].resize(10); + panelApis[0].resize("10%"); expect(onLayoutChange).not.toHaveBeenCalled(); }); test("ignores an invalid size update", () => { const { panelApis } = init([{ defaultSize: 10, minSize: 10 }, {}]); - panelApis[0].resize(0); + panelApis[0].resize("0%"); expect(onLayoutChange).not.toHaveBeenCalled(); }); test("validates and updates the panel size", () => { const { panelApis } = init([{ defaultSize: 25, minSize: 10 }, {}]); - panelApis[0].resize(0); + panelApis[0].resize("0%"); expect(onLayoutChange).toHaveBeenCalledTimes(1); expect(onLayoutChange).toHaveBeenCalledWith([10, 90]); @@ -322,7 +374,7 @@ describe("getImperativePanelMethods", () => { {} ]); - panelApis[0].resize(0); + panelApis[0].resize("0%"); expect(onLayoutChange).toHaveBeenCalledTimes(1); expect(onLayoutChange).toHaveBeenCalledWith([10, 90]); diff --git a/lib/global/utils/getImperativePanelMethods.ts b/lib/global/utils/getImperativePanelMethods.ts index fe54f499f..edecec587 100644 --- a/lib/global/utils/getImperativePanelMethods.ts +++ b/lib/global/utils/getImperativePanelMethods.ts @@ -1,6 +1,7 @@ import type { PanelImperativeHandle } from "../../components/panel/types"; import { calculateAvailableGroupSize } from "../dom/calculateAvailableGroupSize"; import { getMountedGroups, updateMountedGroup } from "../mutable-state/groups"; +import { sizeStyleToPixels } from "../styles/sizeStyleToPixels"; import { adjustLayoutByDelta } from "./adjustLayoutByDelta"; import { formatLayoutNumber } from "./formatLayoutNumber"; import { layoutNumbersEqual } from "./layoutNumbersEqual"; @@ -164,24 +165,19 @@ export function getImperativePanelMethods({ return collapsible && layoutNumbersEqual(collapsedSize, size); }, resize: (size: number | string) => { - const prevSize = getPanelSize(); - if (prevSize !== size) { - let asPercentage; - switch (typeof size) { - case "number": { - const { group } = find(); - const groupSize = calculateAvailableGroupSize({ group }); - asPercentage = formatLayoutNumber((size / groupSize) * 100); - break; - } - case "string": { - asPercentage = parseFloat(size); - break; - } - } + const { group } = find(); + const { element } = getPanel(); + const groupSize = calculateAvailableGroupSize({ group }); - setPanelSize(asPercentage); - } + const asPixels = sizeStyleToPixels({ + groupSize, + panelElement: element, + styleProp: size + }); + + const asPercentage = formatLayoutNumber((asPixels / groupSize) * 100); + + setPanelSize(asPercentage); } } satisfies PanelImperativeHandle; } From 39c197d6e23fdf3f3c65a0b8fcd96df45e47d67e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 14 Mar 2026 11:35:46 -0400 Subject: [PATCH 2/2] Tidied up mock utils --- lib/global/styles/sizeStyleToPixels.test.ts | 15 ++------- .../utils/getImperativePanelMethods.test.ts | 14 +++----- lib/utils/test/mockBoundingClientRect.ts | 18 +++++------ lib/utils/test/mockGetComputedStyle.ts | 29 +++++++++++------ lib/utils/test/mockResizeObserver.ts | 12 +++---- vitest.setup.ts | 32 +++++++++++-------- 6 files changed, 59 insertions(+), 61 deletions(-) diff --git a/lib/global/styles/sizeStyleToPixels.test.ts b/lib/global/styles/sizeStyleToPixels.test.ts index f24dd1e29..aaf5cfa21 100644 --- a/lib/global/styles/sizeStyleToPixels.test.ts +++ b/lib/global/styles/sizeStyleToPixels.test.ts @@ -1,25 +1,14 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { NOOP_FUNCTION } from "../../constants"; -import { - mockGetComputedStyle, - setDefaultElementStyle -} from "../../utils/test/mockGetComputedStyle"; +import { beforeEach, describe, expect, test } from "vitest"; +import { setDefaultElementStyle } from "../../utils/test/mockGetComputedStyle"; import { sizeStyleToPixels } from "./sizeStyleToPixels"; describe("sizeStyleToPixels", () => { let panelElement: HTMLElement; - let unmockGetComputedStyle = NOOP_FUNCTION; beforeEach(() => { - unmockGetComputedStyle = mockGetComputedStyle(); - panelElement = document.createElement("div"); }); - afterEach(() => { - unmockGetComputedStyle(); - }); - describe("implicit units", () => { test("% units", () => { expect( diff --git a/lib/global/utils/getImperativePanelMethods.test.ts b/lib/global/utils/getImperativePanelMethods.test.ts index 2d571b907..9efe9eb26 100644 --- a/lib/global/utils/getImperativePanelMethods.test.ts +++ b/lib/global/utils/getImperativePanelMethods.test.ts @@ -321,17 +321,13 @@ describe("getImperativePanelMethods", () => { fontSize: 16, writingMode: "" } as unknown as CSSStyleDeclaration); - const unmockGetComputedStyle = mockGetComputedStyle(); + mockGetComputedStyle(); - try { - const { panelApis } = init([{}, {}]); - panelApis[0].resize("10rem"); + const { panelApis } = init([{}, {}]); + panelApis[0].resize("10rem"); - expect(onLayoutChange).toHaveBeenCalledTimes(1); - expect(onLayoutChange).toHaveBeenCalledWith([16, 84]); - } finally { - unmockGetComputedStyle(); - } + expect(onLayoutChange).toHaveBeenCalledTimes(1); + expect(onLayoutChange).toHaveBeenCalledWith([16, 84]); }); test("accepts viewport units", () => { diff --git a/lib/utils/test/mockBoundingClientRect.ts b/lib/utils/test/mockBoundingClientRect.ts index 344fbac20..cea0a5e07 100644 --- a/lib/utils/test/mockBoundingClientRect.ts +++ b/lib/utils/test/mockBoundingClientRect.ts @@ -2,6 +2,9 @@ import EventEmitter from "node:events"; type GetDOMRect = (element: HTMLElement) => DOMRectReadOnly | undefined | void; +const originalGetBoundingClientRect = + HTMLElement.prototype.getBoundingClientRect; + export const emitter = new EventEmitter(); emitter.setMaxListeners(100); @@ -76,9 +79,6 @@ export function setElementBounds(element: HTMLElement, rect: DOMRect) { } export function mockBoundingClientRect() { - const originalGetBoundingClientRect = - HTMLElement.prototype.getBoundingClientRect; - HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() { if (getDOMRect) { @@ -129,13 +129,13 @@ export function mockBoundingClientRect() { return (this as HTMLElement).getBoundingClientRect().width; } }); +} - return function unmockBoundingClientRect() { - HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect; +export function unmockBoundingClientRect() { + HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect; - defaultDomRect = new DOMRect(0, 0, 0, 0); - getDOMRect = undefined; + defaultDomRect = new DOMRect(0, 0, 0, 0); + getDOMRect = undefined; - elementToDOMRect.clear(); - }; + elementToDOMRect.clear(); } diff --git a/lib/utils/test/mockGetComputedStyle.ts b/lib/utils/test/mockGetComputedStyle.ts index f65665468..d2eb927fe 100644 --- a/lib/utils/test/mockGetComputedStyle.ts +++ b/lib/utils/test/mockGetComputedStyle.ts @@ -2,24 +2,29 @@ import { EventEmitter } from "stream"; const elementToStyle = new Map(); +const originalGetComputedStyle = window.getComputedStyle; + let defaultStyle: CSSStyleDeclaration | undefined = undefined; +const emptyStyle = {} as CSSStyleDeclaration; + export const emitter = new EventEmitter(); emitter.setMaxListeners(100); export function mockGetComputedStyle() { - const originalGetComputedStyle = window.getComputedStyle; - window.getComputedStyle = function getComputedStyle(element: Element) { - return ( - elementToStyle.get(element) ?? - defaultStyle ?? - originalGetComputedStyle(element) - ); - }; + return new Proxy(emptyStyle, { + get(_, name) { + const key = name as keyof CSSStyleDeclaration; - return () => { - window.getComputedStyle = originalGetComputedStyle; + const mockedStyle = + elementToStyle.get(element) ?? defaultStyle ?? emptyStyle; + + const actualStyle = originalGetComputedStyle(element); + + return name in mockedStyle ? mockedStyle[key] : actualStyle[key]; + } + }); }; } @@ -34,3 +39,7 @@ export function setElementStyle(element: Element, style: CSSStyleDeclaration) { emitter.emit("change", element); } + +export function unmockGetComputedStyle() { + window.getComputedStyle = originalGetComputedStyle; +} diff --git a/lib/utils/test/mockResizeObserver.ts b/lib/utils/test/mockResizeObserver.ts index e30fb7a8a..72c02db20 100644 --- a/lib/utils/test/mockResizeObserver.ts +++ b/lib/utils/test/mockResizeObserver.ts @@ -1,5 +1,7 @@ import { emitter } from "./mockBoundingClientRect"; +const originalResizeObserver = window.ResizeObserver; + let disabled: boolean = false; export function disableResizeObserverForCurrentTest() { @@ -14,15 +16,13 @@ export function simulateUnsupportedEnvironmentForTest() { export function mockResizeObserver() { disabled = false; - const originalResizeObserver = window.ResizeObserver; - window.ResizeObserver = MockResizeObserver; +} - return function unmockResizeObserver() { - window.ResizeObserver = originalResizeObserver; +export function unmockResizeObserver() { + window.ResizeObserver = originalResizeObserver; - disabled = false; - }; + disabled = false; } class MockResizeObserver implements ResizeObserver { diff --git a/vitest.setup.ts b/vitest.setup.ts index 64eebe83c..9d306aeb8 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -3,11 +3,18 @@ import { cleanup } from "@testing-library/react"; import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from "vitest"; import failOnConsole from "vitest-fail-on-console"; import { resetMockGroupIdCounter } from "./lib/global/test/mockGroup"; -import { mockBoundingClientRect } from "./lib/utils/test/mockBoundingClientRect"; -import { mockResizeObserver } from "./lib/utils/test/mockResizeObserver"; - -let unmockBoundingClientRect: (() => void) | null = null; -let unmockResizeObserver: (() => void) | null = null; +import { + mockBoundingClientRect, + unmockBoundingClientRect +} from "./lib/utils/test/mockBoundingClientRect"; +import { + mockGetComputedStyle, + unmockGetComputedStyle +} from "./lib/utils/test/mockGetComputedStyle"; +import { + mockResizeObserver, + unmockResizeObserver +} from "./lib/utils/test/mockResizeObserver"; const PROTOTYPE_PROPS = [ "clientHeight", @@ -76,8 +83,9 @@ afterAll(() => { }); beforeEach(() => { - unmockBoundingClientRect = mockBoundingClientRect(); - unmockResizeObserver = mockResizeObserver(); + mockBoundingClientRect(); + mockGetComputedStyle(); + mockResizeObserver(); }); afterEach(() => { @@ -85,11 +93,7 @@ afterEach(() => { resetMockGroupIdCounter(); - if (unmockBoundingClientRect) { - unmockBoundingClientRect(); - } - - if (unmockResizeObserver) { - unmockResizeObserver(); - } + unmockBoundingClientRect(); + unmockGetComputedStyle(); + unmockResizeObserver(); });