Skip to content

Commit 61e516f

Browse files
SabbeAndresbvaughn
andauthored
Introduced autoResize prop in Panel component (#677)
This pull request introduces a new auto-resizing behavior for panels based on issue #675, allowing individual panels to opt out of automatic resizing when the container or window size changes. This is achieved by adding an `autoResize` prop to the `Panel` component and updating the core layout logic to respect this setting. Panels with `autoResize={false}` will maintain their pixel size while other panels absorb size changes. The update also includes new documentation, types, test coverage, and example routes to demonstrate the feature. **Panel auto-resize feature:** * Added an `autoResize` prop to the `Panel` component, allowing panels to opt out of resizing when the group/container size changes. Panels with `autoResize={false}` keep their pixel size stable, while siblings absorb the remaining delta. * Updated panel types (`PanelProps`, `PanelConstraints`, etc.) and internal state to support the new `autoResize` option. **Layout and state management:** * Modified the group mounting and layout validation logic to preserve the pixel size of panels with `autoResize={false}` across group size changes. This includes a new helper function, `preserveNonAutoResizingPanelSizes`, and updates to state propagation throughout the layout system. **Documentation and examples:** * Updated the documentation (`README.md`) to describe the new `autoResize` prop and its usage. * Added new example routes and navigation links to demonstrate the fixed sidebar panel feature, including a new `AutoResizeSidebarRoute`. **Testing:** * Added a new test (`mountGroup.autoResize.test.ts`) to verify that panels with `autoResize={false}` preserve their pixel size when the group size changes. --------- Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
1 parent 5b01d38 commit 61e516f

20 files changed

Lines changed: 469 additions & 13 deletions

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.7.0
4+
5+
= [677](https://github.com/bvaughn/react-resizable-panels/pull/677): Add `groupResizeBehavior` prop to `Panel`, enabling panels to retain their current size (pixels) size when the parent `Group` is resized.
6+
37
## 4.6.5
48

59
- [670](https://github.com/bvaughn/react-resizable-panels/pull/670): Check for undefined `adoptedStyleSheets` (to better support environments like jsdom)

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@ Falls back to <code>useId</code> when not provided.</p>
245245
<tr>
246246
<td>elementRef</td>
247247
<td><p>Ref attached to the root <code>HTMLDivElement</code>.</p>
248+
</td>
249+
</tr>
250+
<tr>
251+
<td>groupResizeBehavior</td>
252+
<td><p>How should this Panel behave if the parent Group is resized?
253+
Defaults to <code>preserve-relative-size</code>.</p>
254+
<ul>
255+
<li><code>preserve-relative-size</code>: Retain the current relative size (as a percentage of the Group)</li>
256+
<li><code>preserve-pixel-size</code>: Retain its current size (in pixels)</li>
257+
</ul>
258+
<p>ℹ️ Panel min/max size constraints may impact this behavior.</p>
259+
<p>⚠️ A Group must contain at least one Panel with <code>preserve-relative-size</code> resize behavior.</p>
248260
</td>
249261
</tr>
250262
<tr>

integrations/tests/tests/resize-events.spec.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,106 @@ test.describe("resize events", () => {
208208
onLayoutChangedCount: 2
209209
});
210210
});
211+
212+
test('resizing the group should not impact panels with "preserve-pixel-size" behavior', async ({
213+
page: mainPage
214+
}) => {
215+
const page = await goToUrl(
216+
mainPage,
217+
<Group>
218+
<Panel
219+
defaultSize="30%"
220+
groupResizeBehavior="preserve-pixel-size"
221+
id="left"
222+
minSize={250}
223+
/>
224+
<Separator />
225+
<Panel id="right" minSize={50} />
226+
</Group>
227+
);
228+
229+
await expectLayout({
230+
layout: {
231+
left: 30,
232+
right: 70
233+
},
234+
mainPage,
235+
onLayoutChangeCount: 1,
236+
onLayoutChangedCount: 1
237+
});
238+
await expectPanelSize({
239+
mainPage,
240+
onResizeCount: 1,
241+
panelId: "left",
242+
panelSize: {
243+
asPercentage: 30,
244+
inPixels: 293
245+
}
246+
});
247+
await expectPanelSize({
248+
mainPage,
249+
onResizeCount: 1,
250+
panelId: "right",
251+
panelSize: {
252+
asPercentage: 70,
253+
inPixels: 683
254+
}
255+
});
256+
257+
await page.setViewportSize({
258+
width: 500,
259+
height: 500
260+
});
261+
262+
await expectLayout({
263+
layout: {
264+
left: 62,
265+
right: 38
266+
},
267+
mainPage,
268+
onLayoutChangeCount: 2,
269+
onLayoutChangedCount: 2
270+
});
271+
await expectPanelSize({
272+
mainPage,
273+
onResizeCount: 3,
274+
panelId: "left",
275+
panelSize: {
276+
asPercentage: 62,
277+
inPixels: 293
278+
},
279+
prevPanelSize: {
280+
asPercentage: 30,
281+
inPixels: 143
282+
}
283+
});
284+
await expectPanelSize({
285+
mainPage,
286+
onResizeCount: 3,
287+
panelId: "right",
288+
panelSize: {
289+
asPercentage: 38,
290+
inPixels: 183
291+
},
292+
prevPanelSize: {
293+
asPercentage: 70,
294+
inPixels: 333
295+
}
296+
});
297+
298+
await page.setViewportSize({
299+
width: 1000,
300+
height: 500
301+
});
302+
303+
await expectLayout({
304+
layout: {
305+
left: 30,
306+
right: 70
307+
},
308+
mainPage,
309+
onLayoutChangeCount: 3,
310+
onLayoutChangedCount: 3
311+
});
312+
});
211313
});

