Skip to content

Commit

Permalink
Add optional collapsedSize prop to Panel
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Jun 24, 2023
1 parent bcf641f commit a87cdb6
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 60 deletions.
9 changes: 5 additions & 4 deletions packages/react-resizable-panels-website/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { PlaywrightTestConfig } from "@playwright/test";

const { DEBUG } = process.env;

const config: PlaywrightTestConfig = {
use: {
headless: true,
Expand All @@ -20,10 +22,9 @@ if (process.env.DEBUG) {
browserName: "chromium",
headless: false,

// Uncomment to slow down interactions if needed for visual debugging.
// launchOptions: {
// slowMo: 100,
// },
launchOptions: {
slowMo: DEBUG ? 250 : undefined,
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ export default function CollapsibleRoute() {
headerNode={
<>
<p>
The example below uses the <code>collapsible</code> and{" "}
<code>onCollapse</code> props to mimic the UX of apps like VS Code
by configuring a panel to be <em>collapsible</em>.
The example below uses the <code>collapsedSize</code>,{" "}
<code>collapsible</code>, and <code>onCollapse</code> props to mimic
the UX of apps like VS Code by configuring a panel to be{" "}
<em>collapsible</em>. Panels can be collapsed all the way (to size
0) but in this example, the panel is configured to collapse to a
size that allows the file icons to remain visible.
</p>
<p>
Drag the bar between the file browser and the source viewer to
Expand Down Expand Up @@ -66,6 +69,7 @@ function Content() {
</div>
<Panel
className={sharedStyles.PanelColumn}
collapsedSize={4}
collapsible={true}
defaultSize={20}
maxSize={25}
Expand Down Expand Up @@ -165,7 +169,7 @@ const FILES: File[] = FILE_PATHS.map(([path, code]) => {
const CODE = `
<PanelGroup direction="horizontal">
<SideTabBar />
<Panel collapsible={true}>
<Panel collapsedSize={5} collapsible={true} minSize={10}>
<SourceBrowser />
</Panel>
<PanelResizeHandle />
Expand Down
3 changes: 3 additions & 0 deletions packages/react-resizable-panels-website/src/utils/UrlData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ImperativeDebugLogHandle } from "../routes/examples/DebugLog";

type UrlPanel = {
children: Array<string | UrlPanelGroup>;
collapsedSize?: number;
collapsible?: boolean;
defaultSize?: number | null;
id?: string | null;
Expand Down Expand Up @@ -100,6 +101,7 @@ function UrlPanelToData(urlPanel: ReactElement<PanelProps>): UrlPanel {
return child;
}
}),
collapsedSize: urlPanel.props.collapsedSize,
collapsible: urlPanel.props.collapsible,
defaultSize: urlPanel.props.defaultSize,
id: urlPanel.props.id,
Expand Down Expand Up @@ -193,6 +195,7 @@ function urlPanelToPanel(
Panel,
{
className: "Panel",
collapsedSize: urlPanel.collapsedSize,
collapsible: urlPanel.collapsible,
defaultSize: urlPanel.defaultSize,
id: urlPanel.id,
Expand Down
46 changes: 46 additions & 0 deletions packages/react-resizable-panels-website/tests/Collapsing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,50 @@ test.describe("collapsible prop", () => {
await page.keyboard.press("ArrowLeft");
await verifyPanelSize(lastPanel, 20);
});

test("should support custom collapsedSize values", async ({ page }) => {
await goToUrl(
page,
createElement(
PanelGroup,
{ direction: "horizontal" },
createElement(Panel, {
collapsedSize: 2,
collapsible: true,
defaultSize: 35,
minSize: 10,
}),
createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }),
createElement(Panel)
)
);

const resizeHandle = page.locator("[data-panel-resize-handle-id]");

const panels = page.locator("[data-panel]");
const firstPanel = panels.first();
const lastPanel = panels.last();

await verifyPanelSize(firstPanel, 35);
await verifyPanelSize(lastPanel, 65);

await resizeHandle.focus();
await page.keyboard.press("Shift+ArrowLeft");
await verifyPanelSize(firstPanel, 25);
await page.keyboard.press("Shift+ArrowLeft");
await verifyPanelSize(firstPanel, 15);
// Once it drops below half, it will collapse
await page.keyboard.press("Shift+ArrowLeft");
await verifyPanelSize(firstPanel, 2);
await page.keyboard.press("Shift+ArrowRight");
await verifyPanelSize(firstPanel, 12);
await page.keyboard.press("ArrowLeft");
await verifyPanelSize(firstPanel, 11);
await page.keyboard.press("ArrowLeft");
await verifyPanelSize(firstPanel, 10);
await page.keyboard.press("ArrowLeft");
await verifyPanelSize(firstPanel, 2);
await page.keyboard.press("ArrowRight");
await verifyPanelSize(firstPanel, 10);
});
});
29 changes: 15 additions & 14 deletions packages/react-resizable-panels/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,21 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
| `setLayout(panelSizes: number[])` | Resize panel group to the specified _panelSizes_ (`[1 - 100, ...]`).

### `Panel`
| prop | type | description
| :------------ | :------------------------------ | :---
| `children` | `ReactNode` | Arbitrary React element(s)
| `className` | `?string` | Class name to attach to root element
| `collapsible` | `?boolean=false` | Panel should collapse when resized beyond its `minSize`
| `defaultSize` | `?number` | Initial size of panel (numeric value between 1-100)
| `id` | `?string` | Panel id (unique within group); falls back to `useId` when not provided
| `maxSize` | `?number = 100` | Maximum allowable size of panel (numeric value between 1-100); defaults to `100`
| `minSize` | `?number = 10` | Minimum allowable size of panel (numeric value between 1-100); defaults to `10`
| `onCollapse` | `?(collapsed: boolean) => void` | Called when panel is collapsed; `collapsed` boolean parameter reflecting the new state
| `onResize` | `?(size: number) => void` | Called when panel is resized; `size` parameter is a numeric value between 1-100. <sup>1</sup>
| `order` | `?number` | Order of panel within group; required for groups with conditionally rendered panels
| `style` | `?CSSProperties` | CSS style to attach to root element
| `tagName` | `?string = "div"` | HTML element tag name for root element
| prop | type | description
| :-------------- | :------------------------------ | :---
| `children` | `ReactNode` | Arbitrary React element(s)
| `className` | `?string` | Class name to attach to root element
| `collapsedSize` | `?number=0` | Panel should collapse to this size
| `collapsible` | `?boolean=false` | Panel should collapse when resized beyond its `minSize`
| `defaultSize` | `?number` | Initial size of panel (numeric value between 1-100)
| `id` | `?string` | Panel id (unique within group); falls back to `useId` when not provided
| `maxSize` | `?number = 100` | Maximum allowable size of panel (numeric value between 1-100); defaults to `100`
| `minSize` | `?number = 10` | Minimum allowable size of panel (numeric value between 1-100); defaults to `10`
| `onCollapse` | `?(collapsed: boolean) => void` | Called when panel is collapsed; `collapsed` boolean parameter reflecting the new state
| `onResize` | `?(size: number) => void` | Called when panel is resized; `size` parameter is a numeric value between 1-100. <sup>1</sup>
| `order` | `?number` | Order of panel within group; required for groups with conditionally rendered panels
| `style` | `?CSSProperties` | CSS style to attach to root element
| `tagName` | `?string = "div"` | HTML element tag name for root element

<sup>1</sup>: If any `Panel` has an `onResize` callback, the `order` prop should be provided for all `Panel`s.

Expand Down
5 changes: 5 additions & 0 deletions packages/react-resizable-panels/src/Panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
export type PanelProps = {
children?: ReactNode;
className?: string;
collapsedSize?: number;
collapsible?: boolean;
defaultSize?: number | null;
id?: string | null;
Expand All @@ -47,6 +48,7 @@ export type ImperativePanelHandle = {
function PanelWithForwardedRef({
children = null,
className: classNameFromProps = "",
collapsedSize = 0,
collapsible = false,
defaultSize = null,
forwardedRef,
Expand Down Expand Up @@ -119,6 +121,7 @@ function PanelWithForwardedRef({
});
const panelDataRef = useRef<{
callbacksRef: PanelCallbackRef;
collapsedSize: number;
collapsible: boolean;
defaultSize: number | null;
id: string;
Expand All @@ -127,6 +130,7 @@ function PanelWithForwardedRef({
order: number | null;
}>({
callbacksRef,
collapsedSize,
collapsible,
defaultSize,
id: panelId,
Expand All @@ -138,6 +142,7 @@ function PanelWithForwardedRef({
committedValuesRef.current.size = parseSizeFromStyle(style);

panelDataRef.current.callbacksRef = callbacksRef;
panelDataRef.current.collapsedSize = collapsedSize;
panelDataRef.current.collapsible = collapsible;
panelDataRef.current.defaultSize = defaultSize;
panelDataRef.current.id = panelId;
Expand Down
54 changes: 33 additions & 21 deletions packages/react-resizable-panels/src/PanelGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@ function PanelGroupWithForwardedRef({
panelIdToLastNotifiedSizeMapRef.current;
const panelsArray = panelsMapToSortedArray(panels);

callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);

setSizes(sizes);

callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
},
}),
[]
Expand Down Expand Up @@ -487,14 +487,15 @@ function PanelGroupWithForwardedRef({
const panelIdToLastNotifiedSizeMap =
panelIdToLastNotifiedSizeMapRef.current;

setSizes(nextSizes);

// If resize change handlers have been declared, this is the time to call them.
// Trigger user callbacks after updating state, so that user code can override the sizes.
callPanelCallbacks(
panelsArray,
nextSizes,
panelIdToLastNotifiedSizeMap
);

setSizes(nextSizes);
}

prevDeltaRef.current = delta;
Expand Down Expand Up @@ -522,7 +523,12 @@ function PanelGroupWithForwardedRef({
const { panels, sizes: prevSizes } = committedValuesRef.current;

const panel = panels.get(id);
if (panel == null || !panel.current.collapsible) {
if (panel == null) {
return;
}

const { collapsedSize, collapsible } = panel.current;
if (!collapsible) {
return;
}

Expand All @@ -534,7 +540,7 @@ function PanelGroupWithForwardedRef({
}

const currentSize = prevSizes[index];
if (currentSize === 0) {
if (currentSize === collapsedSize) {
// Panel is already collapsed.
return;
}
Expand All @@ -547,7 +553,7 @@ function PanelGroupWithForwardedRef({
}

const isLastPanel = index === panelsArray.length - 1;
const delta = isLastPanel ? currentSize : 0 - currentSize;
const delta = isLastPanel ? currentSize : collapsedSize - currentSize;

const nextSizes = adjustByDelta(
null,
Expand All @@ -563,10 +569,11 @@ function PanelGroupWithForwardedRef({
const panelIdToLastNotifiedSizeMap =
panelIdToLastNotifiedSizeMapRef.current;

setSizes(nextSizes);

// If resize change handlers have been declared, this is the time to call them.
// Trigger user callbacks after updating state, so that user code can override the sizes.
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);

setSizes(nextSizes);
}
}, []);

Expand All @@ -578,8 +585,10 @@ function PanelGroupWithForwardedRef({
return;
}

const { collapsedSize, minSize } = panel.current;

const sizeBeforeCollapse =
panelSizeBeforeCollapse.current.get(id) || panel.current.minSize;
panelSizeBeforeCollapse.current.get(id) || minSize;
if (!sizeBeforeCollapse) {
return;
}
Expand All @@ -592,7 +601,7 @@ function PanelGroupWithForwardedRef({
}

const currentSize = prevSizes[index];
if (currentSize !== 0) {
if (currentSize !== collapsedSize) {
// Panel is already expanded.
return;
}
Expand All @@ -603,7 +612,9 @@ function PanelGroupWithForwardedRef({
}

const isLastPanel = index === panelsArray.length - 1;
const delta = isLastPanel ? 0 - sizeBeforeCollapse : sizeBeforeCollapse;
const delta = isLastPanel
? collapsedSize - sizeBeforeCollapse
: sizeBeforeCollapse;

const nextSizes = adjustByDelta(
null,
Expand All @@ -619,10 +630,11 @@ function PanelGroupWithForwardedRef({
const panelIdToLastNotifiedSizeMap =
panelIdToLastNotifiedSizeMapRef.current;

setSizes(nextSizes);

// If resize change handlers have been declared, this is the time to call them.
// Trigger user callbacks after updating state, so that user code can override the sizes.
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);

setSizes(nextSizes);
}
}, []);

Expand All @@ -634,6 +646,8 @@ function PanelGroupWithForwardedRef({
return;
}

const { collapsedSize, collapsible, maxSize, minSize } = panel.current;

const panelsArray = panelsMapToSortedArray(panels);

const index = panelsArray.indexOf(panel);
Expand All @@ -646,13 +660,10 @@ function PanelGroupWithForwardedRef({
return;
}

if (panel.current.collapsible && nextSize === 0) {
if (collapsible && nextSize === collapsedSize) {
// This is a valid resize state.
} else {
nextSize = Math.min(
panel.current.maxSize,
Math.max(panel.current.minSize, nextSize)
);
nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
}

const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
Expand All @@ -677,10 +688,11 @@ function PanelGroupWithForwardedRef({
const panelIdToLastNotifiedSizeMap =
panelIdToLastNotifiedSizeMapRef.current;

setSizes(nextSizes);

// If resize change handlers have been declared, this is the time to call them.
// Trigger user callbacks after updating state, so that user code can override the sizes.
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);

setSizes(nextSizes);
}
}, []);

Expand Down
3 changes: 2 additions & 1 deletion packages/react-resizable-panels/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type PanelGroupStorage = {

export type PanelGroupOnLayout = (sizes: number[]) => void;
export type PanelOnCollapse = (collapsed: boolean) => void;
export type PanelOnResize = (size: number) => void;
export type PanelOnResize = (size: number, prevSize: number) => void;
export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;

export type PanelCallbackRef = RefObject<{
Expand All @@ -20,6 +20,7 @@ export type PanelCallbackRef = RefObject<{
export type PanelData = {
current: {
callbacksRef: PanelCallbackRef;
collapsedSize: number;
collapsible: boolean;
defaultSize: number | null;
id: string;
Expand Down
Loading

1 comment on commit a87cdb6

@vercel
Copy link

@vercel vercel bot commented on a87cdb6 Jun 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.