;
diff --git a/lib/components/separator/Separator.tsx b/lib/components/separator/Separator.tsx
index a73756f69..dbfdd962b 100644
--- a/lib/components/separator/Separator.tsx
+++ b/lib/components/separator/Separator.tsx
@@ -28,6 +28,7 @@ import type { RegisteredSeparator, SeparatorProps } from "./types";
export function Separator({
children,
className,
+ disabled,
elementRef: elementRefProp,
id: idProp,
style,
@@ -64,6 +65,7 @@ export function Separator({
const element = elementRef.current;
if (element !== null) {
const separator: RegisteredSeparator = {
+ disabled,
element,
id
};
@@ -119,30 +121,32 @@ export function Separator({
unregisterSeparator();
};
}
- }, [groupId, id, registerSeparator]);
+ }, [disabled, groupId, id, registerSeparator]);
return (
);
}
diff --git a/lib/components/separator/types.ts b/lib/components/separator/types.ts
index 9e689edf8..798bc3a74 100644
--- a/lib/components/separator/types.ts
+++ b/lib/components/separator/types.ts
@@ -1,6 +1,7 @@
import type { CSSProperties, HTMLAttributes, Ref } from "react";
export type RegisteredSeparator = {
+ disabled?: boolean | undefined;
element: HTMLDivElement;
id: string;
};
@@ -20,6 +21,14 @@ export type SeparatorProps = BaseSeparatorAttributes & {
*/
className?: string | undefined;
+ /**
+ * When disabled, the separator cannot be used to resize its neighboring panels.
+ *
+ * ℹ️ The panels may still be resized indirectly (while other panels are being resized).
+ * To prevent a panel from being resized at all, it needs to also be disabled.
+ */
+ disabled?: boolean | undefined;
+
/**
* Ref attached to the root `HTMLDivElement`.
*/
diff --git a/lib/global/cursor/getCursorStyle.test.ts b/lib/global/cursor/getCursorStyle.test.ts
index 5a69e36d0..ca00c8b09 100644
--- a/lib/global/cursor/getCursorStyle.test.ts
+++ b/lib/global/cursor/getCursorStyle.test.ts
@@ -38,7 +38,7 @@ describe("getCursorStyle", () => {
groups: [horizontalGroup, verticalGroup],
state: "inactive"
})
- ).toBeNull();
+ ).toBeUndefined();
});
});
@@ -90,7 +90,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "hover"
})
- ).toBeNull();
+ ).toBeUndefined();
});
});
@@ -151,7 +151,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "active"
})
- ).toBeNull();
+ ).toBeUndefined();
});
});
});
@@ -169,7 +169,7 @@ describe("getCursorStyle", () => {
groups: [horizontalGroup, verticalGroup],
state: "inactive"
})
- ).toBeNull();
+ ).toBeUndefined();
});
});
@@ -221,7 +221,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "hover"
})
- ).toBeNull();
+ ).toBeUndefined();
});
});
@@ -282,7 +282,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "active"
})
- ).toBeNull();
+ ).toBeUndefined();
});
});
});
diff --git a/lib/global/cursor/getCursorStyle.ts b/lib/global/cursor/getCursorStyle.ts
index d7ecfce23..e92a33d7e 100644
--- a/lib/global/cursor/getCursorStyle.ts
+++ b/lib/global/cursor/getCursorStyle.ts
@@ -17,7 +17,7 @@ export function getCursorStyle({
cursorFlags: number;
groups: RegisteredGroup[];
state: InteractionState["state"];
-}): Properties["cursor"] | null {
+}): Properties["cursor"] {
let horizontalCount = 0;
let verticalCount = 0;
@@ -44,7 +44,7 @@ export function getCursorStyle({
}
if (horizontalCount === 0 && verticalCount === 0) {
- return null;
+ return undefined;
}
switch (state) {
diff --git a/lib/global/dom/calculateHitRegions.test.ts b/lib/global/dom/calculateHitRegions.test.ts
index 47dbe68a6..6b9057bd5 100644
--- a/lib/global/dom/calculateHitRegions.test.ts
+++ b/lib/global/dom/calculateHitRegions.test.ts
@@ -8,6 +8,7 @@ describe("calculateHitRegions", () => {
return JSON.stringify(
hitRegions.map((region) => ({
+ disabled: region.disabled || undefined,
panels: region.panels.map((panel) => panel.id),
rect: `${region.rect.x},${region.rect.y} ${region.rect.width} x ${region.rect.height}`,
separator: region.separator?.id
@@ -267,4 +268,146 @@ describe("calculateHitRegions", () => {
]"
`);
});
+
+ test("should disable a hit region if the separator is disabled", () => {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 50, 50), "left");
+ group.addSeparator(new DOMRect(50, 0, 5, 50), "separator", true);
+ group.addPanel(new DOMRect(55, 0, 50, 50), "right");
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "disabled": true,
+ "panels": [
+ "group-1-left",
+ "group-1-right"
+ ],
+ "rect": "47.5,0 10 x 50",
+ "separator": "group-1-separator"
+ }
+ ]"
+ `);
+ });
+
+ test("should disable a hit region if one or both panels are disabled and there is no explicit separator", () => {
+ {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 50, 50), "left", { disabled: true });
+ group.addPanel(new DOMRect(50, 0, 50, 50), "right");
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "disabled": true,
+ "panels": [
+ "group-1-left",
+ "group-1-right"
+ ],
+ "rect": "45,0 10 x 50"
+ }
+ ]"
+ `);
+ }
+
+ {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 50, 50), "left");
+ group.addPanel(new DOMRect(50, 0, 50, 50), "right", { disabled: true });
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "disabled": true,
+ "panels": [
+ "group-2-left",
+ "group-2-right"
+ ],
+ "rect": "45,0 10 x 50"
+ }
+ ]"
+ `);
+ }
+
+ {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 50, 50), "left", { disabled: true });
+ group.addPanel(new DOMRect(50, 0, 50, 50), "right", { disabled: true });
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "disabled": true,
+ "panels": [
+ "group-3-left",
+ "group-3-right"
+ ],
+ "rect": "45,0 10 x 50"
+ }
+ ]"
+ `);
+ }
+ });
+
+ test("should not disable a hit region if one or both panels are disabled but there is an enabled separator", () => {
+ {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 50, 50), "left", { disabled: true });
+ group.addSeparator(new DOMRect(50, 0, 5, 50), "separator");
+ group.addPanel(new DOMRect(55, 0, 50, 50), "right");
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "panels": [
+ "group-1-left",
+ "group-1-right"
+ ],
+ "rect": "47.5,0 10 x 50",
+ "separator": "group-1-separator"
+ }
+ ]"
+ `);
+ }
+
+ {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 50, 50), "left");
+ group.addSeparator(new DOMRect(50, 0, 5, 50), "separator");
+ group.addPanel(new DOMRect(55, 0, 50, 50), "right", { disabled: true });
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "panels": [
+ "group-2-left",
+ "group-2-right"
+ ],
+ "rect": "47.5,0 10 x 50",
+ "separator": "group-2-separator"
+ }
+ ]"
+ `);
+ }
+
+ {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 50, 50), "left", { disabled: true });
+ group.addSeparator(new DOMRect(50, 0, 5, 50), "separator");
+ group.addPanel(new DOMRect(55, 0, 50, 50), "right", { disabled: true });
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "panels": [
+ "group-3-left",
+ "group-3-right"
+ ],
+ "rect": "47.5,0 10 x 50",
+ "separator": "group-3-separator"
+ }
+ ]"
+ `);
+ }
+ });
});
diff --git a/lib/global/dom/calculateHitRegions.ts b/lib/global/dom/calculateHitRegions.ts
index 6de090215..bc32a4e1f 100644
--- a/lib/global/dom/calculateHitRegions.ts
+++ b/lib/global/dom/calculateHitRegions.ts
@@ -10,6 +10,7 @@ import { calculateAvailableGroupSize } from "./calculateAvailableGroupSize";
type PanelsTuple = [panel: RegisteredPanel, panel: RegisteredPanel];
export type HitRegion = {
+ disabled: boolean;
group: RegisteredGroup;
groupSize: number;
panels: PanelsTuple;
@@ -37,12 +38,18 @@ export function calculateHitRegions(group: RegisteredGroup) {
const hitRegions: HitRegion[] = [];
+ let disabledPanel = false;
+ let disabledSeparator = false;
let hasInterleavedStaticContent = false;
let prevPanel: RegisteredPanel | undefined = undefined;
let pendingSeparators: RegisteredSeparator[] = [];
for (const childElement of sortedChildElements) {
if (childElement.hasAttribute("data-panel")) {
+ if (childElement.ariaDisabled !== null) {
+ disabledPanel = true;
+ }
+
const panelData = panels.find(
(current) => current.element === childElement
);
@@ -151,7 +158,10 @@ export function calculateHitRegions(group: RegisteredGroup) {
);
}
+ const hasSeparator = !("width" in rectOrSeparator);
+
hitRegions.push({
+ disabled: disabledSeparator || (disabledPanel && !hasSeparator),
group,
groupSize: calculateAvailableGroupSize({ group }),
panels: [prevPanel, panelData],
@@ -159,6 +169,9 @@ export function calculateHitRegions(group: RegisteredGroup) {
"width" in rectOrSeparator ? undefined : rectOrSeparator,
rect
});
+
+ disabledPanel = false;
+ disabledSeparator = false;
}
}
@@ -167,6 +180,10 @@ export function calculateHitRegions(group: RegisteredGroup) {
pendingSeparators = [];
}
} else if (childElement.hasAttribute("data-separator")) {
+ if (childElement.ariaDisabled !== null) {
+ disabledSeparator = true;
+ }
+
const separatorData = separators.find(
(current) => current.element === childElement
);
diff --git a/lib/global/dom/calculatePanelConstraints.ts b/lib/global/dom/calculatePanelConstraints.ts
index fa1e0b922..3e6cfefd7 100644
--- a/lib/global/dom/calculatePanelConstraints.ts
+++ b/lib/global/dom/calculatePanelConstraints.ts
@@ -15,6 +15,7 @@ export function calculatePanelConstraints(group: RegisteredGroup) {
collapsedSize: 0,
collapsible: current.panelConstraints.collapsible === true,
defaultSize: undefined,
+ disabled: current.panelConstraints.disabled,
minSize: 0,
maxSize: 100,
panelId: current.id
@@ -72,6 +73,7 @@ export function calculatePanelConstraints(group: RegisteredGroup) {
collapsedSize,
collapsible: panelConstraints.collapsible === true,
defaultSize,
+ disabled: panelConstraints.disabled,
minSize,
maxSize,
panelId: panel.id
diff --git a/lib/global/test/mockGroup.ts b/lib/global/test/mockGroup.ts
index ba4405780..c18aa17dd 100644
--- a/lib/global/test/mockGroup.ts
+++ b/lib/global/test/mockGroup.ts
@@ -17,7 +17,11 @@ export type MockGroup = RegisteredGroup & {
id?: string,
constraints?: Partial
) => () => void;
- addSeparator: (relativeBounds: DOMRect, id?: string) => () => void;
+ addSeparator: (
+ relativeBounds: DOMRect,
+ id?: string,
+ disabled?: boolean
+ ) => () => void;
};
let groupIdCounter = 0;
@@ -91,6 +95,9 @@ export function mockGroup(
const element = document.createElement("div");
element.setAttribute("data-panel", panelId);
+ if (constraints?.disabled) {
+ element.setAttribute("aria-disabled", "");
+ }
setElementBounds(element, relativeBoundsToBounds(relativeBounds));
@@ -119,16 +126,21 @@ export function mockGroup(
addSeparator: (
relativeBounds: DOMRect,
- id: string = `${groupId}-${++separatorIdCounter}`
+ id: string = `${groupId}-${++separatorIdCounter}`,
+ disabled?: boolean
) => {
const separatorId = `${groupId}-${id}`;
const element = document.createElement("div");
element.setAttribute("data-separator", separatorId);
+ if (disabled) {
+ element.setAttribute("aria-disabled", "");
+ }
setElementBounds(element, relativeBoundsToBounds(relativeBounds));
const separator: RegisteredSeparator = {
+ disabled,
element,
id: separatorId
};
diff --git a/lib/global/utils/adjustLayoutByDelta.test.ts b/lib/global/utils/adjustLayoutByDelta.test.ts
index 3d9b1efaf..413b0b12d 100644
--- a/lib/global/utils/adjustLayoutByDelta.test.ts
+++ b/lib/global/utils/adjustLayoutByDelta.test.ts
@@ -2325,7 +2325,7 @@ describe("adjustLayoutByDelta", () => {
minSize: 20
}
]),
- prevLayout: l([10, 90]),
+ prevLayout: l([90, 10]),
trigger: "mouse-or-touch"
})
).toEqual(expectedLayout);
@@ -2333,4 +2333,147 @@ describe("adjustLayoutByDelta", () => {
});
});
});
+
+ describe("disabled panels", () => {
+ test("should not be resizable in a 2 panel group", () => {
+ (
+ [
+ [-50, c([{ disabled: true }, {}])],
+ [50, c([{ disabled: true }, {}])],
+ [-50, c([{}, { disabled: true }])],
+ [50, c([{}, { disabled: true }])]
+ ] satisfies [number, PanelConstraints[]][]
+ ).forEach(([delta, panelConstraints]) => {
+ expect(
+ adjustLayoutByDelta({
+ delta,
+ initialLayout: l([50, 50]),
+ panelConstraints,
+ prevLayout: l([50, 50]),
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([50, 50]));
+ });
+ });
+
+ test("should not be resizable if 1 of 3 panels are disabled", () => {
+ const layout = l([25, 50, 25]);
+
+ {
+ // Left panel disabled
+ const panelConstraints = c([{ disabled: true }, {}, {}]);
+
+ expect(
+ adjustLayoutByDelta({
+ delta: -25,
+ initialLayout: layout,
+ panelConstraints,
+ pivotIndices: [0, 1],
+ prevLayout: layout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(layout);
+
+ expect(
+ adjustLayoutByDelta({
+ delta: -75,
+ initialLayout: layout,
+ panelConstraints,
+ pivotIndices: [1, 2],
+ prevLayout: layout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([25, 0, 75]));
+ }
+
+ {
+ // Center panel disabled
+ const panelConstraints = c([{}, { disabled: true }, {}]);
+
+ expect(
+ adjustLayoutByDelta({
+ delta: -25,
+ initialLayout: layout,
+ panelConstraints,
+ pivotIndices: [0, 1],
+ prevLayout: layout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([0, 50, 50]));
+
+ expect(
+ adjustLayoutByDelta({
+ delta: -25,
+ initialLayout: layout,
+ panelConstraints,
+ pivotIndices: [1, 2],
+ prevLayout: layout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([0, 50, 50]));
+ }
+
+ {
+ // Right panel disabled
+ const panelConstraints = c([{}, {}, { disabled: true }]);
+
+ expect(
+ adjustLayoutByDelta({
+ delta: -25,
+ initialLayout: layout,
+ panelConstraints,
+ pivotIndices: [0, 1],
+ prevLayout: layout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([0, 75, 25]));
+
+ expect(
+ adjustLayoutByDelta({
+ delta: -25,
+ initialLayout: layout,
+ panelConstraints,
+ pivotIndices: [1, 2],
+ prevLayout: layout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([25, 50, 25]));
+ }
+ });
+
+ test("should not be resizable if 2 of 3 panels are disabled", () => {
+ (
+ [
+ [-50, c([{ disabled: true }, { disabled: true }, {}])],
+ [50, c([{ disabled: true }, { disabled: true }, {}])],
+ [-50, c([{ disabled: true }, {}, { disabled: true }])],
+ [50, c([{ disabled: true }, {}, { disabled: true }])],
+ [-50, c([{}, { disabled: true }, { disabled: true }])],
+ [50, c([{}, { disabled: true }, { disabled: true }])]
+ ] satisfies [number, PanelConstraints[]][]
+ ).forEach(([delta, panelConstraints]) => {
+ expect(
+ adjustLayoutByDelta({
+ delta,
+ initialLayout: l([25, 50, 25]),
+ panelConstraints,
+ pivotIndices: [0, 1],
+ prevLayout: l([25, 50, 25]),
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([25, 50, 25]));
+
+ expect(
+ adjustLayoutByDelta({
+ delta,
+ initialLayout: l([25, 50, 25]),
+ panelConstraints,
+ pivotIndices: [1, 2],
+ prevLayout: l([25, 50, 25]),
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(l([25, 50, 25]));
+ });
+ });
+ });
});
diff --git a/lib/global/utils/adjustLayoutByDelta.ts b/lib/global/utils/adjustLayoutByDelta.ts
index a3c18c575..56a9dc4bc 100644
--- a/lib/global/utils/adjustLayoutByDelta.ts
+++ b/lib/global/utils/adjustLayoutByDelta.ts
@@ -211,6 +211,7 @@ export function adjustLayoutByDelta({
const maxSafeSize = validatePanelSize({
panelConstraints: panelConstraintsArray[index],
+ prevSize,
size: 100
});
const delta = maxSafeSize - prevSize;
@@ -248,6 +249,7 @@ export function adjustLayoutByDelta({
const unsafeSize = prevSize - deltaRemaining;
const safeSize = validatePanelSize({
panelConstraints: panelConstraintsArray[index],
+ prevSize,
size: unsafeSize
});
@@ -300,6 +302,7 @@ export function adjustLayoutByDelta({
const unsafeSize = prevSize + deltaApplied;
const safeSize = validatePanelSize({
panelConstraints: panelConstraintsArray[pivotIndex],
+ prevSize,
size: unsafeSize
});
@@ -322,6 +325,7 @@ export function adjustLayoutByDelta({
const unsafeSize = prevSize + deltaRemaining;
const safeSize = validatePanelSize({
panelConstraints: panelConstraintsArray[index],
+ prevSize,
size: unsafeSize
});
diff --git a/lib/global/utils/findClosestHitRegion.ts b/lib/global/utils/findClosestHitRegion.ts
index 5ec66196b..ee308276d 100644
--- a/lib/global/utils/findClosestHitRegion.ts
+++ b/lib/global/utils/findClosestHitRegion.ts
@@ -15,6 +15,10 @@ export function findClosestHitRegion(
};
for (const hitRegion of hitRegions) {
+ if (hitRegion.disabled) {
+ continue;
+ }
+
const data = getDistanceBetweenPointAndRect(point, hitRegion.rect);
switch (orientation) {
case "horizontal": {
diff --git a/lib/global/utils/validatePanelGroupLayout.ts b/lib/global/utils/validatePanelGroupLayout.ts
index f94664119..0452dcf94 100644
--- a/lib/global/utils/validatePanelGroupLayout.ts
+++ b/lib/global/utils/validatePanelGroupLayout.ts
@@ -43,11 +43,15 @@ export function validatePanelGroupLayout({
// First pass: Validate the proposed layout given each panel's constraints
for (let index = 0; index < panelConstraints.length; index++) {
+ const prevSize = prevLayout[index];
+ assert(prevSize != null, `No layout data found for index ${index}`);
+
const unsafeSize = nextLayout[index];
assert(unsafeSize != null, `No layout data found for index ${index}`);
const safeSize = validatePanelSize({
panelConstraints: panelConstraints[index],
+ prevSize,
size: unsafeSize
});
@@ -67,6 +71,7 @@ export function validatePanelGroupLayout({
const unsafeSize = prevSize + remainingSize;
const safeSize = validatePanelSize({
panelConstraints: panelConstraints[index],
+ prevSize,
size: unsafeSize
});
diff --git a/lib/global/utils/validatePanelSize.ts b/lib/global/utils/validatePanelSize.ts
index 5ec812ff8..c2aaa24df 100644
--- a/lib/global/utils/validatePanelSize.ts
+++ b/lib/global/utils/validatePanelSize.ts
@@ -5,18 +5,25 @@ import { formatLayoutNumber } from "./formatLayoutNumber";
// Panel size must be in percentages; pixel values should be pre-converted
export function validatePanelSize({
panelConstraints,
+ prevSize,
size
}: {
panelConstraints: PanelConstraints;
+ prevSize: number;
size: number;
}) {
const {
collapsedSize = 0,
collapsible,
+ disabled,
maxSize = 100,
minSize = 0
} = panelConstraints;
+ if (disabled) {
+ return prevSize;
+ }
+
if (compareLayoutNumbers(size, minSize) < 0) {
if (collapsible) {
// Collapsible panels should snap closed or open only once they cross the halfway point between collapsed and min size.
diff --git a/public/generated/docs/Panel.json b/public/generated/docs/Panel.json
index 56aec95c0..0ea9162fd 100644
--- a/public/generated/docs/Panel.json
+++ b/public/generated/docs/Panel.json
@@ -108,6 +108,16 @@
"name": "defaultSize",
"required": false
},
+ "disabled": {
+ "description": [
+ {
+ "content": "When disabled, a panel cannot be resized either directly or indirectly (by resizing another panel).
\n"
+ }
+ ],
+ "html": "disabled?: boolean
",
+ "name": "disabled",
+ "required": false
+ },
"elementRef": {
"description": [
{
diff --git a/public/generated/docs/Separator.json b/public/generated/docs/Separator.json
index f41bf6e5f..9e4f715ba 100644
--- a/public/generated/docs/Separator.json
+++ b/public/generated/docs/Separator.json
@@ -75,6 +75,20 @@
"name": "style",
"required": false
},
+ "disabled": {
+ "description": [
+ {
+ "content": "When disabled, the separator cannot be used to resize its neighboring panels.
\n"
+ },
+ {
+ "content": "The panels may still be resized indirectly (while other panels are being resized).\nTo prevent a panel from being resized at all, it needs to also be disabled.
\n",
+ "intent": "primary"
+ }
+ ],
+ "html": "disabled?: boolean
",
+ "name": "disabled",
+ "required": false
+ },
"elementRef": {
"description": [
{
diff --git a/public/generated/examples/DisabledPanel.json b/public/generated/examples/DisabledPanel.json
new file mode 100644
index 000000000..b8c701624
--- /dev/null
+++ b/public/generated/examples/DisabledPanel.json
@@ -0,0 +1,3 @@
+{
+ "html": "<Group>
\n <Panel>left</Panel>
\n <Separator />
\n <Panel disabled>center (disabled)</Panel>
\n <Separator />
\n <Panel>right</Panel>
\n</Group>
"
+}
\ No newline at end of file
diff --git a/public/generated/examples/DisabledPanelAndSeparator.json b/public/generated/examples/DisabledPanelAndSeparator.json
new file mode 100644
index 000000000..35fed7bfc
--- /dev/null
+++ b/public/generated/examples/DisabledPanelAndSeparator.json
@@ -0,0 +1,3 @@
+{
+ "html": "<Group>
\n <Panel disabled>left (disabled</Panel>
\n <Separator disabled />
\n <Panel>center</Panel>
\n <Separator />
\n <Panel>right</Panel>
\n</Group>
"
+}
\ No newline at end of file
diff --git a/public/generated/examples/DisabledPanels.json b/public/generated/examples/DisabledPanels.json
new file mode 100644
index 000000000..c7f90524f
--- /dev/null
+++ b/public/generated/examples/DisabledPanels.json
@@ -0,0 +1,3 @@
+{
+ "html": "<Group>
\n <Panel disabled>left</Panel>
\n <Panel disabled>right</Panel>
\n</Group>
"
+}
\ No newline at end of file
diff --git a/public/generated/examples/DisabledSeparator.json b/public/generated/examples/DisabledSeparator.json
new file mode 100644
index 000000000..b90dfae04
--- /dev/null
+++ b/public/generated/examples/DisabledSeparator.json
@@ -0,0 +1,3 @@
+{
+ "html": "<Group>
\n <Panel>left</Panel>
\n <Separator disabled />
\n <Panel>right</Panel>
\n</Group>
"
+}
\ No newline at end of file
diff --git a/public/generated/examples/SeparatorDataAttributes.json b/public/generated/examples/SeparatorDataAttributes.json
index ba33974c7..d4b882a64 100644
--- a/public/generated/examples/SeparatorDataAttributes.json
+++ b/public/generated/examples/SeparatorDataAttributes.json
@@ -1,3 +1,3 @@
{
- "html": "<div data-separator=\"inactive\" />
\n<div data-separator=\"hover\" />
\n<div data-separator=\"active\" />
"
+ "html": "<div data-separator=\"disabled\" />
\n<div data-separator=\"inactive\" />
\n<div data-separator=\"hover\" />
\n<div data-separator=\"active\" />
"
}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 100f389c6..daef2ab83 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -52,6 +52,7 @@ export default function App() {
Fixed size panels
+ Disabled panels
Custom CSS styles
diff --git a/src/components/styled-panels/Panel.tsx b/src/components/styled-panels/Panel.tsx
index 5e8caf1ff..f2f4b1ea9 100644
--- a/src/components/styled-panels/Panel.tsx
+++ b/src/components/styled-panels/Panel.tsx
@@ -5,10 +5,12 @@ import {
type PanelProps
} from "react-resizable-panels";
import { PanelText } from "./PanelText";
+import { cn } from "react-lib-tools";
export function Panel({
children,
className = "",
+ disabled,
showSizeAsPercentage,
showSizeInPixels,
...rest
@@ -25,7 +27,12 @@ export function Panel({
return (
diff --git a/src/components/styled-panels/Separator.tsx b/src/components/styled-panels/Separator.tsx
index 39c98775b..f2a39738a 100644
--- a/src/components/styled-panels/Separator.tsx
+++ b/src/components/styled-panels/Separator.tsx
@@ -8,10 +8,12 @@ import GrabDotsIcon from "../../../public/svgs/grab-dots.svg?react";
export function Separator({
className = "",
+ disabled,
id,
orientation = "horizontal"
}: PropsWithChildren<{
className?: string;
+ disabled?: boolean;
id?: string;
orientation?: Orientation;
}>) {
@@ -19,12 +21,13 @@ export function Separator({
diff --git a/src/routes.ts b/src/routes.ts
index ae125cadd..7e221a669 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -29,6 +29,9 @@ export const routes = {
"/examples/fixed-size-panels": lazy(
() => import("./routes/FixedSizePanelsRoute")
),
+ "/examples/disabled-panels": lazy(
+ () => import("./routes/DisabledPanelsRoute")
+ ),
"/examples/custom-css-styles": lazy(
() => import("./routes/CustomStylesRoute")
),
diff --git a/src/routes/DisabledPanelsRoute.tsx b/src/routes/DisabledPanelsRoute.tsx
new file mode 100644
index 000000000..4c7541341
--- /dev/null
+++ b/src/routes/DisabledPanelsRoute.tsx
@@ -0,0 +1,81 @@
+import { Box, Callout, Code, Header } from "react-lib-tools";
+import { html as DisabledSeparatorHTML } from "../../public/generated/examples/DisabledSeparator.json";
+import { html as DisabledPanelsHTML } from "../../public/generated/examples/DisabledPanels.json";
+import { html as DisabledPanelHTML } from "../../public/generated/examples/DisabledPanel.json";
+import { html as DisabledPanelAndSeparatorHTML } from "../../public/generated/examples/DisabledPanelAndSeparator.json";
+import { Group } from "../components/styled-panels/Group";
+import { Panel } from "../components/styled-panels/Panel";
+import { Separator } from "../components/styled-panels/Separator";
+
+export default function DisabledPanelsRoute() {
+ return (
+
+
+
+ Panel and Separator components can be disabled
+ to disable or limit resize behavior. Below are a few examples of how
+ this can be used to implement different types of UIs.
+
+
+ In groups with only two panels, disabling a separator is sufficient to
+ completely prevent resizing.
+
+
+
+ left
+
+ right
+
+
+ The same applies to disabling one or both panels when there is no
+ explicit separator element.
+
+
+ Note this is functionally the same as disabling the entire{" "}
+ Group component.
+
+
+
+ left
+ right
+
+
+ In groups with three or more panels, disabling a separator does not
+ completely prevent a panel from being resized. In the example below,
+ resizing the center panel can indirectly cause the left panel to be
+ resized as well.
+
+
+ left
+
+ center
+
+ right
+
+
+ Disabling a panel prevents it from being resized, though its separator
+ can still be used to resize other panels.
+
+
+
+ left
+
+ center (disabled)
+
+ right
+
+
+ You can also disable both a panel and its separator to completely
+ prevent them from being resized or interacted with in any way.
+
+
+
+ left (disabled)
+
+ center
+
+ right
+
+
+ );
+}
diff --git a/src/routes/examples/DisabledPanel.tsx b/src/routes/examples/DisabledPanel.tsx
new file mode 100644
index 000000000..321b8179f
--- /dev/null
+++ b/src/routes/examples/DisabledPanel.tsx
@@ -0,0 +1,12 @@
+import { Group, Panel, Separator } from "react-resizable-panels";
+
+//
+
+/* prettier-ignore */
+
+ left
+
+ center (disabled)
+
+ right
+
diff --git a/src/routes/examples/DisabledPanelAndSeparator.tsx b/src/routes/examples/DisabledPanelAndSeparator.tsx
new file mode 100644
index 000000000..d0bc4a62a
--- /dev/null
+++ b/src/routes/examples/DisabledPanelAndSeparator.tsx
@@ -0,0 +1,12 @@
+import { Group, Panel, Separator } from "react-resizable-panels";
+
+//
+
+/* prettier-ignore */
+
+ left (disabled
+
+ center
+
+ right
+
diff --git a/src/routes/examples/DisabledPanels.tsx b/src/routes/examples/DisabledPanels.tsx
new file mode 100644
index 000000000..b3b091fb8
--- /dev/null
+++ b/src/routes/examples/DisabledPanels.tsx
@@ -0,0 +1,9 @@
+import { Group, Panel } from "react-resizable-panels";
+
+//
+
+/* prettier-ignore */
+
+ left
+ right
+
diff --git a/src/routes/examples/DisabledSeparator.tsx b/src/routes/examples/DisabledSeparator.tsx
new file mode 100644
index 000000000..779ff3e7b
--- /dev/null
+++ b/src/routes/examples/DisabledSeparator.tsx
@@ -0,0 +1,10 @@
+import { Group, Panel, Separator } from "react-resizable-panels";
+
+//
+
+/* prettier-ignore */
+
+ left
+
+ right
+
diff --git a/src/routes/examples/SeparatorDataAttributes.html b/src/routes/examples/SeparatorDataAttributes.html
index 0fee19dae..64917018d 100644
--- a/src/routes/examples/SeparatorDataAttributes.html
+++ b/src/routes/examples/SeparatorDataAttributes.html
@@ -1,3 +1,4 @@
+
From f039bf26143a2c9474ae51b2d6b192401ca6ef3e Mon Sep 17 00:00:00 2001
From: Brian Vaughn
Date: Wed, 4 Feb 2026 18:11:56 -0500
Subject: [PATCH 2/2] 4.5.9 -> 4.6.0
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index d03bd57ed..246e8cc9d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
- "version": "4.5.9",
+ "version": "4.6.0",
"type": "module",
"author": "Brian Vaughn (https://github.com/bvaughn/)",
"contributors": [