Skip to content

Commit e8dd430

Browse files
authored
Panel imperative API resize() accepts all size units (#690)
Resolves #687
1 parent 5521119 commit e8dd430

7 files changed

Lines changed: 119 additions & 73 deletions

lib/global/styles/sizeStyleToPixels.test.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,14 @@
1-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
2-
import { NOOP_FUNCTION } from "../../constants";
3-
import {
4-
mockGetComputedStyle,
5-
setDefaultElementStyle
6-
} from "../../utils/test/mockGetComputedStyle";
1+
import { beforeEach, describe, expect, test } from "vitest";
2+
import { setDefaultElementStyle } from "../../utils/test/mockGetComputedStyle";
73
import { sizeStyleToPixels } from "./sizeStyleToPixels";
84

95
describe("sizeStyleToPixels", () => {
106
let panelElement: HTMLElement;
11-
let unmockGetComputedStyle = NOOP_FUNCTION;
127

138
beforeEach(() => {
14-
unmockGetComputedStyle = mockGetComputedStyle();
15-
169
panelElement = document.createElement("div");
1710
});
1811

19-
afterEach(() => {
20-
unmockGetComputedStyle();
21-
});
22-
2312
describe("implicit units", () => {
2413
test("% units", () => {
2514
expect(

lib/global/utils/getImperativePanelMethods.test.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import type {
1111
PanelConstraints,
1212
PanelImperativeHandle
1313
} from "../../components/panel/types";
14+
import {
15+
mockGetComputedStyle,
16+
setDefaultElementStyle
17+
} from "../../utils/test/mockGetComputedStyle";
1418
import { mountGroup } from "../mountGroup";
1519
import { subscribeToMountedGroup } from "../mutable-state/groups";
1620
import { mockGroup } from "../test/mockGroup";
@@ -294,23 +298,67 @@ describe("getImperativePanelMethods", () => {
294298
});
295299

296300
describe("resize", () => {
301+
describe("units", () => {
302+
test("accepts percentage units", () => {
303+
const { panelApis } = init([{}, {}]);
304+
panelApis[0].resize("35%");
305+
306+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
307+
expect(onLayoutChange).toHaveBeenCalledWith([35, 65]);
308+
});
309+
310+
test("accepts pixel units", () => {
311+
// Computed group size is 1,000
312+
const { panelApis } = init([{}, {}]);
313+
panelApis[0].resize(400);
314+
315+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
316+
expect(onLayoutChange).toHaveBeenCalledWith([40, 60]);
317+
});
318+
319+
test("accepts rem units", () => {
320+
setDefaultElementStyle({
321+
fontSize: 16,
322+
writingMode: ""
323+
} as unknown as CSSStyleDeclaration);
324+
mockGetComputedStyle();
325+
326+
const { panelApis } = init([{}, {}]);
327+
panelApis[0].resize("10rem");
328+
329+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
330+
expect(onLayoutChange).toHaveBeenCalledWith([16, 84]);
331+
});
332+
333+
test("accepts viewport units", () => {
334+
window.innerHeight = 2000;
335+
window.innerWidth = 2000;
336+
337+
const { panelApis } = init([{}, {}]);
338+
panelApis[0].resize("15vw");
339+
340+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
341+
expect(onLayoutChange).toHaveBeenCalledWith([30, 70]);
342+
});
343+
});
344+
297345
test("ignores a no-op size update", () => {
298346
const { panelApis } = init([{ defaultSize: 10 }, {}]);
299-
panelApis[0].resize(10);
347+
panelApis[0].resize("10%");
300348

301349
expect(onLayoutChange).not.toHaveBeenCalled();
302350
});
303351

304352
test("ignores an invalid size update", () => {
305353
const { panelApis } = init([{ defaultSize: 10, minSize: 10 }, {}]);
306-
panelApis[0].resize(0);
354+
panelApis[0].resize("0%");
307355

308356
expect(onLayoutChange).not.toHaveBeenCalled();
309357
});
310358

311359
test("validates and updates the panel size", () => {
312360
const { panelApis } = init([{ defaultSize: 25, minSize: 10 }, {}]);
313-
panelApis[0].resize(0);
361+
panelApis[0].resize("0%");
314362

315363
expect(onLayoutChange).toHaveBeenCalledTimes(1);
316364
expect(onLayoutChange).toHaveBeenCalledWith([10, 90]);
@@ -322,7 +370,7 @@ describe("getImperativePanelMethods", () => {
322370
{}
323371
]);
324372

325-
panelApis[0].resize(0);
373+
panelApis[0].resize("0%");
326374

327375
expect(onLayoutChange).toHaveBeenCalledTimes(1);
328376
expect(onLayoutChange).toHaveBeenCalledWith([10, 90]);

lib/global/utils/getImperativePanelMethods.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { PanelImperativeHandle } from "../../components/panel/types";
22
import { calculateAvailableGroupSize } from "../dom/calculateAvailableGroupSize";
33
import { getMountedGroups, updateMountedGroup } from "../mutable-state/groups";
4+
import { sizeStyleToPixels } from "../styles/sizeStyleToPixels";
45
import { adjustLayoutByDelta } from "./adjustLayoutByDelta";
56
import { formatLayoutNumber } from "./formatLayoutNumber";
67
import { layoutNumbersEqual } from "./layoutNumbersEqual";
@@ -164,24 +165,19 @@ export function getImperativePanelMethods({
164165
return collapsible && layoutNumbersEqual(collapsedSize, size);
165166
},
166167
resize: (size: number | string) => {
167-
const prevSize = getPanelSize();
168-
if (prevSize !== size) {
169-
let asPercentage;
170-
switch (typeof size) {
171-
case "number": {
172-
const { group } = find();
173-
const groupSize = calculateAvailableGroupSize({ group });
174-
asPercentage = formatLayoutNumber((size / groupSize) * 100);
175-
break;
176-
}
177-
case "string": {
178-
asPercentage = parseFloat(size);
179-
break;
180-
}
181-
}
168+
const { group } = find();
169+
const { element } = getPanel();
170+
const groupSize = calculateAvailableGroupSize({ group });
182171

183-
setPanelSize(asPercentage);
184-
}
172+
const asPixels = sizeStyleToPixels({
173+
groupSize,
174+
panelElement: element,
175+
styleProp: size
176+
});
177+
178+
const asPercentage = formatLayoutNumber((asPixels / groupSize) * 100);
179+
180+
setPanelSize(asPercentage);
185181
}
186182
} satisfies PanelImperativeHandle;
187183
}

lib/utils/test/mockBoundingClientRect.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import EventEmitter from "node:events";
22

33
type GetDOMRect = (element: HTMLElement) => DOMRectReadOnly | undefined | void;
44

5+
const originalGetBoundingClientRect =
6+
HTMLElement.prototype.getBoundingClientRect;
7+
58
export const emitter = new EventEmitter();
69
emitter.setMaxListeners(100);
710

@@ -76,9 +79,6 @@ export function setElementBounds(element: HTMLElement, rect: DOMRect) {
7679
}
7780

7881
export function mockBoundingClientRect() {
79-
const originalGetBoundingClientRect =
80-
HTMLElement.prototype.getBoundingClientRect;
81-
8282
HTMLElement.prototype.getBoundingClientRect =
8383
function getBoundingClientRect() {
8484
if (getDOMRect) {
@@ -129,13 +129,13 @@ export function mockBoundingClientRect() {
129129
return (this as HTMLElement).getBoundingClientRect().width;
130130
}
131131
});
132+
}
132133

133-
return function unmockBoundingClientRect() {
134-
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
134+
export function unmockBoundingClientRect() {
135+
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
135136

136-
defaultDomRect = new DOMRect(0, 0, 0, 0);
137-
getDOMRect = undefined;
137+
defaultDomRect = new DOMRect(0, 0, 0, 0);
138+
getDOMRect = undefined;
138139

139-
elementToDOMRect.clear();
140-
};
140+
elementToDOMRect.clear();
141141
}

lib/utils/test/mockGetComputedStyle.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,29 @@ import { EventEmitter } from "stream";
22

33
const elementToStyle = new Map<Element, CSSStyleDeclaration>();
44

5+
const originalGetComputedStyle = window.getComputedStyle;
6+
57
let defaultStyle: CSSStyleDeclaration | undefined = undefined;
68

9+
const emptyStyle = {} as CSSStyleDeclaration;
10+
711
export const emitter = new EventEmitter();
812
emitter.setMaxListeners(100);
913

1014
export function mockGetComputedStyle() {
11-
const originalGetComputedStyle = window.getComputedStyle;
12-
1315
window.getComputedStyle = function getComputedStyle(element: Element) {
14-
return (
15-
elementToStyle.get(element) ??
16-
defaultStyle ??
17-
originalGetComputedStyle(element)
18-
);
19-
};
16+
return new Proxy(emptyStyle, {
17+
get(_, name) {
18+
const key = name as keyof CSSStyleDeclaration;
2019

21-
return () => {
22-
window.getComputedStyle = originalGetComputedStyle;
20+
const mockedStyle =
21+
elementToStyle.get(element) ?? defaultStyle ?? emptyStyle;
22+
23+
const actualStyle = originalGetComputedStyle(element);
24+
25+
return name in mockedStyle ? mockedStyle[key] : actualStyle[key];
26+
}
27+
});
2328
};
2429
}
2530

@@ -34,3 +39,7 @@ export function setElementStyle(element: Element, style: CSSStyleDeclaration) {
3439

3540
emitter.emit("change", element);
3641
}
42+
43+
export function unmockGetComputedStyle() {
44+
window.getComputedStyle = originalGetComputedStyle;
45+
}

lib/utils/test/mockResizeObserver.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { emitter } from "./mockBoundingClientRect";
22

3+
const originalResizeObserver = window.ResizeObserver;
4+
35
let disabled: boolean = false;
46

57
export function disableResizeObserverForCurrentTest() {
@@ -14,15 +16,13 @@ export function simulateUnsupportedEnvironmentForTest() {
1416
export function mockResizeObserver() {
1517
disabled = false;
1618

17-
const originalResizeObserver = window.ResizeObserver;
18-
1919
window.ResizeObserver = MockResizeObserver;
20+
}
2021

21-
return function unmockResizeObserver() {
22-
window.ResizeObserver = originalResizeObserver;
22+
export function unmockResizeObserver() {
23+
window.ResizeObserver = originalResizeObserver;
2324

24-
disabled = false;
25-
};
25+
disabled = false;
2626
}
2727

2828
class MockResizeObserver implements ResizeObserver {

vitest.setup.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ import { cleanup } from "@testing-library/react";
33
import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from "vitest";
44
import failOnConsole from "vitest-fail-on-console";
55
import { resetMockGroupIdCounter } from "./lib/global/test/mockGroup";
6-
import { mockBoundingClientRect } from "./lib/utils/test/mockBoundingClientRect";
7-
import { mockResizeObserver } from "./lib/utils/test/mockResizeObserver";
8-
9-
let unmockBoundingClientRect: (() => void) | null = null;
10-
let unmockResizeObserver: (() => void) | null = null;
6+
import {
7+
mockBoundingClientRect,
8+
unmockBoundingClientRect
9+
} from "./lib/utils/test/mockBoundingClientRect";
10+
import {
11+
mockGetComputedStyle,
12+
unmockGetComputedStyle
13+
} from "./lib/utils/test/mockGetComputedStyle";
14+
import {
15+
mockResizeObserver,
16+
unmockResizeObserver
17+
} from "./lib/utils/test/mockResizeObserver";
1118

1219
const PROTOTYPE_PROPS = [
1320
"clientHeight",
@@ -76,20 +83,17 @@ afterAll(() => {
7683
});
7784

7885
beforeEach(() => {
79-
unmockBoundingClientRect = mockBoundingClientRect();
80-
unmockResizeObserver = mockResizeObserver();
86+
mockBoundingClientRect();
87+
mockGetComputedStyle();
88+
mockResizeObserver();
8189
});
8290

8391
afterEach(() => {
8492
cleanup();
8593

8694
resetMockGroupIdCounter();
8795

88-
if (unmockBoundingClientRect) {
89-
unmockBoundingClientRect();
90-
}
91-
92-
if (unmockResizeObserver) {
93-
unmockResizeObserver();
94-
}
96+
unmockBoundingClientRect();
97+
unmockGetComputedStyle();
98+
unmockResizeObserver();
9599
});

0 commit comments

Comments
 (0)