diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11ebacb81..75b53640f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 4.5.3
+
+- [635](https://github.com/bvaughn/react-resizable-panels/pull/635): Expand pre-collapsed panels if drug past the halfway point for more consistent collapse/expand behavior.
+- [631](https://github.com/bvaughn/react-resizable-panels/pull/631): **Bugfix**: Panels set `max-width` and `max-height` to 100% to fix potential CSS overflow bug.
+
## 4.5.2
- [626](https://github.com/bvaughn/react-resizable-panels/pull/626): Decrease default hit target size for `Separator` and `Panel` edges; make configurable via a new `Group` prop.
diff --git a/lib/components/panel/Panel.tsx b/lib/components/panel/Panel.tsx
index 8814e39d8..1db49bc85 100644
--- a/lib/components/panel/Panel.tsx
+++ b/lib/components/panel/Panel.tsx
@@ -137,6 +137,8 @@ export function Panel({
{
test("should fallback to the previous layout if an intermediate layout is invalid", () => {
expect(
adjustLayoutByDelta({
- delta: 1,
+ delta: 16,
initialLayout: l([5, 15, 40, 40]),
panelConstraints: c([
{ collapsedSize: 5, collapsible: true, minSize: 15, maxSize: 20 },
- { minSize: 15, maxSize: 30 },
+ { minSize: 30, maxSize: 30 },
{ minSize: 30 },
{ minSize: 20, maxSize: 40 }
]),
- prevLayout: l([5, 30, 30, 36])
+ prevLayout: l([20, 30, 30, 20])
})
- ).toEqual(l([5, 30, 30, 36]));
+ ).toEqual(l([20, 30, 30, 20]));
});
// Edge case (issues/311)
@@ -1935,4 +1935,211 @@ describe("adjustLayoutByDelta", () => {
})
).toEqual(l([5, 15, 40, 40]));
});
+
+ // Edge case issues/210 and issues/629
+ describe("collapsible panel thresholds", () => {
+ ["left", "right"].forEach((panelId) => {
+ describe(`${panelId} panel`, () => {
+ const collapsiblePanelConstraints = {
+ collapsedSize: 0,
+ collapsible: true,
+ minSize: 10
+ };
+
+ const open = panelId === "left" ? l([10, 90]) : l([90, 10]);
+ const closed = panelId === "left" ? l([0, 100]) : l([100, 0]);
+ const panelConstraints =
+ panelId === "left"
+ ? c([collapsiblePanelConstraints, {}])
+ : c([{}, collapsiblePanelConstraints]);
+
+ test.each([
+ [
+ "remain open if delta is less than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: open,
+ delta: panelId === "left" ? -4 : 4,
+ expectedLayout: open
+ }
+ ],
+ [
+ "close if delta is greater than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: open,
+ delta: panelId === "left" ? -6 : 6,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "re-open if delta is less than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: closed,
+ delta: panelId === "left" ? -4 : 4,
+ expectedLayout: open
+ }
+ ],
+ [
+ "remain closed if delta is more than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: closed,
+ delta: panelId === "left" ? -6 : 6,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "remain closed if delta is less than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: closed,
+ delta: panelId === "left" ? 4 : -4,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "open if delta is greater than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: closed,
+ delta: panelId === "left" ? 6 : -6,
+ expectedLayout: open
+ }
+ ],
+ [
+ "close if delta is less than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: open,
+ delta: panelId === "left" ? 4 : -4,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "remain open if delta is more than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: open,
+ delta: panelId === "left" ? 6 : -6,
+ expectedLayout: open
+ }
+ ]
+ ])("%s", (_, { initialLayout, prevLayout, delta, expectedLayout }) => {
+ expect(
+ adjustLayoutByDelta({
+ delta,
+ initialLayout,
+ panelConstraints,
+ prevLayout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(expectedLayout);
+ });
+ });
+ });
+
+ ["left", "right"].forEach((panelId) => {
+ describe(`${panelId} panel`, () => {
+ const collapsiblePanelConstraints = {
+ collapsedSize: 5,
+ collapsible: true,
+ minSize: 15
+ };
+
+ const open = panelId === "left" ? l([15, 85]) : l([85, 15]);
+ const closed = panelId === "left" ? l([5, 95]) : l([95, 5]);
+ const panelConstraints =
+ panelId === "left"
+ ? c([collapsiblePanelConstraints, {}])
+ : c([{}, collapsiblePanelConstraints]);
+
+ test.each([
+ [
+ "remain open if delta is less than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: open,
+ delta: panelId === "left" ? -4 : 4,
+ expectedLayout: open
+ }
+ ],
+ [
+ "close if delta is greater than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: open,
+ delta: panelId === "left" ? -6 : 6,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "re-open if delta is less than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: closed,
+ delta: panelId === "left" ? -4 : 4,
+ expectedLayout: open
+ }
+ ],
+ [
+ "remain closed if delta is more than minimum threshold",
+ {
+ initialLayout: open,
+ prevLayout: closed,
+ delta: panelId === "left" ? -6 : 6,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "remain closed if delta is less than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: closed,
+ delta: panelId === "left" ? 4 : -4,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "open if delta is greater than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: closed,
+ delta: panelId === "left" ? 6 : -6,
+ expectedLayout: open
+ }
+ ],
+ [
+ "close if delta is less than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: open,
+ delta: panelId === "left" ? 4 : -4,
+ expectedLayout: closed
+ }
+ ],
+ [
+ "remain open if delta is more than minimum threshold",
+ {
+ initialLayout: closed,
+ prevLayout: open,
+ delta: panelId === "left" ? 6 : -6,
+ expectedLayout: open
+ }
+ ]
+ ])("%s", (_, { initialLayout, prevLayout, delta, expectedLayout }) => {
+ expect(
+ adjustLayoutByDelta({
+ delta,
+ initialLayout,
+ panelConstraints,
+ prevLayout,
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(expectedLayout);
+ });
+ });
+ });
+ });
});
diff --git a/lib/global/utils/adjustLayoutByDelta.ts b/lib/global/utils/adjustLayoutByDelta.ts
index 3aed5810d..33cc46f78 100644
--- a/lib/global/utils/adjustLayoutByDelta.ts
+++ b/lib/global/utils/adjustLayoutByDelta.ts
@@ -20,7 +20,7 @@ export function adjustLayoutByDelta({
panelConstraints: PanelConstraints[];
pivotIndices: number[];
prevLayout: Layout;
- trigger: "imperative-api" | "keyboard" | "mouse-or-touch";
+ trigger?: "imperative-api" | "keyboard" | "mouse-or-touch";
}): Layout {
if (layoutNumbersEqual(delta, 0)) {
return initialLayoutProp;
@@ -54,79 +54,123 @@ export function adjustLayoutByDelta({
// This is accomplished by shrinking/contracting (and shifting) one or more of the panels after the separator.
{
- // If this is a resize triggered by a keyboard event, our logic for expanding/collapsing is different.
- // We no longer check the halfway threshold because this may prevent the panel from expanding at all.
- if (trigger === "keyboard") {
- {
- // Check if we should expand a collapsed panel
- const index = delta < 0 ? secondPivotIndex : firstPivotIndex;
- const panelConstraints = panelConstraintsArray[index];
- assert(
- panelConstraints,
- `Panel constraints not found for index ${index}`
- );
+ switch (trigger) {
+ case "keyboard": {
+ // If this is a resize triggered by a keyboard event, our logic for expanding/collapsing is different.
+ // We no longer check the halfway threshold because this may prevent the panel from expanding at all.
+ {
+ // Check if we should expand a collapsed panel
+ const index = delta < 0 ? secondPivotIndex : firstPivotIndex;
+ const panelConstraints = panelConstraintsArray[index];
+ assert(
+ panelConstraints,
+ `Panel constraints not found for index ${index}`
+ );
- const {
- collapsedSize = 0,
- collapsible,
- minSize = 0
- } = panelConstraints;
+ const {
+ collapsedSize = 0,
+ collapsible,
+ minSize = 0
+ } = panelConstraints;
+
+ // DEBUG.push(`edge case check 1: ${index}`);
+ // DEBUG.push(` -> collapsible? ${collapsible}`);
+ if (collapsible) {
+ const prevSize = initialLayout[index];
+ assert(
+ prevSize != null,
+ `Previous layout not found for panel index ${index}`
+ );
+
+ if (layoutNumbersEqual(prevSize, collapsedSize)) {
+ const localDelta = minSize - prevSize;
+ // DEBUG.push(` -> expand delta: ${localDelta}`);
+
+ if (compareLayoutNumbers(localDelta, Math.abs(delta)) > 0) {
+ delta = delta < 0 ? 0 - localDelta : localDelta;
+ // DEBUG.push(` -> delta: ${delta}`);
+ }
+ }
+ }
+ }
- // DEBUG.push(`edge case check 1: ${index}`);
- // DEBUG.push(` -> collapsible? ${collapsible}`);
- if (collapsible) {
- const prevSize = initialLayout[index];
+ {
+ // Check if we should collapse a panel at its minimum size
+ const index = delta < 0 ? firstPivotIndex : secondPivotIndex;
+ const panelConstraints = panelConstraintsArray[index];
assert(
- prevSize != null,
- `Previous layout not found for panel index ${index}`
+ panelConstraints,
+ `No panel constraints found for index ${index}`
);
- if (layoutNumbersEqual(prevSize, collapsedSize)) {
- const localDelta = minSize - prevSize;
- // DEBUG.push(` -> expand delta: ${localDelta}`);
-
- if (compareLayoutNumbers(localDelta, Math.abs(delta)) > 0) {
- delta = delta < 0 ? 0 - localDelta : localDelta;
- // DEBUG.push(` -> delta: ${delta}`);
+ const {
+ collapsedSize = 0,
+ collapsible,
+ minSize = 0
+ } = panelConstraints;
+
+ // DEBUG.push(`edge case check 2: ${index}`);
+ // DEBUG.push(` -> collapsible? ${collapsible}`);
+ if (collapsible) {
+ const prevSize = initialLayout[index];
+ assert(
+ prevSize != null,
+ `Previous layout not found for panel index ${index}`
+ );
+
+ if (layoutNumbersEqual(prevSize, minSize)) {
+ const localDelta = prevSize - collapsedSize;
+ // DEBUG.push(` -> expand delta: ${localDelta}`);
+
+ if (compareLayoutNumbers(localDelta, Math.abs(delta)) > 0) {
+ delta = delta < 0 ? 0 - localDelta : localDelta;
+ // DEBUG.push(` -> delta: ${delta}`);
+ }
}
}
}
+ break;
}
+ default: {
+ // If we're starting from a collapsed state, dragging past the halfway point should cause the panel to expand
+ // This can happen for positive or negative drags, and panels on either side of the separator can be collapsible
+ // The easiest way to support this is to detect this scenario and pre-adjust the delta before applying the rest of the layout algorithm
+ // DEBUG.push(`edge case check 3: collapsible panels`);
- {
- // Check if we should collapse a panel at its minimum size
- const index = delta < 0 ? firstPivotIndex : secondPivotIndex;
+ const index = delta < 0 ? secondPivotIndex : firstPivotIndex;
const panelConstraints = panelConstraintsArray[index];
assert(
panelConstraints,
- `No panel constraints found for index ${index}`
+ `Panel constraints not found for index ${index}`
);
- const {
- collapsedSize = 0,
- collapsible,
- minSize = 0
- } = panelConstraints;
-
- // DEBUG.push(`edge case check 2: ${index}`);
- // DEBUG.push(` -> collapsible? ${collapsible}`);
+ const { collapsible, collapsedSize, minSize } = panelConstraints;
if (collapsible) {
- const prevSize = initialLayout[index];
- assert(
- prevSize != null,
- `Previous layout not found for panel index ${index}`
- );
-
- if (layoutNumbersEqual(prevSize, minSize)) {
- const localDelta = prevSize - collapsedSize;
- // DEBUG.push(` -> expand delta: ${localDelta}`);
-
- if (compareLayoutNumbers(localDelta, Math.abs(delta)) > 0) {
- delta = delta < 0 ? 0 - localDelta : localDelta;
- // DEBUG.push(` -> delta: ${delta}`);
+ // DEBUG.push(` -> collapsible panel`);
+ // DEBUG.push(` -> halfway point: ${halfwayPoint}`);
+ if (delta > 0) {
+ const gapSize = minSize - collapsedSize;
+ const halfwayPoint = gapSize / 2;
+
+ if (compareLayoutNumbers(delta, minSize) < 0) {
+ delta =
+ compareLayoutNumbers(delta, halfwayPoint) <= 0 ? 0 : gapSize;
+ // DEBUG.push(` -> adjusting delta for collapse: ${delta}`);
+ }
+ } else {
+ const gapSize = minSize - collapsedSize;
+ const halfwayPoint = 100 - gapSize / 2;
+
+ if (compareLayoutNumbers(100 + delta, minSize) > 0) {
+ delta =
+ compareLayoutNumbers(100 + delta, halfwayPoint) > 0
+ ? 0
+ : -gapSize;
+ // DEBUG.push(` -> adjusting delta for collapse: ${delta}`);
}
}
}
+ break;
}
}
// DEBUG.push("");
diff --git a/lib/global/utils/calculateSeparatorAriaValues.ts b/lib/global/utils/calculateSeparatorAriaValues.ts
index ed714d2f5..0deccd45e 100644
--- a/lib/global/utils/calculateSeparatorAriaValues.ts
+++ b/lib/global/utils/calculateSeparatorAriaValues.ts
@@ -41,8 +41,7 @@ export function calculateSeparatorAriaValues({
initialLayout: layout,
panelConstraints,
pivotIndices,
- prevLayout: layout,
- trigger: "keyboard"
+ prevLayout: layout
}),
panelConstraints
});
@@ -55,8 +54,7 @@ export function calculateSeparatorAriaValues({
initialLayout: layout,
panelConstraints,
pivotIndices,
- prevLayout: layout,
- trigger: "keyboard"
+ prevLayout: layout
}),
panelConstraints
});
diff --git a/package.json b/package.json
index d6ee42183..6ca741474 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
- "version": "4.5.2",
+ "version": "4.5.3",
"type": "module",
"author": "Brian Vaughn (https://github.com/bvaughn/)",
"contributors": [