Skip to content

Commit

Permalink
Rewrite layout algorithm internals (#182)
Browse files Browse the repository at this point in the history
Support mix of percentage and pixel units within a group or panel.

```jsx
<Panel
  defaultSizePixels={100}
  minSizePercentage={20}
  maxSizePercentage={50}
/>
```
  • Loading branch information
bvaughn authored Nov 12, 2023
1 parent c5898fe commit bb84b29
Show file tree
Hide file tree
Showing 110 changed files with 9,109 additions and 3,801 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/e2e-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ jobs:
with:
version: 7
- name: Install dependencies
run: pnpm install -r
run: pnpm install --frozen-lockfile --recursive
- name: Install Playwright dependencies
run: npx playwright install
- name: Build NPM package
run: pnpm prerelease
- name: Run Playwright tests
run: cd packages/react-resizable-panels-website && pnpm test:e2e
run: cd packages/react-resizable-panels-website && pnpm test:e2e
2 changes: 1 addition & 1 deletion .github/workflows/eslint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ jobs:
with:
version: 7
- name: Install dependencies
run: pnpm install -r
run: pnpm install --frozen-lockfile --recursive
- name: Run ESLint
run: pnpm lint
17 changes: 17 additions & 0 deletions .github/workflows/jest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: "Jest"
on: [pull_request]
jobs:
tests-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 7
- name: Install dependencies
run: pnpm install --frozen-lockfile --recursive
- name: Build NPM packages
run: pnpm run prerelease
- name: Run tests
run: cd packages/react-resizable-panels && pnpm run test
2 changes: 1 addition & 1 deletion .github/workflows/prettier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ jobs:
with:
version: 7
- name: Install dependencies
run: pnpm install -r
run: pnpm install --frozen-lockfile --recursive
- name: Run Prettier
run: pnpm run prettier:ci
2 changes: 1 addition & 1 deletion .github/workflows/typescript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
with:
version: 7
- name: Install dependencies
run: pnpm install -r
run: pnpm install --frozen-lockfile --recursive
- name: Build NPM package
run: pnpm prerelease
- name: Run TypeScript
Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
auto-install-peers=false
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<img width="328" alt="React Resizable Panels logo" src="https://user-images.githubusercontent.com/29597/210075327-faeb4ca8-31df-4dc8-a649-01d0ee3cd315.png" />

## react-resizable-panels

React components for resizable panel groups/layouts.

* [View the website](https://react-resizable-panels.vercel.app/)
* [Try it on CodeSandbox](https://codesandbox.io/s/react-resizable-panels-zf7hwd)
* [Read the documentation](https://github.com/bvaughn/react-resizable-panels/tree/main/packages/react-resizable-panels)
* [View the changelog](https://github.com/bvaughn/react-resizable-panels/blob/main/packages/react-resizable-panels/CHANGELOG.md)
- [View the website](https://react-resizable-panels.vercel.app/)
- [Try it on CodeSandbox](https://codesandbox.io/s/react-resizable-panels-zf7hwd)
- [Read the documentation](https://github.com/bvaughn/react-resizable-panels/tree/main/packages/react-resizable-panels)
- [View the changelog](https://github.com/bvaughn/react-resizable-panels/blob/main/packages/react-resizable-panels/CHANGELOG.md)

Supported input methods include mouse, touch, and keyboard (via [Window Splitter](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)).

Expand All @@ -26,13 +27,13 @@ The `Panel` API doesn't _require_ `id` and `order` props because they aren't nec
<PanelGroup direction="horizontal">
{renderSideBar && (
<>
<Panel id="sidebar" minSize={25} order={1}>
<Panel id="sidebar" minSizePercentage={25} order={1}>
<Sidebar />
</Panel>
<PanelResizeHandle />
</>
)}
<Panel minSize={25} order={2}>
<Panel minSizePercentage={25} order={2}>
<Main />
</Panel>
</PanelGroup>
Expand All @@ -43,6 +44,7 @@ The `Panel` API doesn't _require_ `id` and `order` props because they aren't nec
By default, this library uses `localStorage` to persist layouts. With server rendering, this can cause a flicker when the default layout (rendered on the server) is replaced with the persisted layout (in `localStorage`). The way to avoid this flicker is to also persist the layout with a cookie like so:

##### Server component

```tsx
import ResizablePanels from "@/app/ResizablePanels";
import { cookies } from "next/headers";
Expand All @@ -60,32 +62,29 @@ export function ServerComponent() {
```

##### Client component

```tsx
"use client";

import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";

export function ClientComponent({
defaultLayout = [33, 67]
defaultLayout = [33, 67],
}: {
defaultLayout: number[] | undefined
defaultLayout: number[] | undefined;
}) {
const onLayout = (sizes: number[]) => {
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`;
};

return (
<PanelGroup direction="horizontal" onLayout={onLayout}>
<Panel defaultSize={defaultLayout[0]}>
{/* ... */}
</Panel>
<Panel defaultSizePercentage={defaultLayout[0]}>{/* ... */}</Panel>
<PanelResizeHandle className="w-2 bg-blue-800" />
<Panel defaultSize={defaultLayout[1]}>
{/* ... */}
</Panel>
<Panel defaultSizePercentage={defaultLayout[1]}>{/* ... */}</Panel>
</PanelGroup>
);
}
```

A demo of this is available [here](https://github.com/bvaughn/react-resizable-panels-demo-ssr).
A demo of this is available [here](https://github.com/bvaughn/react-resizable-panels-demo-ssr).
24 changes: 14 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,23 @@
"typescript:watch": "tsc --noEmit --watch"
},
"devDependencies": {
"@babel/preset-typescript": "^7.21.5",
"@playwright/test": "^1.35.0",
"@types/node": "^18.15.11",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"@typescript-eslint/type-utils": "^5.57.0",
"eslint": "^8.37.0",
"@babel/preset-typescript": "^7.22.5",
"@playwright/test": "^1.37.0",
"@types/jest": "^29.5.3",
"@types/node": "^18.17.5",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@typescript-eslint/type-utils": "^5.62.0",
"eslint": "^8.47.0",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"parcel": "^2.9.3",
"prettier": "latest",
"process": "^0.11.10",
"typescript": ">=3.0.0"
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
},
"dependencies": {
"@parcel/config-default": "^2.9.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PanelResizeHandle } from "react-resizable-panels";
import Icon from "./Icon";
import styles from "./ResizeHandle.module.css";

export default function ResizeHandle({
export function ResizeHandle({
className = "",
collapsed = false,
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import {
import {
ImperativePanelGroupHandle,
ImperativePanelHandle,
Units,
getAvailableGroupSizePixels,
MixedSizes,
} from "react-resizable-panels";

import { urlPanelGroupToPanelGroup, urlToUrlData } from "../../utils/UrlData";
Expand Down Expand Up @@ -55,8 +54,10 @@ function EndToEndTesting() {
const [panelIds, setPanelIds] = useState<string[]>([]);
const [panelGroupId, setPanelGroupId] = useState("");
const [panelGroupIds, setPanelGroupIds] = useState<string[]>([]);
const [size, setSize] = useState(0);
const [units, setUnits] = useState("");
const [sizePercentage, setSizePercentage] = useState<number | undefined>(
undefined
);
const [sizePixels, setSizePixels] = useState<number | undefined>(undefined);
const [layoutString, setLayoutString] = useState("");

const debugLogRef = useRef<ImperativeDebugLogHandle>(null);
Expand All @@ -80,10 +81,6 @@ function EndToEndTesting() {
);
setPanelGroupIds(panelGroupIds);
setPanelGroupId(panelGroupIds[0]);

// const panelGroupElement = document.querySelector("[data-panel-group]")!;
// const units = panelGroupElement.getAttribute("data-panel-group-units")!;
// setUnits(units);
};

window.addEventListener("popstate", (event) => {
Expand Down Expand Up @@ -113,12 +110,11 @@ function EndToEndTesting() {
panelId
) as ImperativePanelHandle;
if (panel != null) {
const percentage = panel.getSize("percentages");
const pixels = panel.getSize("pixels");
const { sizePercentage, sizePixels } = panel.getSize();

panelElement.textContent = `${percentage.toFixed(
panelElement.textContent = `${sizePercentage.toFixed(
1
)}%\n${pixels.toFixed(1)}px`;
)}%\n${sizePixels.toFixed(1)}px`;
}
}
}, 0);
Expand Down Expand Up @@ -174,12 +170,16 @@ function EndToEndTesting() {

const onSizeInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.value;
setSize(parseFloat(value));
};

const onUnitsSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
const value = event.currentTarget.value;
setUnits(value);
if (value.endsWith("%")) {
setSizePercentage(parseFloat(value));
setSizePixels(undefined);
} else if (value.endsWith("px")) {
setSizePercentage(undefined);
setSizePixels(parseFloat(value));
} else {
throw Error(`Invalid size: ${value}`);
}
};

const onCollapseButtonClick = () => {
Expand All @@ -202,18 +202,28 @@ function EndToEndTesting() {
const idToRefMap = idToRefMapRef.current;
const panel = idToRefMap.get(panelId);
if (panel && assertImperativePanelHandle(panel)) {
panel.resize(size, (units as any) || undefined);
panel.resize({ sizePercentage, sizePixels });
}
};

const onSetLayoutButton = () => {
const idToRefMap = idToRefMapRef.current;
const panelGroup = idToRefMap.get(panelGroupId);
if (panelGroup && assertImperativePanelGroupHandle(panelGroup)) {
panelGroup.setLayout(
JSON.parse(layoutString),
(units as any) || undefined
const trimmedLayoutString = layoutString.substring(
1,
layoutString.length - 1
);
const layout = trimmedLayoutString.split(",").map((text) => {
if (text.endsWith("%")) {
return { sizePercentage: parseFloat(text) };
} else if (text.endsWith("px")) {
return { sizePixels: parseFloat(text) };
} else {
throw Error(`Invalid layout: ${layoutString}`);
}
}) satisfies Partial<MixedSizes>[];
panelGroup.setLayout(layout);
}
};

Expand Down Expand Up @@ -251,8 +261,8 @@ function EndToEndTesting() {
className={styles.Input}
id="sizeInput"
onChange={onSizeInputChange}
placeholder="Size"
type="number"
placeholder="Size (% or px)"
type="text"
/>
<button
id="resizeButton"
Expand All @@ -262,18 +272,6 @@ function EndToEndTesting() {
<Icon type="resize" />
</button>
<div className={styles.Spacer} />
<select
className={styles.Input}
defaultValue={units}
id="unitsSelect"
onChange={onUnitsSelectChange}
placeholder="Units"
>
<option value=""></option>
<option value="percentages">percentages</option>
<option value="pixels">pixels</option>
</select>
<div className={styles.Spacer} />
<select
className={styles.Input}
id="panelGroupIdSelect"
Expand Down Expand Up @@ -302,8 +300,8 @@ function EndToEndTesting() {
</button>
</div>
</div>
<div className={styles.Children}>{children}</div>
<DebugLog apiRef={debugLogRef} />
<div className={styles.Children}>{children}</div>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useReducer } from "react";

import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";

import {
Expand Down Expand Up @@ -56,25 +57,30 @@ function Content() {
dispatch({ type: "open", file });
};

const toggleCollapsed = (collapsed: boolean) => {
dispatch({ type: "toggleCollapsed", collapsed });
const onCollapse = () => {
dispatch({ type: "toggleCollapsed", collapsed: false });
};

const onExpand = () => {
dispatch({ type: "toggleCollapsed", collapsed: true });
};

return (
<div className={sharedStyles.PanelGroupWrapper}>
<PanelGroup className={styles.IDE} direction="horizontal" units="pixels">
<PanelGroup className={styles.IDE} direction="horizontal">
<div className={styles.Toolbar}>
<Icon className={styles.ToolbarIconActive} type="files" />
<Icon className={styles.ToolbarIcon} type="search" />
</div>
<Panel
className={sharedStyles.PanelColumn}
collapsedSize={36}
collapsedSizePixels={36}
collapsible={true}
defaultSize={150}
maxSize={150}
minSize={60}
onCollapse={toggleCollapsed}
defaultSizePixels={150}
maxSizePixels={150}
minSizePixels={60}
onCollapse={onCollapse}
onExpand={onExpand}
>
<div className={styles.FileList}>
<div className={styles.DirectoryEntry}>
Expand Down Expand Up @@ -103,7 +109,7 @@ function Content() {
: styles.ResizeHandle
}
/>
<Panel className={sharedStyles.PanelColumn} minSize={10}>
<Panel className={sharedStyles.PanelColumn} minSizePercentage={50}>
<div className={styles.SourceTabs}>
{Array.from(openFiles).map((file) => (
<div
Expand Down Expand Up @@ -169,7 +175,7 @@ const FILES: File[] = FILE_PATHS.map(([path, code]) => {
const CODE = `
<PanelGroup direction="horizontal">
<SideTabBar />
<Panel collapsedSize={5} collapsible={true} minSize={10}>
<Panel collapsible={true} collapsedSizePixels={35} minSizePercentage={10}>
<SourceBrowser />
</Panel>
<PanelResizeHandle />
Expand Down
Loading

1 comment on commit bb84b29

@vercel
Copy link

@vercel vercel bot commented on bb84b29 Nov 12, 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.