Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 4.0.15

- [556](https://github.com/bvaughn/react-resizable-panels/pull/556): Ignore `defaultLayout` when keys don't match Panel ids

## 4.0.14

- [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`
Expand Down
59 changes: 58 additions & 1 deletion lib/components/group/Group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
import { eventEmitter } from "../../global/mutableState";
import { moveSeparator } from "../../global/test/moveSeparator";
import { assert } from "../../utils/assert";
import { setElementBoundsFunction } from "../../utils/test/mockBoundingClientRect";
import {
setDefaultElementBounds,
setElementBoundsFunction
} from "../../utils/test/mockBoundingClientRect";
import { Panel } from "../panel/Panel";
import { Separator } from "../separator/Separator";
import { Group } from "./Group";
Expand Down Expand Up @@ -81,6 +84,60 @@ describe("Group", () => {
);
});

describe("defaultLayout", () => {
test("should be ignored if it does not match Panel ids", () => {
const onLayoutChange = vi.fn();

setDefaultElementBounds(new DOMRect(0, 0, 100, 50));

render(
<Group
defaultLayout={{
foo: 40,
bar: 60
}}
onLayoutChange={onLayoutChange}
>
<Panel id="bar" />
<Panel id="baz" />
</Group>
);

expect(onLayoutChange).toHaveBeenCalledTimes(1);
expect(onLayoutChange).toHaveBeenCalledWith({
bar: 50,
baz: 50
});
});

test("should be ignored if it does not match Panel ids (mounted within hidden subtree)", () => {
const onLayoutChange = vi.fn();

render(
<Group
defaultLayout={{
foo: 40,
bar: 60
}}
onLayoutChange={onLayoutChange}
>
<Panel id="bar" />
<Panel id="baz" />
</Group>
);

expect(onLayoutChange).not.toHaveBeenCalled();

setDefaultElementBounds(new DOMRect(0, 0, 100, 50));

expect(onLayoutChange).toHaveBeenCalledTimes(1);
expect(onLayoutChange).toHaveBeenCalledWith({
bar: 50,
baz: 50
});
});
});

describe("groupRef", () => {
test("should work with an empty Group", () => {
const onLayoutChange = vi.fn();
Expand Down
32 changes: 29 additions & 3 deletions lib/components/group/useDefaultLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { render, renderHook } from "@testing-library/react";
import { createRef } from "react";
import { describe, expect, test, vi } from "vitest";
import { setDefaultElementBounds } from "../../utils/test/mockBoundingClientRect";
import { Panel } from "../panel/Panel";
import { Group } from "./Group";
import type { GroupImperativeHandle, LayoutStorage } from "./types";
import { type GroupImperativeHandle, type LayoutStorage } from "./types";
import { useDefaultLayout } from "./useDefaultLayout";
import { setDefaultElementBounds } from "../../utils/test/mockBoundingClientRect";

describe("useDefaultLayout", () => {
test("should read/write from the provided Storage API", () => {
Expand Down Expand Up @@ -83,7 +83,7 @@ describe("useDefaultLayout", () => {
});

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

setDefaultElementBounds(new DOMRect(0, 0, 100, 50));
Expand Down Expand Up @@ -134,4 +134,30 @@ describe("useDefaultLayout", () => {
}
`);
});

test("should ignore invalid layouts (panel ids mismatch)", () => {
setDefaultElementBounds(new DOMRect(0, 0, 100, 50));

const groupRef = createRef<GroupImperativeHandle>();

render(
<Group
defaultLayout={{
foo: 40,
bar: 60
}}
groupRef={groupRef}
>
<Panel id="bar" defaultSize="30%" />
<Panel id="baz" />
</Group>
);

expect(groupRef.current?.getLayout()).toMatchInlineSnapshot(`
{
"bar": 30,
"baz": 70,
}
`);
});
});
3 changes: 2 additions & 1 deletion lib/global/mountGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { calculateDefaultLayout } from "./utils/calculateDefaultLayout";
import { layoutsEqual } from "./utils/layoutsEqual";
import { notifyPanelOnResize } from "./utils/notifyPanelOnResize";
import { objectsEqual } from "./utils/objectsEqual";
import { validateLayoutKeys } from "./utils/validateLayoutKeys";
import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout";

const ownerDocumentReferenceCounts = new Map<Document, number>();
Expand Down Expand Up @@ -114,7 +115,7 @@ export function mountGroup(group: RegisteredGroup) {
// In this case the best we can do is ignore the incoming layout
let defaultLayout: Layout | undefined = group.defaultLayout;
if (defaultLayout) {
if (group.panels.length !== Object.keys(defaultLayout).length) {
if (!validateLayoutKeys(group.panels, defaultLayout)) {
defaultLayout = undefined;
}
}
Expand Down
19 changes: 19 additions & 0 deletions lib/global/utils/validateLayoutKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Layout } from "../../components/group/types";
import type { RegisteredPanel } from "../../components/panel/types";

export function validateLayoutKeys(panels: RegisteredPanel[], layout: Layout) {
const panelIds = panels.map((panel) => panel.id);
const layoutKeys = Object.keys(layout);

if (panelIds.length !== layoutKeys.length) {
return false;
}

for (const panelId of panelIds) {
if (!layoutKeys.includes(panelId)) {
return false;
}
}

return true;
}
Loading