From bb84b29ad51cf4df7f77124ad317461241982806 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Nov 2023 01:05:37 +0900 Subject: [PATCH] Rewrite layout algorithm internals (#182) Support mix of percentage and pixel units within a group or panel. ```jsx ``` --- .github/workflows/e2e-ci.yml | 4 +- .github/workflows/eslint.yml | 2 +- .github/workflows/jest.yml | 17 + .github/workflows/prettier.yml | 2 +- .github/workflows/typescript.yml | 2 +- .npmrc | 1 + README.md | 29 +- package.json | 24 +- .../src/components/ResizeHandle.tsx | 2 +- .../src/routes/EndToEndTesting/index.tsx | 70 +- .../src/routes/examples/Collapsible.tsx | 26 +- .../src/routes/examples/Conditional.tsx | 23 +- .../routes/examples/ExternalPersistence.tsx | 16 +- .../src/routes/examples/Horizontal.tsx | 22 +- .../routes/examples/ImperativePanelApi.tsx | 32 +- .../examples/ImperativePanelGroupApi.tsx | 31 +- .../src/routes/examples/Nested.tsx | 28 +- .../src/routes/examples/Overflow.tsx | 14 +- .../src/routes/examples/Persistence.tsx | 8 +- .../src/routes/examples/PixelBasedLayouts.tsx | 45 +- .../src/routes/examples/Vertical.tsx | 18 +- .../src/routes/examples/types.ts | 14 +- .../src/utils/UrlData.ts | 73 +- .../tests/Collapsing.spec.ts | 88 +- .../tests/CursorStyle.spec.ts | 10 +- .../DevelopmentWarningsAndErrors.spec.ts | 422 --- .../tests/Group-OnLayout.spec.ts | 28 +- .../tests/ImperativePanelApi.spec.ts | 188 - .../tests/ImperativePanelGroupApi.spec.ts | 116 - .../tests/NestedGroups.spec.ts | 18 +- .../tests/Panel-OnCollapse.spec.ts | 58 +- .../tests/Panel-OnResize.spec.ts | 63 +- .../tests/PanelGroup-PixelUnits.spec.ts | 94 +- .../tests/ResizeHandle-OnDragging.spec.ts | 14 +- .../tests/ResizeHandle.spec.ts | 6 +- .../tests/Springy.spec.ts | 22 +- .../tests/Storage.spec.ts | 21 +- .../tests/WindowSplitter.spec.ts | 78 +- .../tests/utils/assert.ts | 3 +- .../tests/utils/debug.ts | 51 +- .../tests/utils/panels.ts | 29 +- .../tests/utils/verify.ts | 11 +- packages/react-resizable-panels/.eslintrc.cjs | 26 + .../react-resizable-panels/.eslintrc.json | 22 - packages/react-resizable-panels/CHANGELOG.md | 324 +- packages/react-resizable-panels/README.md | 104 +- .../react-resizable-panels/jest.config.js | 10 + packages/react-resizable-panels/package.json | 2 + .../react-resizable-panels/src/Panel.test.tsx | 308 ++ packages/react-resizable-panels/src/Panel.ts | 298 +- .../src/PanelContexts.ts | 22 - .../src/PanelGroup.test.tsx | 210 ++ .../react-resizable-panels/src/PanelGroup.ts | 1399 +++---- .../src/PanelGroupContext.ts | 33 + .../src/PanelResizeHandle.ts | 21 +- .../src/hooks/useUniqueId.ts | 2 +- .../src/hooks/useWindowSplitterBehavior.ts | 173 +- .../useWindowSplitterPanelGroupBehavior.ts | 185 + packages/react-resizable-panels/src/index.ts | 33 +- packages/react-resizable-panels/src/types.ts | 33 +- .../src/utils/adjustLayoutByDelta.test.ts | 1808 +++++++++ .../src/utils/adjustLayoutByDelta.ts | 211 ++ .../src/utils/calculateAriaValues.test.ts | 111 + .../src/utils/calculateAriaValues.ts | 67 + .../src/utils/calculateDeltaPercentage.ts | 68 + .../utils/calculateDragOffsetPercentage.ts | 30 + .../calculateUnsafeDefaultLayout.test.ts | 92 + .../src/utils/calculateUnsafeDefaultLayout.ts | 55 + .../src/utils/callPanelCallbacks.ts | 81 + .../src/utils/compareLayouts.test.ts | 9 + .../src/utils/compareLayouts.ts | 12 + .../src/utils/computePanelFlexBoxStyle.ts | 44 + .../computePercentagePanelConstraints.test.ts | 71 + .../computePercentagePanelConstraints.ts | 56 + .../utils/convertPercentageToPixels.test.ts | 9 + .../src/utils/convertPercentageToPixels.ts | 6 + .../convertPixelConstraintsToPercentages.ts | 55 + .../utils/convertPixelsToPercentage.test.ts | 9 + .../src/utils/convertPixelsToPercentage.ts | 6 + .../src/utils/coordinates.ts | 149 - .../src/utils/determinePivotIndices.ts | 10 + .../calculateAvailablePanelSizeInPixels.ts | 29 + .../utils/dom/getAvailableGroupSizePixels.ts | 29 + .../src/utils/dom/getPanelElement.ts | 7 + .../src/utils/dom/getPanelGroupElement.ts | 7 + .../src/utils/dom/getResizeHandleElement.ts | 9 + .../utils/dom/getResizeHandleElementIndex.ts | 12 + .../dom/getResizeHandleElementsForGroup.ts | 9 + .../src/utils/dom/getResizeHandlePanelIds.ts | 18 + .../src/utils/events.ts | 13 + .../getPercentageSizeFromMixedSizes.test.ts | 47 + .../utils/getPercentageSizeFromMixedSizes.ts | 15 + .../src/utils/getResizeEventCursorPosition.ts | 19 + .../react-resizable-panels/src/utils/group.ts | 614 ---- .../src/utils/initializeDefaultStorage.ts | 26 + .../utils/numbers/fuzzyCompareNumbers.test.ts | 16 + .../src/utils/numbers/fuzzyCompareNumbers.ts | 17 + .../src/utils/numbers/fuzzyNumbersEqual.ts | 9 + .../src/utils/resizePanel.ts | 41 + .../src/utils/serialization.ts | 13 +- ...shouldMonitorPixelBasedConstraints.test.ts | 23 + .../shouldMonitorPixelBasedConstraints.ts | 13 + .../src/utils/test-utils.ts | 136 + .../utils/validatePanelConstraints.test.ts | 151 + .../src/utils/validatePanelConstraints.ts | 103 + .../utils/validatePanelGroupLayout.test.ts | 233 ++ .../src/utils/validatePanelGroupLayout.ts | 88 + .../src/vendor/react.ts | 4 + pnpm-lock.yaml | 3218 +++++++++++++---- tsconfig.json | 4 +- 110 files changed, 9109 insertions(+), 3801 deletions(-) create mode 100644 .github/workflows/jest.yml create mode 100644 .npmrc delete mode 100644 packages/react-resizable-panels-website/tests/DevelopmentWarningsAndErrors.spec.ts delete mode 100644 packages/react-resizable-panels-website/tests/ImperativePanelApi.spec.ts delete mode 100644 packages/react-resizable-panels-website/tests/ImperativePanelGroupApi.spec.ts create mode 100644 packages/react-resizable-panels/.eslintrc.cjs delete mode 100644 packages/react-resizable-panels/.eslintrc.json create mode 100644 packages/react-resizable-panels/jest.config.js create mode 100644 packages/react-resizable-panels/src/Panel.test.tsx delete mode 100644 packages/react-resizable-panels/src/PanelContexts.ts create mode 100644 packages/react-resizable-panels/src/PanelGroup.test.tsx create mode 100644 packages/react-resizable-panels/src/PanelGroupContext.ts create mode 100644 packages/react-resizable-panels/src/hooks/useWindowSplitterPanelGroupBehavior.ts create mode 100644 packages/react-resizable-panels/src/utils/adjustLayoutByDelta.test.ts create mode 100644 packages/react-resizable-panels/src/utils/adjustLayoutByDelta.ts create mode 100644 packages/react-resizable-panels/src/utils/calculateAriaValues.test.ts create mode 100644 packages/react-resizable-panels/src/utils/calculateAriaValues.ts create mode 100644 packages/react-resizable-panels/src/utils/calculateDeltaPercentage.ts create mode 100644 packages/react-resizable-panels/src/utils/calculateDragOffsetPercentage.ts create mode 100644 packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.test.ts create mode 100644 packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.ts create mode 100644 packages/react-resizable-panels/src/utils/callPanelCallbacks.ts create mode 100644 packages/react-resizable-panels/src/utils/compareLayouts.test.ts create mode 100644 packages/react-resizable-panels/src/utils/compareLayouts.ts create mode 100644 packages/react-resizable-panels/src/utils/computePanelFlexBoxStyle.ts create mode 100644 packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.test.ts create mode 100644 packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.ts create mode 100644 packages/react-resizable-panels/src/utils/convertPercentageToPixels.test.ts create mode 100644 packages/react-resizable-panels/src/utils/convertPercentageToPixels.ts create mode 100644 packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.ts create mode 100644 packages/react-resizable-panels/src/utils/convertPixelsToPercentage.test.ts create mode 100644 packages/react-resizable-panels/src/utils/convertPixelsToPercentage.ts delete mode 100644 packages/react-resizable-panels/src/utils/coordinates.ts create mode 100644 packages/react-resizable-panels/src/utils/determinePivotIndices.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/calculateAvailablePanelSizeInPixels.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/getAvailableGroupSizePixels.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/getPanelElement.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/getPanelGroupElement.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/getResizeHandleElement.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/getResizeHandleElementIndex.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/getResizeHandleElementsForGroup.ts create mode 100644 packages/react-resizable-panels/src/utils/dom/getResizeHandlePanelIds.ts create mode 100644 packages/react-resizable-panels/src/utils/events.ts create mode 100644 packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.test.ts create mode 100644 packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.ts create mode 100644 packages/react-resizable-panels/src/utils/getResizeEventCursorPosition.ts delete mode 100644 packages/react-resizable-panels/src/utils/group.ts create mode 100644 packages/react-resizable-panels/src/utils/initializeDefaultStorage.ts create mode 100644 packages/react-resizable-panels/src/utils/numbers/fuzzyCompareNumbers.test.ts create mode 100644 packages/react-resizable-panels/src/utils/numbers/fuzzyCompareNumbers.ts create mode 100644 packages/react-resizable-panels/src/utils/numbers/fuzzyNumbersEqual.ts create mode 100644 packages/react-resizable-panels/src/utils/resizePanel.ts create mode 100644 packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.test.ts create mode 100644 packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.ts create mode 100644 packages/react-resizable-panels/src/utils/test-utils.ts create mode 100644 packages/react-resizable-panels/src/utils/validatePanelConstraints.test.ts create mode 100644 packages/react-resizable-panels/src/utils/validatePanelConstraints.ts create mode 100644 packages/react-resizable-panels/src/utils/validatePanelGroupLayout.test.ts create mode 100644 packages/react-resizable-panels/src/utils/validatePanelGroupLayout.ts diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index 25564fc52..84b5da897 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -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 \ No newline at end of file + run: cd packages/react-resizable-panels-website && pnpm test:e2e diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 1a7315fe7..599039d0d 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -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 diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml new file mode 100644 index 000000000..ee012b528 --- /dev/null +++ b/.github/workflows/jest.yml @@ -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 diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 60cf63ef7..edbd8f16e 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -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 diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index d5e962751..7899f765b 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -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 diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..b1c3f64c1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers=false \ No newline at end of file diff --git a/README.md b/README.md index 80da3140d..5fb392754 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ React Resizable Panels logo ## 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/)). @@ -26,13 +27,13 @@ The `Panel` API doesn't _require_ `id` and `order` props because they aren't nec {renderSideBar && ( <> - + )} - +
@@ -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"; @@ -60,15 +62,16 @@ 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)}`; @@ -76,16 +79,12 @@ export function ClientComponent({ return ( - - {/* ... */} - + {/* ... */} - - {/* ... */} - + {/* ... */} ); } ``` -A demo of this is available [here](https://github.com/bvaughn/react-resizable-panels-demo-ssr). \ No newline at end of file +A demo of this is available [here](https://github.com/bvaughn/react-resizable-panels-demo-ssr). diff --git a/package.json b/package.json index 92893207a..a6e475c8f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/react-resizable-panels-website/src/components/ResizeHandle.tsx b/packages/react-resizable-panels-website/src/components/ResizeHandle.tsx index be4786299..928ed0306 100644 --- a/packages/react-resizable-panels-website/src/components/ResizeHandle.tsx +++ b/packages/react-resizable-panels-website/src/components/ResizeHandle.tsx @@ -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, diff --git a/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx b/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx index 75015aaa0..f6c201131 100644 --- a/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx +++ b/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx @@ -9,8 +9,7 @@ import { import { ImperativePanelGroupHandle, ImperativePanelHandle, - Units, - getAvailableGroupSizePixels, + MixedSizes, } from "react-resizable-panels"; import { urlPanelGroupToPanelGroup, urlToUrlData } from "../../utils/UrlData"; @@ -55,8 +54,10 @@ function EndToEndTesting() { const [panelIds, setPanelIds] = useState([]); const [panelGroupId, setPanelGroupId] = useState(""); const [panelGroupIds, setPanelGroupIds] = useState([]); - const [size, setSize] = useState(0); - const [units, setUnits] = useState(""); + const [sizePercentage, setSizePercentage] = useState( + undefined + ); + const [sizePixels, setSizePixels] = useState(undefined); const [layoutString, setLayoutString] = useState(""); const debugLogRef = useRef(null); @@ -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) => { @@ -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); @@ -174,12 +170,16 @@ function EndToEndTesting() { const onSizeInputChange = (event: ChangeEvent) => { const value = event.currentTarget.value; - setSize(parseFloat(value)); - }; - const onUnitsSelectChange = (event: ChangeEvent) => { - 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 = () => { @@ -202,7 +202,7 @@ 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 }); } }; @@ -210,10 +210,20 @@ function EndToEndTesting() { 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[]; + panelGroup.setLayout(layout); } }; @@ -251,8 +261,8 @@ function EndToEndTesting() { className={styles.Input} id="sizeInput" onChange={onSizeInputChange} - placeholder="Size" - type="number" + placeholder="Size (% or px)" + type="text" />
- -