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.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.
Expand Down
139 changes: 127 additions & 12 deletions lib/components/group/Group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,20 @@ describe("Group", () => {
render(
<Group
defaultLayout={{
foo: 40,
bar: 60
top: 40,
bottom: 60
}}
onLayoutChange={onLayoutChange}
>
<Panel id="bar" />
<Panel id="baz" />
<Panel id="left" />
<Panel id="right" />
</Group>
);

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

Expand All @@ -124,13 +124,13 @@ describe("Group", () => {
render(
<Group
defaultLayout={{
foo: 40,
bar: 60
top: 40,
bottom: 60
}}
onLayoutChange={onLayoutChange}
>
<Panel id="bar" />
<Panel id="baz" />
<Panel id="left" />
<Panel id="right" />
</Group>
);

Expand All @@ -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(
<Group
defaultLayout={{
right: 40,
left: 60
}}
id="group"
onLayoutChanged={onLayoutChanged}
>
<Panel id="left" />
<Separator id="separator" />
<Panel id="right" />
</Group>
);

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(
<Group
defaultLayout={{
bottom: 50,
middle: 30,
top: 20
}}
onLayoutChanged={onLayoutChanged}
orientation="vertical"
>
<Panel id="top" defaultSize="30%" />
<Separator id="top-separator" />
<Panel id="middle" />
<Separator id="bottom-separator" />
<Panel id="bottom" defaultSize="30%" />
</Group>
);

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
});
});
});

Expand Down
19 changes: 18 additions & 1 deletion lib/components/group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions lib/components/group/useDefaultLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<GroupImperativeHandle>();

render(
<Group
defaultLayout={{
bottom: 50,
middle: 30,
top: 20
}}
groupRef={groupRef}
>
<Panel id="top" defaultSize="30%" />
<Panel id="middle" />
<Panel id="bottom" defaultSize="30%" />
</Group>
);

expect(groupRef.current?.getLayout()).toMatchInlineSnapshot(`
{
"bottom": 50,
"middle": 30,
"top": 20,
}
`);
});
});

describe("legacy onLayoutChange prop", () => {
Expand Down
4 changes: 3 additions & 1 deletion lib/global/test/moveSeparator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
"version": "4.6.1",
"version": "4.6.2",
"type": "module",
"author": "Brian Vaughn <brian.david.vaughn@gmail.com> (https://github.com/bvaughn/)",
"contributors": [
Expand Down
Loading