From 32c1fbc768ba1e8aae5a26f0917c7f48f94ef777 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 28 Jan 2026 06:51:43 -0500 Subject: [PATCH 1/3] Panels set `max-width` and `max-height` to 100% to fix potential CSS overflow bug (#633) Published as `react-resizable-panels@4.5.3-alpha.0` --- CHANGELOG.md | 4 ++++ lib/components/panel/Panel.tsx | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ebacb81..aded7ba69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- [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({
Date: Wed, 28 Jan 2026 06:53:23 -0500 Subject: [PATCH 2/3] Expand pre-collapsed panels if drug past the halfway point (#635) Resolves #629 --- lib/global/utils/adjustLayoutByDelta.test.ts | 215 +++++++++++++++++- lib/global/utils/adjustLayoutByDelta.ts | 154 ++++++++----- .../utils/calculateSeparatorAriaValues.ts | 6 +- 3 files changed, 312 insertions(+), 63 deletions(-) diff --git a/lib/global/utils/adjustLayoutByDelta.test.ts b/lib/global/utils/adjustLayoutByDelta.test.ts index ce6323976..96b17f2d7 100644 --- a/lib/global/utils/adjustLayoutByDelta.test.ts +++ b/lib/global/utils/adjustLayoutByDelta.test.ts @@ -1906,17 +1906,17 @@ describe("adjustLayoutByDelta", () => { 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 }); From 55893417b97124960de1bcddf93e5a95db28992e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 28 Jan 2026 06:55:55 -0500 Subject: [PATCH 3/3] 4.5.2 -> 4.5.3 --- CHANGELOG.md | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aded7ba69..75b53640f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## Unreleased +## 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 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": [