lib/components/group/Group.test.tsx

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render } from "@testing-library/react";
1+
import { act, render } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
33
import {
44
createRef,
@@ -217,6 +217,128 @@ describe("Group", () => {
217217
});
218218
});
219219

220+
describe("groupResizeBehavior", () => {
221+
function mockElementBounds({
222+
groupSize,
223+
leftPanelSize,
224+
rightPanelSize
225+
}: {
226+
groupSize: number;
227+
leftPanelSize: number;
228+
rightPanelSize: number;
229+
}) {
230+
setElementBoundsFunction((element: HTMLElement) => {
231+
switch (element.id) {
232+
case "group": {
233+
return new DOMRect(0, 0, groupSize, 50);
234+
}
235+
case "left": {
236+
return new DOMRect(0, 0, leftPanelSize, 50);
237+
}
238+
case "right": {
239+
return new DOMRect(leftPanelSize, 0, rightPanelSize, 50);
240+
}
241+
}
242+
});
243+
}
244+
245+
test("preserve-relative-size preserves relative (percentage based) size Group size changes", async () => {
246+
mockElementBounds({
247+
groupSize: 1,
248+
leftPanelSize: 1,
249+
rightPanelSize: 1
250+
});
251+
252+
const groupRef = createRef<GroupImperativeHandle | null>();
253+
const onLayoutChanged = vi.fn();
254+
255+
render(
256+
<Group groupRef={groupRef} onLayoutChanged={onLayoutChanged}>
257+
<Panel
258+
defaultSize="25%"
259+
groupResizeBehavior="preserve-relative-size"
260+
id="left"
261+
/>
262+
<Panel id="right" />
263+
</Group>
264+
);
265+
266+
expect(onLayoutChanged).toBeCalledTimes(1);
267+
expect(onLayoutChanged).toHaveBeenLastCalledWith({
268+
left: 25,
269+
right: 75
270+
});
271+
expect(groupRef.current?.getLayout()).toEqual({
272+
left: 25,
273+
right: 75
274+
});
275+
276+
await act(() => {
277+
mockElementBounds({
278+
groupSize: 2,
279+
leftPanelSize: 1,
280+
rightPanelSize: 1
281+
});
282+
});
283+
284+
expect(groupRef.current?.getLayout()).toEqual({
285+
left: 25,
286+
right: 75
287+
});
288+
expect(onLayoutChanged).toBeCalledTimes(1);
289+
});
290+
291+
test("preserve-pixel-size preserves pixel size Group size changes", async () => {
292+
mockElementBounds({
293+
groupSize: 800,
294+
leftPanelSize: 200,
295+
rightPanelSize: 600
296+
});
297+
298+
const groupRef = createRef<GroupImperativeHandle | null>();
299+
const onLayoutChanged = vi.fn();
300+
301+
render(
302+
<Group groupRef={groupRef} onLayoutChanged={onLayoutChanged}>
303+
<Panel
304+
defaultSize="25%"
305+
groupResizeBehavior="preserve-pixel-size"
306+
id="left"
307+
/>
308+
<Panel id="right" />
309+
</Group>
310+
);
311+
312+
expect(onLayoutChanged).toBeCalledTimes(1);
313+
expect(onLayoutChanged).toHaveBeenLastCalledWith({
314+
left: 25,
315+
right: 75
316+
});
317+
expect(groupRef.current?.getLayout()).toEqual({
318+
left: 25,
319+
right: 75
320+
});
321+
322+
await act(() => {
323+
mockElementBounds({
324+
groupSize: 1000,
325+
leftPanelSize: 200,
326+
rightPanelSize: 800
327+
});
328+
});
329+
330+
expect(onLayoutChanged).toBeCalledTimes(2);
331+
expect(onLayoutChanged).toHaveBeenLastCalledWith({
332+
left: 20,
333+
right: 80
334+
});
335+
expect(groupRef.current?.getLayout()).toEqual({
336+
left: 20,
337+
right: 80
338+
});
339+
});
340+
});
341+
220342
describe("defaultLayout", () => {
221343
test("should be ignored if it does not match Panel ids", () => {
222344
const onLayoutChange = vi.fn();

lib/components/panel/Panel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function Panel({
4949
defaultSize,
5050
disabled,
5151
elementRef: elementRefProp,
52+
groupResizeBehavior = "preserve-relative-size",
5253
id: idProp,
5354
maxSize = "100%",
5455
minSize = "0%",
@@ -102,6 +103,7 @@ export function Panel({
102103
},
103104
onResize: hasOnResize ? onResizeStable : undefined,
104105
panelConstraints: {
106+
groupResizeBehavior,
105107
collapsedSize,
106108
collapsible,
107109
defaultSize,
@@ -114,6 +116,7 @@ export function Panel({
114116
return registerPanel(registeredPanel);
115117
}
116118
}, [
119+
groupResizeBehavior,
117120
collapsedSize,
118121
collapsible,
119122
defaultSize,

lib/components/panel/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export type PanelSize = {
55
inPixels: number;
66
};
77

8+
export type GroupResizeBehavior =
9+
| "preserve-relative-size"
10+
| "preserve-pixel-size";
11+
812
/**
913
* Numeric Panel constraints are represented as numeric percentages (0..100)
1014
* Values specified using other CSS units must be pre-converted.
@@ -14,6 +18,7 @@ export type PanelConstraints = {
1418
collapsible: boolean;
1519
defaultSize: number | undefined;
1620
disabled: boolean | undefined;
21+
groupResizeBehavior?: GroupResizeBehavior | undefined;
1722
maxSize: number;
1823
minSize: number;
1924
panelId: string;
@@ -126,6 +131,22 @@ export type PanelProps = BasePanelAttributes & {
126131
*/
127132
elementRef?: Ref<HTMLDivElement | null> | undefined;
128133

134+
/**
135+
* How should this Panel behave if the parent Group is resized?
136+
* Defaults to `preserve-relative-size`.
137+
*
138+
* - `preserve-relative-size`: Retain the current relative size (as a percentage of the Group)
139+
* - `preserve-pixel-size`: Retain its current size (in pixels)
140+
*
141+
* ℹ️ Panel min/max size constraints may impact this behavior.
142+
*
143+
* ⚠️ A Group must contain at least one Panel with `preserve-relative-size` resize behavior.
144+
*/
145+
groupResizeBehavior?:
146+
| "preserve-relative-size"
147+
| "preserve-pixel-size"
148+
| undefined;
149+
129150
/**
130151
* Uniquely identifies this panel within the parent group.
131152
* Falls back to `useId` when not provided.
@@ -204,6 +225,7 @@ export type PanelConstraintProps = Pick<
204225
| "collapsible"
205226
| "defaultSize"
206227
| "disabled"
228+
| "groupResizeBehavior"
207229
| "maxSize"
208230
| "minSize"
209231
>;

lib/global/dom/calculatePanelConstraints.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export function calculatePanelConstraints(group: RegisteredGroup) {
1111
if (groupSize === 0) {
1212
// Can't calculate anything meaningful if the group has a width/height of 0
1313
// (This could indicate that it's within a hidden subtree)
14-
return panels.map((current) => ({
14+
return panels.map<PanelConstraints>((current) => ({
15+
groupResizeBehavior: current.panelConstraints.groupResizeBehavior,
1516
collapsedSize: 0,
1617
collapsible: current.panelConstraints.collapsible === true,
1718
defaultSize: undefined,
@@ -70,6 +71,7 @@ export function calculatePanelConstraints(group: RegisteredGroup) {
7071
}
7172

7273
return {
74+
groupResizeBehavior: panelConstraints.groupResizeBehavior,
7375
collapsedSize,
7476
collapsible: panelConstraints.collapsible === true,
7577
defaultSize,

0 commit comments

Comments
 (0)