Skip to content

Commit 6db7cc9

Browse files
authored
Ignore defaultLayouts with mismatching Panel ids (#556)
1 parent 281b343 commit 6db7cc9

5 files changed

Lines changed: 112 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 4.0.15
4+
5+
- [556](https://github.com/bvaughn/react-resizable-panels/pull/556): Ignore `defaultLayout` when keys don't match Panel ids
6+
37
## 4.0.14
48

59
- [555](https://github.com/bvaughn/react-resizable-panels/pull/555): Allow resizable panels to be rendered into a different Window (e.g. popup or frame) by accessing globals through `element.ownerDocument.defaultView`

lib/components/group/Group.test.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
33
import { eventEmitter } from "../../global/mutableState";
44
import { moveSeparator } from "../../global/test/moveSeparator";
55
import { assert } from "../../utils/assert";
6-
import { setElementBoundsFunction } from "../../utils/test/mockBoundingClientRect";
6+
import {
7+
setDefaultElementBounds,
8+
setElementBoundsFunction
9+
} from "../../utils/test/mockBoundingClientRect";
710
import { Panel } from "../panel/Panel";
811
import { Separator } from "../separator/Separator";
912
import { Group } from "./Group";
@@ -81,6 +84,60 @@ describe("Group", () => {
8184
);
8285
});
8386

87+
describe("defaultLayout", () => {
88+
test("should be ignored if it does not match Panel ids", () => {
89+
const onLayoutChange = vi.fn();
90+
91+
setDefaultElementBounds(new DOMRect(0, 0, 100, 50));
92+
93+
render(
94+
<Group
95+
defaultLayout={{
96+
foo: 40,
97+
bar: 60
98+
}}
99+
onLayoutChange={onLayoutChange}
100+
>
101+
<Panel id="bar" />
102+
<Panel id="baz" />
103+
</Group>
104+
);
105+
106+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
107+
expect(onLayoutChange).toHaveBeenCalledWith({
108+
bar: 50,
109+
baz: 50
110+
});
111+
});
112+
113+
test("should be ignored if it does not match Panel ids (mounted within hidden subtree)", () => {
114+
const onLayoutChange = vi.fn();
115+
116+
render(
117+
<Group
118+
defaultLayout={{
119+
foo: 40,
120+
bar: 60
121+
}}
122+
onLayoutChange={onLayoutChange}
123+
>
124+
<Panel id="bar" />
125+
<Panel id="baz" />
126+
</Group>
127+
);
128+
129+
expect(onLayoutChange).not.toHaveBeenCalled();
130+
131+
setDefaultElementBounds(new DOMRect(0, 0, 100, 50));
132+
133+
expect(onLayoutChange).toHaveBeenCalledTimes(1);
134+
expect(onLayoutChange).toHaveBeenCalledWith({
135+
bar: 50,
136+
baz: 50
137+
});
138+
});
139+
});
140+
84141
describe("groupRef", () => {
85142
test("should work with an empty Group", () => {
86143
const onLayoutChange = vi.fn();

lib/components/group/useDefaultLayout.test.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { render, renderHook } from "@testing-library/react";
22
import { createRef } from "react";
33
import { describe, expect, test, vi } from "vitest";
4+
import { setDefaultElementBounds } from "../../utils/test/mockBoundingClientRect";
45
import { Panel } from "../panel/Panel";
56
import { Group } from "./Group";
6-
import type { GroupImperativeHandle, LayoutStorage } from "./types";
7+
import { type GroupImperativeHandle, type LayoutStorage } from "./types";
78
import { useDefaultLayout } from "./useDefaultLayout";
8-
import { setDefaultElementBounds } from "../../utils/test/mockBoundingClientRect";
99

1010
describe("useDefaultLayout", () => {
1111
test("should read/write from the provided Storage API", () => {
@@ -83,7 +83,7 @@ describe("useDefaultLayout", () => {
8383
});
8484

8585
// See github.com/bvaughn/react-resizable-panels/pull/540
86-
test("should not break when coupled with dynamic layouts", () => {
86+
test("should ignore invalid layouts (num panels mismatch)", () => {
8787
const groupRef = createRef<GroupImperativeHandle>();
8888

8989
setDefaultElementBounds(new DOMRect(0, 0, 100, 50));
@@ -134,4 +134,30 @@ describe("useDefaultLayout", () => {
134134
}
135135
`);
136136
});
137+
138+
test("should ignore invalid layouts (panel ids mismatch)", () => {
139+
setDefaultElementBounds(new DOMRect(0, 0, 100, 50));
140+
141+
const groupRef = createRef<GroupImperativeHandle>();
142+
143+
render(
144+
<Group
145+
defaultLayout={{
146+
foo: 40,
147+
bar: 60
148+
}}
149+
groupRef={groupRef}
150+
>
151+
<Panel id="bar" defaultSize="30%" />
152+
<Panel id="baz" />
153+
</Group>
154+
);
155+
156+
expect(groupRef.current?.getLayout()).toMatchInlineSnapshot(`
157+
{
158+
"bar": 30,
159+
"baz": 70,
160+
}
161+
`);
162+
});
137163
});

lib/global/mountGroup.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { calculateDefaultLayout } from "./utils/calculateDefaultLayout";
1313
import { layoutsEqual } from "./utils/layoutsEqual";
1414
import { notifyPanelOnResize } from "./utils/notifyPanelOnResize";
1515
import { objectsEqual } from "./utils/objectsEqual";
16+
import { validateLayoutKeys } from "./utils/validateLayoutKeys";
1617
import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout";
1718

1819
const ownerDocumentReferenceCounts = new Map<Document, number>();
@@ -114,7 +115,7 @@ export function mountGroup(group: RegisteredGroup) {
114115
// In this case the best we can do is ignore the incoming layout
115116
let defaultLayout: Layout | undefined = group.defaultLayout;
116117
if (defaultLayout) {
117-
if (group.panels.length !== Object.keys(defaultLayout).length) {
118+
if (!validateLayoutKeys(group.panels, defaultLayout)) {
118119
defaultLayout = undefined;
119120
}
120121
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Layout } from "../../components/group/types";
2+
import type { RegisteredPanel } from "../../components/panel/types";
3+
4+
export function validateLayoutKeys(panels: RegisteredPanel[], layout: Layout) {
5+
const panelIds = panels.map((panel) => panel.id);
6+
const layoutKeys = Object.keys(layout);
7+
8+
if (panelIds.length !== layoutKeys.length) {
9+
return false;
10+
}
11+
12+
for (const panelId of panelIds) {
13+
if (!layoutKeys.includes(panelId)) {
14+
return false;
15+
}
16+
}
17+
18+
return true;
19+
}

0 commit comments

Comments
 (0)