Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ Falls back to <code>useId</code> when not provided.</p>
<td>style</td>
<td><p>CSS properties.</p>
<p>⚠️ Style is applied to nested <code>HTMLDivElement</code> to avoid styles that interfere with Flex layout.</p>
</td>
</tr>
<tr>
<td>autoResize</td>
<td><p>Whether this panel should resize automatically when the Group size changes.</p>
<p>Defaults to true.</p>
<p>Set to false to keep this panel&#39;s pixel size stable on window/container resize,
while sibling panels absorb the remaining delta.</p>
</td>
</tr>
<tr>
Expand Down
3 changes: 3 additions & 0 deletions lib/components/panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { usePanelImperativeHandle } from "./usePanelImperativeHandle";
* ⚠️ Panel elements must be direct DOM children of their parent Group elements.
*/
export function Panel({
autoResize = true,
children,
className,
collapsedSize = "0%",
Expand Down Expand Up @@ -102,6 +103,7 @@ export function Panel({
},
onResize: hasOnResize ? onResizeStable : undefined,
panelConstraints: {
autoResize,
collapsedSize,
collapsible,
defaultSize,
Expand All @@ -114,6 +116,7 @@ export function Panel({
return registerPanel(registeredPanel);
}
}, [
autoResize,
collapsedSize,
collapsible,
defaultSize,
Expand Down
12 changes: 12 additions & 0 deletions lib/components/panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type PanelSize = {
* Values specified using other CSS units must be pre-converted.
*/
export type PanelConstraints = {
autoResize?: boolean;
collapsedSize: number;
collapsible: boolean;
defaultSize: number | undefined;
Expand Down Expand Up @@ -92,6 +93,16 @@ export interface PanelImperativeHandle {
type BasePanelAttributes = Omit<HTMLAttributes<HTMLDivElement>, "onResize">;

export type PanelProps = BasePanelAttributes & {
/**
* Whether this panel should resize automatically when the Group size changes.
*
* Defaults to true.
*
* Set to false to keep this panel's pixel size stable on window/container resize,
* while sibling panels absorb the remaining delta.
*/
autoResize?: boolean | undefined;
Comment thread
bvaughn marked this conversation as resolved.
Outdated

/**
* CSS class name.
*
Expand Down Expand Up @@ -202,6 +213,7 @@ export type PanelConstraintProps = Pick<
PanelProps,
| "collapsedSize"
| "collapsible"
| "autoResize"
| "defaultSize"
| "disabled"
| "maxSize"
Expand Down
2 changes: 2 additions & 0 deletions lib/global/dom/calculatePanelConstraints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function calculatePanelConstraints(group: RegisteredGroup) {
// Can't calculate anything meaningful if the group has a width/height of 0
// (This could indicate that it's within a hidden subtree)
return panels.map((current) => ({
autoResize: current.panelConstraints.autoResize !== false,
collapsedSize: 0,
collapsible: current.panelConstraints.collapsible === true,
defaultSize: undefined,
Expand Down Expand Up @@ -70,6 +71,7 @@ export function calculatePanelConstraints(group: RegisteredGroup) {
}

return {
autoResize: panelConstraints.autoResize !== false,
collapsedSize,
collapsible: panelConstraints.collapsible === true,
defaultSize,
Expand Down
48 changes: 48 additions & 0 deletions lib/global/mountGroup.autoResize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from "vitest";
import { mountGroup } from "./mountGroup";
import { getMountedGroupState } from "./mutable-state/groups";
import { mockGroup } from "./test/mockGroup";
import { setElementBounds } from "../utils/test/mockBoundingClientRect";

describe("mountGroup autoResize", () => {
test("preserves pixel size for panels with autoResize=false when group size changes", () => {
const group = mockGroup(new DOMRect(0, 0, 1000, 50), {
id: "group",
orientation: "horizontal"
});

group.addPanel(new DOMRect(0, 0, 200, 50), "left", {
autoResize: false,
defaultSize: 200
});
group.addPanel(new DOMRect(200, 0, 800, 50), "right", {
defaultSize: 800
});

const unmountGroup = mountGroup(group);

try {
const initialState = getMountedGroupState("group", true);
expect(initialState.layout).toEqual({
"group-left": 20,
"group-right": 80
});

setElementBounds(group.panels[0].element, new DOMRect(0, 0, 200, 50));
setElementBounds(group.panels[1].element, new DOMRect(200, 0, 1000, 50));
setElementBounds(group.element, new DOMRect(0, 0, 1200, 50));

const nextState = getMountedGroupState("group", true);

expect(nextState.groupSize).toBe(1200);

const leftSizeInPixels = (nextState.layout["group-left"] / 100) * 1200;
const rightSizeInPixels = (nextState.layout["group-right"] / 100) * 1200;

expect(leftSizeInPixels).toBeCloseTo(200, 2);
expect(rightSizeInPixels).toBeCloseTo(1000, 2);
} finally {
unmountGroup();
}
});
Comment thread
SabbeAndres marked this conversation as resolved.
Outdated
});
92 changes: 88 additions & 4 deletions lib/global/mountGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "./mutable-state/groups";
import type { SeparatorToPanelsMap } from "./mutable-state/types";
import { calculateDefaultLayout } from "./utils/calculateDefaultLayout";
import { formatLayoutNumber } from "./utils/formatLayoutNumber";
import { layoutsEqual } from "./utils/layoutsEqual";
import { notifyPanelOnResize } from "./utils/notifyPanelOnResize";
import { objectsEqual } from "./utils/objectsEqual";
Expand Down Expand Up @@ -61,29 +62,37 @@ export function mountGroup(group: RegisteredGroup) {
// Update non-percentage based constraints
const nextDerivedPanelConstraints = calculatePanelConstraints(group);

// Revalidate layout in case constraints have changed
// Revalidate layout in case constraints have changed or group size changed
const prevLayout = groupState.defaultLayoutDeferred
? calculateDefaultLayout(nextDerivedPanelConstraints)
: groupState.layout;
const unsafeLayout = preserveNonAutoResizingPanelSizes({
group,
nextGroupSize: groupSize,
prevGroupSize: groupState.groupSize,
prevLayout
});
const nextLayout = validatePanelGroupLayout({
layout: prevLayout,
layout: unsafeLayout,
panelConstraints: nextDerivedPanelConstraints
});

if (
!groupState.defaultLayoutDeferred &&
layoutsEqual(prevLayout, nextLayout) &&
layoutsEqual(groupState.layout, nextLayout) &&
objectsEqual(
groupState.derivedPanelConstraints,
nextDerivedPanelConstraints
)
) &&
groupState.groupSize === groupSize
) {
return;
}

updateMountedGroup(group, {
defaultLayoutDeferred: false,
derivedPanelConstraints: nextDerivedPanelConstraints,
groupSize,
layout: nextLayout,
separatorToPanels: groupState.separatorToPanels
});
Expand Down Expand Up @@ -152,6 +161,7 @@ export function mountGroup(group: RegisteredGroup) {
updateMountedGroup(group, {
defaultLayoutDeferred: groupSize === 0,
derivedPanelConstraints,
groupSize,
layout: defaultLayoutSafe,
separatorToPanels
});
Expand Down Expand Up @@ -212,3 +222,77 @@ export function mountGroup(group: RegisteredGroup) {
resizeObserver.disconnect();
};
}

function preserveNonAutoResizingPanelSizes({
Comment thread
SabbeAndres marked this conversation as resolved.
Outdated
group,
nextGroupSize,
prevGroupSize,
prevLayout
}: {
group: RegisteredGroup;
nextGroupSize: number;
prevGroupSize: number;
prevLayout: Layout;
}) {
if (
prevGroupSize <= 0 ||
nextGroupSize <= 0 ||
prevGroupSize === nextGroupSize
) {
return prevLayout;
}

let hasFixedPanels = false;
let fixedPanelsTotalSize = 0;
let flexiblePanelsTotalPrevSize = 0;

const fixedPanels = new Map<string, number>();
const flexiblePanelIds: string[] = [];

for (const panel of group.panels) {
const prevPanelSize = prevLayout[panel.id] ?? 0;
if (panel.panelConstraints.autoResize === false) {
hasFixedPanels = true;

const prevPanelSizeInPixels = (prevPanelSize / 100) * prevGroupSize;
const nextPanelSize = formatLayoutNumber(
(prevPanelSizeInPixels / nextGroupSize) * 100
);

fixedPanels.set(panel.id, nextPanelSize);
fixedPanelsTotalSize += nextPanelSize;
} else {
flexiblePanelIds.push(panel.id);
flexiblePanelsTotalPrevSize += prevPanelSize;
}
}

if (!hasFixedPanels || flexiblePanelIds.length === 0) {
return prevLayout;
}

const remainingSize = 100 - fixedPanelsTotalSize;
const nextLayout = { ...prevLayout };

fixedPanels.forEach((size, panelId) => {
nextLayout[panelId] = size;
});

if (flexiblePanelsTotalPrevSize > 0) {
for (const panelId of flexiblePanelIds) {
const prevSize = prevLayout[panelId] ?? 0;
nextLayout[panelId] = formatLayoutNumber(
(prevSize / flexiblePanelsTotalPrevSize) * remainingSize
);
}
} else {
const evenSize = formatLayoutNumber(
remainingSize / flexiblePanelIds.length
);
for (const panelId of flexiblePanelIds) {
nextLayout[panelId] = evenSize;
}
}

return nextLayout;
}
1 change: 1 addition & 0 deletions lib/global/mutable-state/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SeparatorToPanelsMap } from "./types";
type State = {
defaultLayoutDeferred: boolean;
derivedPanelConstraints: PanelConstraints[];
groupSize: number;
layout: Layout;
separatorToPanels: SeparatorToPanelsMap;
};
Expand Down
1 change: 1 addition & 0 deletions lib/global/utils/adjustLayoutForSeparator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function adjustLayoutForSeparator(
updateMountedGroup(group, {
defaultLayoutDeferred: groupState.defaultLayoutDeferred,
derivedPanelConstraints: groupState.derivedPanelConstraints,
groupSize: groupState.groupSize,
layout: nextLayout,
separatorToPanels: groupState.separatorToPanels
});
Expand Down
2 changes: 2 additions & 0 deletions lib/global/utils/getImperativeGroupMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function getImperativeGroupMethods({
defaultLayoutDeferred,
derivedPanelConstraints,
group,
groupSize,
layout: prevLayout,
separatorToPanels
} = find();
Expand All @@ -62,6 +63,7 @@ export function getImperativeGroupMethods({
updateMountedGroup(group, {
defaultLayoutDeferred,
derivedPanelConstraints,
groupSize,
layout: nextLayout,
separatorToPanels
});
Expand Down
4 changes: 4 additions & 0 deletions lib/global/utils/getImperativePanelMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function getImperativePanelMethods({
defaultLayoutDeferred,
derivedPanelConstraints,
layout,
groupSize,
separatorToPanels
}
] of mountedGroups) {
Expand All @@ -30,6 +31,7 @@ export function getImperativePanelMethods({
defaultLayoutDeferred,
derivedPanelConstraints,
group,
groupSize,
layout,
separatorToPanels
};
Expand Down Expand Up @@ -78,6 +80,7 @@ export function getImperativePanelMethods({
defaultLayoutDeferred,
derivedPanelConstraints,
group,
groupSize,
layout: prevLayout,
separatorToPanels
} = find();
Expand All @@ -102,6 +105,7 @@ export function getImperativePanelMethods({
updateMountedGroup(group, {
defaultLayoutDeferred,
derivedPanelConstraints,
groupSize,
layout: nextLayout,
separatorToPanels
});
Expand Down
15 changes: 8 additions & 7 deletions lib/global/utils/updateActiveHitRegion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,19 @@ export function updateActiveHitRegions({
}

const initialLayout = initialLayoutMap.get(group);
const groupState = mountedGroups.get(group);
if (!initialLayout || !groupState) {
return;
}

const {
defaultLayoutDeferred,
derivedPanelConstraints,
groupSize: mountedGroupSize,
layout: prevLayout,
separatorToPanels
} = mountedGroups.get(group) ?? { defaultLayoutDeferred: false };
if (
derivedPanelConstraints &&
initialLayout &&
prevLayout &&
separatorToPanels
) {
} = groupState;
if (derivedPanelConstraints && prevLayout && separatorToPanels) {
const nextLayout = adjustLayoutByDelta({
delta: deltaAsPercentage,
initialLayout,
Expand Down Expand Up @@ -113,6 +113,7 @@ export function updateActiveHitRegions({
updateMountedGroup(current.group, {
defaultLayoutDeferred,
derivedPanelConstraints: derivedPanelConstraints,
groupSize: mountedGroupSize,
layout: nextLayout,
separatorToPanels
});
Expand Down
16 changes: 16 additions & 0 deletions public/generated/docs/Panel.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@
"name": "style",
"required": false
},
"autoResize": {
"description": [
{
"content": "<p>Whether this panel should resize automatically when the Group size changes.</p>\n"
},
{
"content": "<p>Defaults to true.</p>\n"
},
{
"content": "<p>Set to false to keep this panel's pixel size stable on window/container resize,\nwhile sibling panels absorb the remaining delta.</p>\n"
}
],
"html": "<div><span class=\"tok-variableName\"><span class=\"tok-propertyName\">autoResize</span></span><span class=\"tok-operator\">?</span><span class=\"tok-punctuation\">:</span><span class=\"\"> </span><span class=\"tok-variableName\">boolean</span><span class=\"\"> </span><span class=\"tok-operator\">=</span><span class=\"\"> </span><span class=\"tok-bool\">true</span></div>",
"name": "autoResize",
"required": false
},
"collapsedSize": {
"description": [
{
Expand Down
3 changes: 3 additions & 0 deletions public/generated/examples/AutoResizeSidebar.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"html": "<div><span class=\"tok-punctuation\">&#60;</span><span class=\"tok-typeName\">Group</span><span class=\"tok-punctuation\">&#62;</span><span class=\"\"></span></div>\n<div><span class=\"\"> </span><span class=\"tok-punctuation\">&#60;</span><span class=\"tok-typeName\">Panel</span><span class=\"\"> </span><span class=\"tok-propertyName\">autoResize</span><span class=\"tok-operator\">=</span><span class=\"tok-punctuation\">{</span><span class=\"tok-bool\">false</span><span class=\"tok-punctuation\">}</span><span class=\"\"> </span><span class=\"tok-propertyName\">defaultSize</span><span class=\"tok-operator\">=</span><span class=\"tok-punctuation\">{</span><span class=\"tok-number\">280</span><span class=\"tok-punctuation\">}</span><span class=\"\"> </span><span class=\"tok-propertyName\">minSize</span><span class=\"tok-operator\">=</span><span class=\"tok-punctuation\">{</span><span class=\"tok-number\">200</span><span class=\"tok-punctuation\">}</span><span class=\"\"> </span><span class=\"tok-propertyName\">maxSize</span><span class=\"tok-operator\">=</span><span class=\"tok-punctuation\">{</span><span class=\"tok-number\">420</span><span class=\"tok-punctuation\">}</span><span class=\"tok-punctuation\">&#62;</span><span class=\"\"></span></div>\n<div><span class=\"\"> left sidebar</span></div>\n<div><span class=\"\"> </span><span class=\"tok-punctuation\">&#60;/</span><span class=\"tok-typeName\">Panel</span><span class=\"tok-punctuation\">&#62;</span><span class=\"\"></span></div>\n<div><span class=\"\"> </span><span class=\"tok-punctuation\">&#60;</span><span class=\"tok-typeName\">Separator</span><span class=\"\"> </span><span class=\"tok-punctuation\">/&#62;</span><span class=\"\"></span></div>\n<div><span class=\"\"> </span><span class=\"tok-punctuation\">&#60;</span><span class=\"tok-typeName\">Panel</span><span class=\"tok-punctuation\">&#62;</span><span class=\"\">main content</span><span class=\"tok-punctuation\">&#60;/</span><span class=\"tok-typeName\">Panel</span><span class=\"tok-punctuation\">&#62;</span><span class=\"\"></span></div>\n<div><span class=\"\"></span><span class=\"tok-punctuation\">&#60;/</span><span class=\"tok-typeName\">Group</span><span class=\"tok-punctuation\">&#62;</span></div>"
}
Loading
Loading