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);
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": [