diff --git a/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelApi.tsx b/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelApi.tsx index 409d3c023..080acd7e5 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelApi.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelApi.tsx @@ -72,10 +72,11 @@ export default function ImperativePanelApiRoute() {
  • - Expand the panel to its previous size + Expand the panel to its previous size (or the default size if + there is no previous size)
  • { - + ); }); @@ -193,6 +198,111 @@ describe("PanelGroup", () => { expect(leftPanelRef.current?.isCollapsed()).toBe(false); expect(leftPanelRef.current?.isExpanded()).toBe(true); }); + + describe("when a panel is mounted in a collapsed state", () => { + beforeEach(() => { + act(() => { + root.unmount(); + }); + }); + + it("should expand to the panel's minSize", () => { + const panelRef = createRef(); + + root = createRoot(container); + + function renderPanelGroup() { + act(() => { + root.render( + + + + + + ); + }); + } + + // Re-render and confirmed collapsed by default + renderPanelGroup(); + act(() => { + panelRef.current?.collapse(); + }); + expect(panelRef.current?.getSize()).toEqual(0); + + // Toggling a panel should expand to the minSize (since there's no previous size to restore to) + act(() => { + panelRef.current?.expand(); + }); + expect(panelRef.current?.getSize()).toEqual(5); + + // Collapse again + act(() => { + panelRef.current?.collapse(); + }); + expect(panelRef.current?.getSize()).toEqual(0); + + // Toggling the panel should expand to the minSize override if one is specified + // Note this only works because the previous non-collapsed size is less than the minSize override + act(() => { + panelRef.current?.expand(15); + }); + expect(panelRef.current?.getSize()).toEqual(15); + }); + + it("should support the (optional) default size", () => { + const panelRef = createRef(); + + root = createRoot(container); + + function renderPanelGroup() { + act(() => { + root.render( + + + + + + ); + }); + } + + // Re-render and confirmed collapsed by default + renderPanelGroup(); + act(() => { + panelRef.current?.collapse(); + }); + expect(panelRef.current?.getSize()).toEqual(0); + + // In this case, toggling the panel to expanded will not change its size + act(() => { + panelRef.current?.expand(); + }); + expect(panelRef.current?.getSize()).toEqual(0); + + // But we can override the toggle behavior by passing an explicit min size + act(() => { + panelRef.current?.expand(10); + }); + expect(panelRef.current?.getSize()).toEqual(10); + + // Toggling an already-expanded panel should not do anything even if we pass a default size + act(() => { + panelRef.current?.expand(15); + }); + expect(panelRef.current?.getSize()).toEqual(10); + }); + }); }); describe("resize", () => { diff --git a/packages/react-resizable-panels/src/Panel.ts b/packages/react-resizable-panels/src/Panel.ts index 68a1f21de..b71eb6f2e 100644 --- a/packages/react-resizable-panels/src/Panel.ts +++ b/packages/react-resizable-panels/src/Panel.ts @@ -46,7 +46,7 @@ export type PanelData = { export type ImperativePanelHandle = { collapse: () => void; - expand: () => void; + expand: (minSize?: number) => void; getId(): string; getSize(): number; isCollapsed: () => boolean; @@ -200,8 +200,8 @@ export function PanelWithForwardedRef({ collapse: () => { collapsePanel(panelDataRef.current); }, - expand: () => { - expandPanel(panelDataRef.current); + expand: (minSize?: number) => { + expandPanel(panelDataRef.current, minSize); }, getId() { return panelId; diff --git a/packages/react-resizable-panels/src/PanelGroup.ts b/packages/react-resizable-panels/src/PanelGroup.ts index b51ff3a12..48c203b01 100644 --- a/packages/react-resizable-panels/src/PanelGroup.ts +++ b/packages/react-resizable-panels/src/PanelGroup.ts @@ -386,65 +386,72 @@ function PanelGroupWithForwardedRef({ }, []); // External APIs are safe to memoize via committed values ref - const expandPanel = useCallback((panelData: PanelData) => { - const { onLayout } = committedValuesRef.current; - const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; + const expandPanel = useCallback( + (panelData: PanelData, minSizeOverride?: number) => { + const { onLayout } = committedValuesRef.current; + const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; - if (panelData.constraints.collapsible) { - const panelConstraintsArray = panelDataArray.map( - (panelData) => panelData.constraints - ); + if (panelData.constraints.collapsible) { + const panelConstraintsArray = panelDataArray.map( + (panelData) => panelData.constraints + ); - const { - collapsedSize = 0, - panelSize = 0, - minSize = 0, - pivotIndices, - } = panelDataHelper(panelDataArray, panelData, prevLayout); + const { + collapsedSize = 0, + panelSize = 0, + minSize: minSizeFromProps = 0, + pivotIndices, + } = panelDataHelper(panelDataArray, panelData, prevLayout); - if (fuzzyNumbersEqual(panelSize, collapsedSize)) { - // Restore this panel to the size it was before it was collapsed, if possible. - const prevPanelSize = panelSizeBeforeCollapseRef.current.get( - panelData.id - ); + const minSize = minSizeOverride ?? minSizeFromProps; - const baseSize = - prevPanelSize != null && prevPanelSize >= minSize - ? prevPanelSize - : minSize; + if (fuzzyNumbersEqual(panelSize, collapsedSize)) { + // Restore this panel to the size it was before it was collapsed, if possible. + const prevPanelSize = panelSizeBeforeCollapseRef.current.get( + panelData.id + ); - const isLastPanel = - findPanelDataIndex(panelDataArray, panelData) === - panelDataArray.length - 1; - const delta = isLastPanel ? panelSize - baseSize : baseSize - panelSize; + const baseSize = + prevPanelSize != null && prevPanelSize >= minSize + ? prevPanelSize + : minSize; + + const isLastPanel = + findPanelDataIndex(panelDataArray, panelData) === + panelDataArray.length - 1; + const delta = isLastPanel + ? panelSize - baseSize + : baseSize - panelSize; + + const nextLayout = adjustLayoutByDelta({ + delta, + initialLayout: prevLayout, + panelConstraints: panelConstraintsArray, + pivotIndices, + prevLayout, + trigger: "imperative-api", + }); - const nextLayout = adjustLayoutByDelta({ - delta, - initialLayout: prevLayout, - panelConstraints: panelConstraintsArray, - pivotIndices, - prevLayout, - trigger: "imperative-api", - }); + if (!compareLayouts(prevLayout, nextLayout)) { + setLayout(nextLayout); - if (!compareLayouts(prevLayout, nextLayout)) { - setLayout(nextLayout); + eagerValuesRef.current.layout = nextLayout; - eagerValuesRef.current.layout = nextLayout; + if (onLayout) { + onLayout(nextLayout); + } - if (onLayout) { - onLayout(nextLayout); + callPanelCallbacks( + panelDataArray, + nextLayout, + panelIdToLastNotifiedSizeMapRef.current + ); } - - callPanelCallbacks( - panelDataArray, - nextLayout, - panelIdToLastNotifiedSizeMapRef.current - ); } } - } - }, []); + }, + [] + ); // External APIs are safe to memoize via committed values ref const getPanelSize = useCallback((panelData: PanelData) => { diff --git a/packages/react-resizable-panels/src/PanelGroupContext.ts b/packages/react-resizable-panels/src/PanelGroupContext.ts index d810ee083..02eef1abd 100644 --- a/packages/react-resizable-panels/src/PanelGroupContext.ts +++ b/packages/react-resizable-panels/src/PanelGroupContext.ts @@ -16,7 +16,7 @@ export type TPanelGroupContext = { collapsePanel: (panelData: PanelData) => void; direction: "horizontal" | "vertical"; dragState: DragState | null; - expandPanel: (panelData: PanelData) => void; + expandPanel: (panelData: PanelData, minSizeOverride?: number) => void; getPanelSize: (panelData: PanelData) => number; getPanelStyle: ( panelData: PanelData,