{
+ describe("disabled prop", () => {
+ test("changes to disabled prop should not cause the Separator to remount", () => {
+ const onMountedGroupsChange = vi.fn();
+ const removeListener = eventEmitter.addListener(
+ "mountedGroupsChange",
+ onMountedGroupsChange
+ );
+
+ const { rerender } = render(
+
+ );
+ expect(onMountedGroupsChange).toHaveBeenCalled();
+
+ onMountedGroupsChange.mockReset();
+
+ rerender(
+
+ );
+ expect(onMountedGroupsChange).not.toHaveBeenCalled();
+
+ removeListener();
+ });
+
+ test("changes to this prop should update Separator behavior", async () => {
+ setElementBoundsFunction((element) => {
+ switch (element.id) {
+ case "left": {
+ return new DOMRect(0, 0, 50, 50);
+ }
+ case "separator": {
+ return new DOMRect(50, 0, 10, 50);
+ }
+ case "right": {
+ return new DOMRect(60, 0, 50, 50);
+ }
+ }
+ });
+
+ const onLayoutChange = vi.fn();
+ const onLayoutChanged = vi.fn();
+
+ const { rerender } = render(
+
+ );
+
+ onLayoutChange.mockReset();
+ onLayoutChanged.mockReset();
+
+ rerender(
+
+ );
+
+ // Resize attempts should be ignored because the Panel is disabled
+ await moveSeparator(25);
+ expect(onLayoutChange).not.toHaveBeenCalled();
+ expect(onLayoutChanged).not.toHaveBeenCalled();
+ onLayoutChange.mockReset();
+ onLayoutChanged.mockReset();
+
+ rerender(
+
+ );
+
+ // Resize attempts should work now that the Panel has been re-enabled
+ await moveSeparator(25);
+ expect(onLayoutChange).toHaveBeenCalled();
+ expect(onLayoutChanged).toHaveBeenCalled();
+ });
+ });
+
describe("HTML attributes", () => {
test("should expose id and data-testid", () => {
render(
diff --git a/lib/components/separator/Separator.tsx b/lib/components/separator/Separator.tsx
index dbfdd962b..6208555e6 100644
--- a/lib/components/separator/Separator.tsx
+++ b/lib/components/separator/Separator.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { eventEmitter } from "../../global/mutableState";
import type { InteractionState } from "../../global/types";
import { calculateSeparatorAriaValues } from "../../global/utils/calculateSeparatorAriaValues";
@@ -9,6 +9,8 @@ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect
import { useMergedRefs } from "../../hooks/useMergedRefs";
import { useGroupContext } from "../group/useGroupContext";
import type { RegisteredSeparator, SeparatorProps } from "./types";
+import type { Properties } from "csstype";
+import { useStableObject } from "../../hooks/useStableObject";
/**
* Separators are not _required_ but they are _recommended_ as they improve keyboard accessibility.
@@ -36,6 +38,10 @@ export function Separator({
}: SeparatorProps) {
const id = useId(idProp);
+ const stableProps = useStableObject({
+ disabled
+ });
+
const [aria, setAria] = useState<{
valueControls?: string | undefined;
valueMin?: number | undefined;
@@ -51,9 +57,11 @@ export function Separator({
const mergedRef = useMergedRefs(elementRef, elementRefProp);
const {
+ disableCursor,
id: groupId,
orientation: groupOrientation,
- registerSeparator
+ registerSeparator,
+ toggleSeparatorDisabled
} = useGroupContext();
const orientation =
@@ -65,7 +73,7 @@ export function Separator({
const element = elementRef.current;
if (element !== null) {
const separator: RegisteredSeparator = {
- disabled,
+ disabled: stableProps.disabled,
element,
id
};
@@ -121,13 +129,23 @@ export function Separator({
unregisterSeparator();
};
}
- }, [disabled, groupId, id, registerSeparator]);
+ }, [groupId, id, registerSeparator, stableProps]);
+
+ // Not all props require re-registering the separator;
+ useEffect(() => {
+ toggleSeparatorDisabled(id, !!disabled);
+ }, [disabled, id, toggleSeparatorDisabled]);
+
+ let cursor: Properties["cursor"] = undefined;
+ if (disabled && !disableCursor) {
+ cursor = "not-allowed";
+ }
return (
{
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
@@ -276,98 +275,67 @@ describe("calculateHitRegions", () => {
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");
+ 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");
+ group.addSeparator(new DOMRect(50, 0, 5, 50), "separator");
+ group.addPanel(new DOMRect(55, 0, 50, 50), "center", { disabled: true });
+ group.addPanel(new DOMRect(105, 0, 50, 50), "right");
- expect(serialize(group)).toMatchInlineSnapshot(`
+ expect(serialize(group)).toMatchInlineSnapshot(`
"[
{
- "disabled": true,
"panels": [
"group-1-left",
+ "group-1-center"
+ ],
+ "rect": "47.5,0 10 x 50",
+ "separator": "group-1-separator"
+ },
+ {
+ "panels": [
+ "group-1-center",
"group-1-right"
],
- "rect": "45,0 10 x 50"
+ "rect": "100,0 10 x 50"
}
]"
`);
- }
+ });
+ test("should disable all hit regions if there is one or fewer enabled panels", () => {
{
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 });
+ 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-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(0, 0, 50, 50), "left");
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");
+ group.addPanel(new DOMRect(50, 0, 50, 50), "right", { disabled: true });
expect(serialize(group)).toMatchInlineSnapshot(`
- "[
- {
- "panels": [
- "group-1-left",
- "group-1-right"
- ],
- "rect": "47.5,0 10 x 50",
- "separator": "group-1-separator"
- }
- ]"
- `);
+ "[]"
+ `);
}
{
@@ -377,16 +345,7 @@ describe("calculateHitRegions", () => {
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"
- }
- ]"
+ "[]"
`);
}
@@ -394,20 +353,65 @@ describe("calculateHitRegions", () => {
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 });
+ group.addPanel(new DOMRect(55, 0, 50, 50), "right");
expect(serialize(group)).toMatchInlineSnapshot(`
- "[
- {
- "panels": [
- "group-3-left",
- "group-3-right"
- ],
- "rect": "47.5,0 10 x 50",
- "separator": "group-3-separator"
- }
- ]"
+ "[]"
`);
}
});
+
+ test("should disable panel boundaries if there are no resizable panels before the current boundary", () => {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 25, 50), "a", { disabled: true });
+ group.addPanel(new DOMRect(25, 0, 25, 50), "b");
+ group.addPanel(new DOMRect(50, 0, 25, 50), "c", { disabled: true });
+ group.addPanel(new DOMRect(75, 0, 25, 50), "d");
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "panels": [
+ "group-1-b",
+ "group-1-c"
+ ],
+ "rect": "45,0 10 x 50"
+ },
+ {
+ "panels": [
+ "group-1-c",
+ "group-1-d"
+ ],
+ "rect": "70,0 10 x 50"
+ }
+ ]"
+ `);
+ });
+
+ test("should disable panel boundaries if there are no resizable panels after the current boundary", () => {
+ const group = mockGroup(new DOMRect(0, 0, 100, 50));
+ group.addPanel(new DOMRect(0, 0, 25, 50), "a");
+ group.addPanel(new DOMRect(25, 0, 25, 50), "b", { disabled: true });
+ group.addPanel(new DOMRect(50, 0, 25, 50), "c");
+ group.addPanel(new DOMRect(75, 0, 25, 50), "d", { disabled: true });
+
+ expect(serialize(group)).toMatchInlineSnapshot(`
+ "[
+ {
+ "panels": [
+ "group-1-a",
+ "group-1-b"
+ ],
+ "rect": "20,0 10 x 50"
+ },
+ {
+ "panels": [
+ "group-1-b",
+ "group-1-c"
+ ],
+ "rect": "45,0 10 x 50"
+ }
+ ]"
+ `);
+ });
});
diff --git a/lib/global/dom/calculateHitRegions.ts b/lib/global/dom/calculateHitRegions.ts
index bc32a4e1f..74eb195f8 100644
--- a/lib/global/dom/calculateHitRegions.ts
+++ b/lib/global/dom/calculateHitRegions.ts
@@ -10,7 +10,6 @@ import { calculateAvailableGroupSize } from "./calculateAvailableGroupSize";
type PanelsTuple = [panel: RegisteredPanel, panel: RegisteredPanel];
export type HitRegion = {
- disabled: boolean;
group: RegisteredGroup;
groupSize: number;
panels: PanelsTuple;
@@ -38,165 +37,197 @@ export function calculateHitRegions(group: RegisteredGroup) {
const hitRegions: HitRegion[] = [];
- let disabledPanel = false;
let disabledSeparator = false;
let hasInterleavedStaticContent = false;
+ let firstEnabledPanelIndex = -1;
+ let lastEnabledPanelIndex = -1;
+ let numEnabledPanels = 0;
let prevPanel: RegisteredPanel | undefined = undefined;
let pendingSeparators: RegisteredSeparator[] = [];
- for (const childElement of sortedChildElements) {
- if (childElement.hasAttribute("data-panel")) {
- if (childElement.ariaDisabled !== null) {
- disabledPanel = true;
- }
+ {
+ let currentPanelIndex = -1;
- const panelData = panels.find(
- (current) => current.element === childElement
- );
- if (panelData) {
- if (prevPanel) {
- const prevRect = prevPanel.element.getBoundingClientRect();
- const rect = childElement.getBoundingClientRect();
-
- let pendingRectsOrSeparators: (DOMRect | RegisteredSeparator)[];
-
- // If an explicit Separator has been rendered, always watch it
- // Otherwise watch the entire space between the panels
- // The one caveat is when there are non-interactive element(s) between panels,
- // in which case we may need to watch individual panel edges
- if (hasInterleavedStaticContent) {
- const firstPanelEdgeRect =
- orientation === "horizontal"
- ? new DOMRect(prevRect.right, prevRect.top, 0, prevRect.height)
- : new DOMRect(
- prevRect.left,
- prevRect.bottom,
- prevRect.width,
- 0
- );
- const secondPanelEdgeRect =
- orientation === "horizontal"
- ? new DOMRect(rect.left, rect.top, 0, rect.height)
- : new DOMRect(rect.left, rect.top, rect.width, 0);
-
- switch (pendingSeparators.length) {
- case 0: {
- pendingRectsOrSeparators = [
- firstPanelEdgeRect,
- secondPanelEdgeRect
- ];
- break;
- }
- case 1: {
- const separator = pendingSeparators[0];
- const closestRect = findClosestRect({
- orientation,
- rects: [prevRect, rect],
- targetRect: separator.element.getBoundingClientRect()
- });
+ for (const childElement of sortedChildElements) {
+ if (childElement.hasAttribute("data-panel")) {
+ currentPanelIndex++;
- pendingRectsOrSeparators = [
- separator,
- closestRect === prevRect
- ? secondPanelEdgeRect
- : firstPanelEdgeRect
- ];
- break;
- }
- default: {
- pendingRectsOrSeparators = pendingSeparators;
- break;
- }
- }
- } else {
- if (pendingSeparators.length) {
- pendingRectsOrSeparators = pendingSeparators;
- } else {
- pendingRectsOrSeparators = [
+ if (childElement.ariaDisabled === null) {
+ numEnabledPanels++;
+
+ if (firstEnabledPanelIndex === -1) {
+ firstEnabledPanelIndex = currentPanelIndex;
+ }
+
+ lastEnabledPanelIndex = currentPanelIndex;
+ }
+ }
+ }
+ }
+
+ // If all (or all but one) of the Panels are disabled, there can be no resize interactions.
+ if (numEnabledPanels > 1) {
+ let currentPanelIndex = -1;
+
+ for (const childElement of sortedChildElements) {
+ if (childElement.hasAttribute("data-panel")) {
+ currentPanelIndex++;
+
+ const panelData = panels.find(
+ (current) => current.element === childElement
+ );
+ if (panelData) {
+ if (prevPanel) {
+ const prevRect = prevPanel.element.getBoundingClientRect();
+ const rect = childElement.getBoundingClientRect();
+
+ let pendingRectsOrSeparators: (DOMRect | RegisteredSeparator)[];
+
+ // If an explicit Separator has been rendered, always watch it
+ // Otherwise watch the entire space between the panels
+ // The one caveat is when there are non-interactive element(s) between panels,
+ // in which case we may need to watch individual panel edges
+ if (hasInterleavedStaticContent) {
+ const firstPanelEdgeRect =
orientation === "horizontal"
? new DOMRect(
prevRect.right,
- rect.top,
- rect.left - prevRect.right,
- rect.height
+ prevRect.top,
+ 0,
+ prevRect.height
)
: new DOMRect(
- rect.left,
+ prevRect.left,
prevRect.bottom,
- rect.width,
- rect.top - prevRect.bottom
- )
- ];
- }
- }
-
- for (const rectOrSeparator of pendingRectsOrSeparators) {
- let rect =
- "width" in rectOrSeparator
- ? rectOrSeparator
- : rectOrSeparator.element.getBoundingClientRect();
-
- const minHitTargetSize = isCoarsePointer()
- ? group.resizeTargetMinimumSize.coarse
- : group.resizeTargetMinimumSize.fine;
- if (rect.width < minHitTargetSize) {
- const delta = minHitTargetSize - rect.width;
- rect = new DOMRect(
- rect.x - delta / 2,
- rect.y,
- rect.width + delta,
- rect.height
- );
- }
- if (rect.height < minHitTargetSize) {
- const delta = minHitTargetSize - rect.height;
- rect = new DOMRect(
- rect.x,
- rect.y - delta / 2,
- rect.width,
- rect.height + delta
- );
+ prevRect.width,
+ 0
+ );
+ const secondPanelEdgeRect =
+ orientation === "horizontal"
+ ? new DOMRect(rect.left, rect.top, 0, rect.height)
+ : new DOMRect(rect.left, rect.top, rect.width, 0);
+
+ switch (pendingSeparators.length) {
+ case 0: {
+ pendingRectsOrSeparators = [
+ firstPanelEdgeRect,
+ secondPanelEdgeRect
+ ];
+ break;
+ }
+ case 1: {
+ const separator = pendingSeparators[0];
+ const closestRect = findClosestRect({
+ orientation,
+ rects: [prevRect, rect],
+ targetRect: separator.element.getBoundingClientRect()
+ });
+
+ pendingRectsOrSeparators = [
+ separator,
+ closestRect === prevRect
+ ? secondPanelEdgeRect
+ : firstPanelEdgeRect
+ ];
+ break;
+ }
+ default: {
+ pendingRectsOrSeparators = pendingSeparators;
+ break;
+ }
+ }
+ } else {
+ if (pendingSeparators.length) {
+ pendingRectsOrSeparators = pendingSeparators;
+ } else {
+ pendingRectsOrSeparators = [
+ orientation === "horizontal"
+ ? new DOMRect(
+ prevRect.right,
+ rect.top,
+ rect.left - prevRect.right,
+ rect.height
+ )
+ : new DOMRect(
+ rect.left,
+ prevRect.bottom,
+ rect.width,
+ rect.top - prevRect.bottom
+ )
+ ];
+ }
}
- const hasSeparator = !("width" in rectOrSeparator);
+ for (const rectOrSeparator of pendingRectsOrSeparators) {
+ let rect =
+ "width" in rectOrSeparator
+ ? rectOrSeparator
+ : rectOrSeparator.element.getBoundingClientRect();
+
+ const minHitTargetSize = isCoarsePointer()
+ ? group.resizeTargetMinimumSize.coarse
+ : group.resizeTargetMinimumSize.fine;
+ if (rect.width < minHitTargetSize) {
+ const delta = minHitTargetSize - rect.width;
+ rect = new DOMRect(
+ rect.x - delta / 2,
+ rect.y,
+ rect.width + delta,
+ rect.height
+ );
+ }
+ if (rect.height < minHitTargetSize) {
+ const delta = minHitTargetSize - rect.height;
+ rect = new DOMRect(
+ rect.x,
+ rect.y - delta / 2,
+ rect.width,
+ rect.height + delta
+ );
+ }
- hitRegions.push({
- disabled: disabledSeparator || (disabledPanel && !hasSeparator),
- group,
- groupSize: calculateAvailableGroupSize({ group }),
- panels: [prevPanel, panelData],
- separator:
- "width" in rectOrSeparator ? undefined : rectOrSeparator,
- rect
- });
+ const skip =
+ currentPanelIndex <= firstEnabledPanelIndex ||
+ currentPanelIndex > lastEnabledPanelIndex;
+
+ if (!disabledSeparator && !skip) {
+ hitRegions.push({
+ group,
+ groupSize: calculateAvailableGroupSize({ group }),
+ panels: [prevPanel, panelData],
+ separator:
+ "width" in rectOrSeparator ? undefined : rectOrSeparator,
+ rect
+ });
+ }
- disabledPanel = false;
- disabledSeparator = false;
+ disabledSeparator = false;
+ }
}
- }
- hasInterleavedStaticContent = false;
- prevPanel = panelData;
- pendingSeparators = [];
- }
- } else if (childElement.hasAttribute("data-separator")) {
- if (childElement.ariaDisabled !== null) {
- disabledSeparator = true;
- }
+ hasInterleavedStaticContent = false;
+ prevPanel = panelData;
+ pendingSeparators = [];
+ }
+ } else if (childElement.hasAttribute("data-separator")) {
+ if (childElement.ariaDisabled !== null) {
+ disabledSeparator = true;
+ }
- const separatorData = separators.find(
- (current) => current.element === childElement
- );
- if (separatorData) {
- // Separators will be included implicitly in the area between the previous and next panel
- // It's important to track them though, to handle the scenario of non-interactive group content
- pendingSeparators.push(separatorData);
+ const separatorData = separators.find(
+ (current) => current.element === childElement
+ );
+ if (separatorData) {
+ // Separators will be included implicitly in the area between the previous and next panel
+ // It's important to track them though, to handle the scenario of non-interactive group content
+ pendingSeparators.push(separatorData);
+ } else {
+ prevPanel = undefined;
+ pendingSeparators = [];
+ }
} else {
- prevPanel = undefined;
- pendingSeparators = [];
+ hasInterleavedStaticContent = true;
}
- } else {
- hasInterleavedStaticContent = true;
}
}
diff --git a/lib/global/utils/adjustLayoutByDelta.test.ts b/lib/global/utils/adjustLayoutByDelta.test.ts
index 413b0b12d..34d501ca0 100644
--- a/lib/global/utils/adjustLayoutByDelta.test.ts
+++ b/lib/global/utils/adjustLayoutByDelta.test.ts
@@ -34,6 +34,7 @@ function c(partials: Partial
[]) {
collapsedSize: 0,
collapsible: false,
defaultSize: undefined,
+ disabled: current.disabled,
maxSize: 100,
minSize: 0,
...current,
@@ -2475,5 +2476,26 @@ describe("adjustLayoutByDelta", () => {
).toEqual(l([25, 50, 25]));
});
});
+
+ test("should be resizable via the imperative API", () => {
+ (
+ [
+ [-5, c([{ disabled: true }, {}]), l([45, 55])],
+ [5, c([{ disabled: true }, {}]), l([55, 45])],
+ [-5, c([{}, { disabled: true }]), l([45, 55])],
+ [5, c([{}, { disabled: true }]), l([55, 45])]
+ ] satisfies [number, PanelConstraints[], Layout][]
+ ).forEach(([delta, panelConstraints, expectedLayout]) => {
+ expect(
+ adjustLayoutByDelta({
+ delta,
+ initialLayout: l([50, 50]),
+ panelConstraints,
+ prevLayout: l([50, 50]),
+ trigger: "imperative-api"
+ })
+ ).toEqual(expectedLayout);
+ });
+ });
});
});
diff --git a/lib/global/utils/adjustLayoutByDelta.ts b/lib/global/utils/adjustLayoutByDelta.ts
index 56a9dc4bc..6fa260f33 100644
--- a/lib/global/utils/adjustLayoutByDelta.ts
+++ b/lib/global/utils/adjustLayoutByDelta.ts
@@ -26,6 +26,8 @@ export function adjustLayoutByDelta({
return initialLayoutProp;
}
+ const overrideDisabledPanels = trigger === "imperative-api";
+
const initialLayout = Object.values(initialLayoutProp);
const prevLayout = Object.values(prevLayoutProp);
const nextLayout = [...initialLayout];
@@ -210,6 +212,7 @@ export function adjustLayoutByDelta({
);
const maxSafeSize = validatePanelSize({
+ overrideDisabledPanels,
panelConstraints: panelConstraintsArray[index],
prevSize,
size: 100
@@ -248,6 +251,7 @@ export function adjustLayoutByDelta({
const unsafeSize = prevSize - deltaRemaining;
const safeSize = validatePanelSize({
+ overrideDisabledPanels,
panelConstraints: panelConstraintsArray[index],
prevSize,
size: unsafeSize
@@ -301,6 +305,7 @@ export function adjustLayoutByDelta({
const unsafeSize = prevSize + deltaApplied;
const safeSize = validatePanelSize({
+ overrideDisabledPanels,
panelConstraints: panelConstraintsArray[pivotIndex],
prevSize,
size: unsafeSize
@@ -324,6 +329,7 @@ export function adjustLayoutByDelta({
const unsafeSize = prevSize + deltaRemaining;
const safeSize = validatePanelSize({
+ overrideDisabledPanels,
panelConstraints: panelConstraintsArray[index],
prevSize,
size: unsafeSize
diff --git a/lib/global/utils/calculateDefaultLayout.test.ts b/lib/global/utils/calculateDefaultLayout.test.ts
index 30974e560..3259c92cf 100644
--- a/lib/global/utils/calculateDefaultLayout.test.ts
+++ b/lib/global/utils/calculateDefaultLayout.test.ts
@@ -8,6 +8,7 @@ const c = (
collapsedSize: 0,
collapsible: false,
defaultSize: undefined,
+ disabled: undefined,
maxSize: 100,
minSize: 0,
...partial
diff --git a/lib/global/utils/calculateSeparatorAriaValues.test.ts b/lib/global/utils/calculateSeparatorAriaValues.test.ts
index 866e088a8..7a0c0f7d2 100644
--- a/lib/global/utils/calculateSeparatorAriaValues.test.ts
+++ b/lib/global/utils/calculateSeparatorAriaValues.test.ts
@@ -6,6 +6,7 @@ const DEFAULT_PANEL_CONSTRAINTS = {
collapsedSize: 0,
collapsible: false,
defaultSize: undefined,
+ disabled: undefined,
minSize: 0,
maxSize: 100
};
@@ -17,6 +18,7 @@ describe("calculateSeparatorAriaValues", () => {
...DEFAULT_PANEL_CONSTRAINTS,
collapsedSize: 5,
collapsible: true,
+ disabled: undefined,
maxSize: 70,
minSize: 20,
panelId: "left"
diff --git a/lib/global/utils/findClosestHitRegion.ts b/lib/global/utils/findClosestHitRegion.ts
index ee308276d..5ec66196b 100644
--- a/lib/global/utils/findClosestHitRegion.ts
+++ b/lib/global/utils/findClosestHitRegion.ts
@@ -15,10 +15,6 @@ 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/getImperativeGroupMethods.test.ts b/lib/global/utils/getImperativeGroupMethods.test.ts
index 580b1c611..b1e9853cf 100644
--- a/lib/global/utils/getImperativeGroupMethods.test.ts
+++ b/lib/global/utils/getImperativeGroupMethods.test.ts
@@ -155,5 +155,32 @@ describe("getImperativeGroupMethods", () => {
"A-2": 70
});
});
+
+ test("allows disabled panels to be resized", () => {
+ const { api } = init([
+ { defaultSize: 200, disabled: true, minSize: 100 },
+ { defaultSize: 800, disabled: true }
+ ]);
+
+ expect(api.getLayout()).toMatchInlineSnapshot(`
+ {
+ "A-1": 20,
+ "A-2": 80,
+ }
+ `);
+
+ api.setLayout({
+ "A-1": 30,
+ "A-2": 70
+ });
+
+ expect(onMountedGroupsChange).toHaveBeenCalledTimes(1);
+ expect(api.getLayout()).toMatchInlineSnapshot(`
+ {
+ "A-1": 30,
+ "A-2": 70,
+ }
+ `);
+ });
});
});
diff --git a/lib/global/utils/getImperativePanelMethods.test.ts b/lib/global/utils/getImperativePanelMethods.test.ts
index 08c815e3e..5a59b806d 100644
--- a/lib/global/utils/getImperativePanelMethods.test.ts
+++ b/lib/global/utils/getImperativePanelMethods.test.ts
@@ -56,7 +56,14 @@ describe("getImperativePanelMethods", () => {
const group = mockGroup(bounds, "horizontal", "A");
panelConstraints.forEach(
- ({ collapsedSize, collapsible, defaultSize, maxSize, minSize }) => {
+ ({
+ collapsedSize,
+ collapsible,
+ defaultSize,
+ disabled,
+ maxSize,
+ minSize
+ }) => {
group.addPanel(
new DOMRect(
0,
@@ -73,6 +80,7 @@ describe("getImperativePanelMethods", () => {
collapsible,
defaultSize:
defaultSize !== undefined ? `${defaultSize}%` : undefined,
+ disabled,
maxSize: maxSize !== undefined ? `${maxSize}%` : undefined,
minSize: minSize !== undefined ? `${minSize}%` : 0
}
@@ -152,6 +160,21 @@ describe("getImperativePanelMethods", () => {
expect(onLayoutChange).toHaveBeenCalledTimes(1);
expect(onLayoutChange).toHaveBeenCalledWith([0, 100]);
});
+
+ test("allows disabled panel to be collapsed", () => {
+ const { panelApis } = init([
+ {
+ collapsible: true,
+ defaultSize: 50,
+ disabled: true
+ },
+ {}
+ ]);
+ panelApis[0].collapse();
+
+ expect(onLayoutChange).toHaveBeenCalledTimes(1);
+ expect(onLayoutChange).toHaveBeenCalledWith([0, 100]);
+ });
});
describe("expand", () => {
@@ -213,6 +236,17 @@ describe("getImperativePanelMethods", () => {
expect(onLayoutChange).toHaveBeenCalledTimes(1);
expect(onLayoutChange).toHaveBeenCalledWith([1, 99]);
});
+
+ test("allows disabled panel to be expanded", () => {
+ const { panelApis } = init([
+ { defaultSize: 0, collapsible: true, disabled: true, minSize: 10 },
+ {}
+ ]);
+
+ panelApis[0].expand();
+ expect(onLayoutChange).toHaveBeenCalledTimes(1);
+ expect(onLayoutChange).toHaveBeenCalledWith([10, 90]);
+ });
});
describe("getSize", () => {
@@ -282,6 +316,18 @@ describe("getImperativePanelMethods", () => {
expect(onLayoutChange).toHaveBeenCalledTimes(1);
expect(onLayoutChange).toHaveBeenCalledWith([10, 90]);
});
+
+ test("allows disabled panel to be resized", () => {
+ const { panelApis } = init([
+ { defaultSize: 25, disabled: true, minSize: 10 },
+ {}
+ ]);
+
+ panelApis[0].resize(0);
+
+ expect(onLayoutChange).toHaveBeenCalledTimes(1);
+ expect(onLayoutChange).toHaveBeenCalledWith([10, 90]);
+ });
});
});
});
diff --git a/lib/global/utils/validatePanelGroupLayout.test.ts b/lib/global/utils/validatePanelGroupLayout.test.ts
index 234567a35..cc382c225 100644
--- a/lib/global/utils/validatePanelGroupLayout.test.ts
+++ b/lib/global/utils/validatePanelGroupLayout.test.ts
@@ -11,6 +11,7 @@ function c(partials: Partial[]) {
collapsedSize: 0,
collapsible: false,
defaultSize: undefined,
+ disabled: undefined,
maxSize: 100,
minSize: 0,
...current,
diff --git a/lib/global/utils/validatePanelGroupLayout.ts b/lib/global/utils/validatePanelGroupLayout.ts
index 0452dcf94..ac058dba3 100644
--- a/lib/global/utils/validatePanelGroupLayout.ts
+++ b/lib/global/utils/validatePanelGroupLayout.ts
@@ -50,6 +50,7 @@ export function validatePanelGroupLayout({
assert(unsafeSize != null, `No layout data found for index ${index}`);
const safeSize = validatePanelSize({
+ overrideDisabledPanels: true,
panelConstraints: panelConstraints[index],
prevSize,
size: unsafeSize
@@ -70,6 +71,7 @@ export function validatePanelGroupLayout({
assert(prevSize != null, `No layout data found for index ${index}`);
const unsafeSize = prevSize + remainingSize;
const safeSize = validatePanelSize({
+ overrideDisabledPanels: true,
panelConstraints: panelConstraints[index],
prevSize,
size: unsafeSize
diff --git a/lib/global/utils/validatePanelSize.ts b/lib/global/utils/validatePanelSize.ts
index c2aaa24df..eb32fbc11 100644
--- a/lib/global/utils/validatePanelSize.ts
+++ b/lib/global/utils/validatePanelSize.ts
@@ -4,10 +4,12 @@ import { formatLayoutNumber } from "./formatLayoutNumber";
// Panel size must be in percentages; pixel values should be pre-converted
export function validatePanelSize({
+ overrideDisabledPanels,
panelConstraints,
prevSize,
size
}: {
+ overrideDisabledPanels?: boolean;
panelConstraints: PanelConstraints;
prevSize: number;
size: number;
@@ -20,7 +22,7 @@ export function validatePanelSize({
minSize = 0
} = panelConstraints;
- if (disabled) {
+ if (disabled && !overrideDisabledPanels) {
return prevSize;
}
diff --git a/package.json b/package.json
index 246e8cc9d..dc3518877 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
- "version": "4.6.0",
+ "version": "4.6.1",
"type": "module",
"author": "Brian Vaughn (https://github.com/bvaughn/)",
"contributors": [
diff --git a/src/App.tsx b/src/App.tsx
index daef2ab83..71c140684 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -227,7 +227,8 @@ const commonQuestions: CommonQuestion[] = [
Putting the defaultSize on the conditional{" "}
- Panel is an easy way to avoid invalid layout constraints.
+ Panel is the easiest way to avoid invalid layout
+ constraints in this type of scenario.
>
)
diff --git a/src/components/accordion/AccordionGroup.tsx b/src/components/accordion/AccordionGroup.tsx
new file mode 100644
index 000000000..b775ab286
--- /dev/null
+++ b/src/components/accordion/AccordionGroup.tsx
@@ -0,0 +1,11 @@
+import { type PropsWithChildren } from "react";
+import { Group, Panel } from "react-resizable-panels";
+
+export function AccordionGroup({ children }: PropsWithChildren) {
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/accordion/AccordionPanel.tsx b/src/components/accordion/AccordionPanel.tsx
new file mode 100644
index 000000000..4c850cfd9
--- /dev/null
+++ b/src/components/accordion/AccordionPanel.tsx
@@ -0,0 +1,35 @@
+import { useState, type PropsWithChildren } from "react";
+import { Panel } from "react-resizable-panels";
+
+export function AccordionPanel({
+ children,
+ index
+}: PropsWithChildren<{
+ index: number;
+}>) {
+ const [isCollapsed, setIsCollapsed] = useState(false);
+
+ return (
+
+ {
+ setIsCollapsed(!isCollapsed);
+ }}
+ >
+ {index}
+
+ {isCollapsed || (
+
+ {children}
+
+ )}
+
+ );
+}
diff --git a/src/routes/DisabledPanelsRoute.tsx b/src/routes/DisabledPanelsRoute.tsx
index 4c7541341..941febf39 100644
--- a/src/routes/DisabledPanelsRoute.tsx
+++ b/src/routes/DisabledPanelsRoute.tsx
@@ -1,8 +1,9 @@
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 { html as DisabledPanelsHTML } from "../../public/generated/examples/DisabledPanels.json";
+import { html as DisabledSeparatorHTML } from "../../public/generated/examples/DisabledSeparator.json";
+import { Link } from "../components/Link";
import { Group } from "../components/styled-panels/Group";
import { Panel } from "../components/styled-panels/Panel";
import { Separator } from "../components/styled-panels/Separator";
@@ -22,9 +23,9 @@ export default function DisabledPanelsRoute() {