From cabb73b8c3ae814b5498e9a21ee4eb00b53dfcb8 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 6 Feb 2026 14:59:54 +0000 Subject: [PATCH 1/2] `Group` guards against layouts with mis-ordered `Panel` id keys (#660) Support `defaultLayout` objects with different key/id orders than their corresponding panels by pre-sorting them. Resolves #656 --- CHANGELOG.md | 4 + lib/components/group/Group.test.tsx | 139 ++++++++++++++++-- lib/components/group/Group.tsx | 19 ++- .../group/useDefaultLayout.test.tsx | 30 ++++ lib/global/test/moveSeparator.ts | 4 +- 5 files changed, 182 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07173f19d..79720a437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.6.2 + +- [660](https://github.com/bvaughn/react-resizable-panels/pull/660): `Group` guards against layouts with mis-ordered `Panel` id keys + ## 4.6.1 - [658](https://github.com/bvaughn/react-resizable-panels/pull/658): Imperative `Panel` and `Group` APIs ignored `disabled` status when resizing panels; this is an explicit override of the _disabled_ state and is required to support conditionally disabled groups. diff --git a/lib/components/group/Group.test.tsx b/lib/components/group/Group.test.tsx index c8fa3aee1..b4c59e029 100644 --- a/lib/components/group/Group.test.tsx +++ b/lib/components/group/Group.test.tsx @@ -101,20 +101,20 @@ describe("Group", () => { render( - - + + ); expect(onLayoutChange).toHaveBeenCalledTimes(1); expect(onLayoutChange).toHaveBeenCalledWith({ - bar: 50, - baz: 50 + left: 50, + right: 50 }); }); @@ -124,13 +124,13 @@ describe("Group", () => { render( - - + + ); @@ -140,8 +140,123 @@ describe("Group", () => { expect(onLayoutChange).toHaveBeenCalledTimes(1); expect(onLayoutChange).toHaveBeenCalledWith({ - bar: 50, - baz: 50 + left: 50, + right: 50 + }); + }); + + describe("should not break the layout if layout Panel ids are in an unexpected order", () => { + test("two panel horizontal group", async () => { + const onLayoutChanged = vi.fn(); + + setElementBoundsFunction((element) => { + switch (element.id) { + case "group": { + return new DOMRect(0, 0, 100, 50); + } + case "left": { + return new DOMRect(0, 0, 50, 50); + } + case "right": { + return new DOMRect(50, 0, 50, 50); + } + case "separator": { + return new DOMRect(50, 0, 0, 50); + } + } + }); + + render( + + + + + + ); + + expect(onLayoutChanged).toHaveBeenCalledTimes(1); + expect(onLayoutChanged).toHaveBeenCalledWith({ + left: 60, + right: 40 + }); + + // Simulate a drag from the draggable element to the target area + await moveSeparator(10); + + expect(onLayoutChanged).toHaveBeenCalledTimes(2); + expect(onLayoutChanged).toHaveBeenCalledWith({ + left: 70, + right: 30 + }); + }); + + test("three panel vertical group", async () => { + const onLayoutChanged = vi.fn(); + + setElementBoundsFunction((element) => { + switch (element.id) { + case "group": { + return new DOMRect(0, 0, 50, 150); + } + case "top": { + return new DOMRect(0, 0, 50, 50); + } + case "top-separator": { + return new DOMRect(0, 50, 0, 50); + } + case "middle": { + return new DOMRect(0, 50, 50, 50); + } + case "bottom": { + return new DOMRect(0, 100, 50, 50); + } + case "bottom-separator": { + return new DOMRect(0, 100, 0, 50); + } + } + }); + + render( + + + + + + + + ); + + expect(onLayoutChanged).toHaveBeenCalledTimes(1); + expect(onLayoutChanged).toHaveBeenCalledWith({ + bottom: 50, + middle: 30, + top: 20 + }); + + // Simulate a drag from the draggable element to the target area + await moveSeparator(15, "top-separator"); + + expect(onLayoutChanged).toHaveBeenCalledTimes(2); + expect(onLayoutChanged).toHaveBeenCalledWith({ + bottom: 50, + middle: 20, + top: 30 + }); }); }); diff --git a/lib/components/group/Group.tsx b/lib/components/group/Group.tsx index 6cb833ce9..b5d67c39d 100644 --- a/lib/components/group/Group.tsx +++ b/lib/components/group/Group.tsx @@ -231,8 +231,25 @@ export function Group({ const inMemoryValues = inMemoryValuesRef.current; + // Guard against unexpected layout attribute ordering by pre-sorting panel ids/keys; see issues/656 + let preSortedDefaultLayout: Layout | undefined = undefined; + if (stableProps.defaultLayout !== undefined) { + if ( + Object.keys(stableProps.defaultLayout).length === + inMemoryValues.panels.length + ) { + preSortedDefaultLayout = {}; + for (const panel of inMemoryValues.panels) { + const size = stableProps.defaultLayout[panel.id]; + if (size !== undefined) { + preSortedDefaultLayout[panel.id] = size; + } + } + } + } + const group: RegisteredGroup = { - defaultLayout: stableProps.defaultLayout, + defaultLayout: preSortedDefaultLayout, disableCursor: !!stableProps.disableCursor, disabled: !!disabled, element, diff --git a/lib/components/group/useDefaultLayout.test.tsx b/lib/components/group/useDefaultLayout.test.tsx index 8f7a01079..166dc4ce4 100644 --- a/lib/components/group/useDefaultLayout.test.tsx +++ b/lib/components/group/useDefaultLayout.test.tsx @@ -261,6 +261,36 @@ describe("useDefaultLayout", () => { } `); }); + + // See https://github.com/bvaughn/react-resizable-panels/issues/656 + test("should support out-of-order panel ids", () => { + setDefaultElementBounds(new DOMRect(0, 0, 100, 50)); + + const groupRef = createRef(); + + render( + + + + + + ); + + expect(groupRef.current?.getLayout()).toMatchInlineSnapshot(` + { + "bottom": 50, + "middle": 30, + "top": 20, + } + `); + }); }); describe("legacy onLayoutChange prop", () => { diff --git a/lib/global/test/moveSeparator.ts b/lib/global/test/moveSeparator.ts index 49627b967..b052e2093 100644 --- a/lib/global/test/moveSeparator.ts +++ b/lib/global/test/moveSeparator.ts @@ -6,7 +6,9 @@ export async function moveSeparator( separatorId?: string ) { const separatorElement = document.querySelector( - separatorId ? `[data-separator="${separatorId}"]` : "[data-separator]" + separatorId + ? `[data-separator][data-testid="${separatorId}"]` + : "[data-separator]" ); assert(separatorElement instanceof HTMLElement); From b4ece6ffcb343bdc90ec4c3805095ec87b39ee83 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 6 Feb 2026 10:01:02 -0500 Subject: [PATCH 2/2] 4.6.1 -> 4.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc3518877..5218d079a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-resizable-panels", - "version": "4.6.1", + "version": "4.6.2", "type": "module", "author": "Brian Vaughn (https://github.com/bvaughn/)", "contributors": [