From bce573c9dac01e6589d2a3e0f6ea99676657ba43 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 13 Dec 2023 12:16:29 +0900 Subject: [PATCH] Version 1.0 release (#230) Checklist: - [x] Remove pixel size constraints - [x] Replaced `dataAttributes` prop with ...rest (to support all HTMLAttributes) - [x] Review and pull in changes from open PRs: - [x] #228 - [x] #224 - [x] Audit open issues and close ones that are no longer releveant - [x] Remove any unnecessary async initialization logic necessitated by pixel constraints I think this library's feature set is stable enough in my mind for a 1.0 release. It might be controversial but I have decided to cut pixel-based constraints from that release as they added too much complexity to the initialization and validation logic. --------- Co-authored-by: Timur Sufiev --- README.md | 12 +- .../react-resizable-panels-website/index.tsx | 5 - .../src/components/LogoAnimation.ts | 5 + .../src/components/useLogoAnimation.ts | 3 + .../src/routes/EndToEndTesting/index.tsx | 58 +- .../src/routes/Home/index.tsx | 1 - .../src/routes/examples/Collapsible.tsx | 27 +- .../src/routes/examples/Conditional.tsx | 21 +- .../routes/examples/ExternalPersistence.tsx | 14 +- .../src/routes/examples/Horizontal.tsx | 20 +- .../routes/examples/ImperativePanelApi.tsx | 44 +- .../examples/ImperativePanelGroupApi.tsx | 39 +- .../src/routes/examples/Nested.tsx | 26 +- .../src/routes/examples/Overflow.tsx | 12 +- .../src/routes/examples/Persistence.tsx | 6 +- .../examples/PixelBasedLayouts.module.css | 22 - .../src/routes/examples/PixelBasedLayouts.tsx | 163 ---- .../src/routes/examples/Vertical.tsx | 16 +- .../src/routes/examples/types.ts | 6 +- .../src/utils/UrlData.ts | 50 +- .../tests/Collapsing.spec.ts | 18 +- .../tests/CursorStyle.spec.ts | 8 +- .../tests/Group-OnLayout.spec.ts | 86 -- .../tests/NestedGroups.spec.ts | 14 +- .../tests/Panel-OnCollapse.spec.ts | 153 ---- .../tests/Panel-OnResize.spec.ts | 161 ---- .../tests/PanelGroup-PixelUnits.spec.ts | 370 --------- .../tests/ResizeHandle-OnDragging.spec.ts | 107 --- .../tests/ResizeHandle.spec.ts | 6 +- .../tests/Springy.spec.ts | 18 +- .../tests/Storage.spec.ts | 14 +- .../tests/WindowSplitter.spec.ts | 44 +- .../tests/utils/assert.ts | 18 +- .../tests/utils/panels.ts | 24 +- .../tests/utils/url.ts | 2 +- .../tests/utils/verify.ts | 44 +- packages/react-resizable-panels/.eslintrc.cjs | 1 + packages/react-resizable-panels/CHANGELOG.md | 5 + packages/react-resizable-panels/package.json | 2 +- .../react-resizable-panels/src/Panel.test.tsx | 370 +++++++-- packages/react-resizable-panels/src/Panel.ts | 112 +-- .../src/PanelGroup.test.tsx | 145 +++- .../react-resizable-panels/src/PanelGroup.ts | 771 ++++++------------ .../src/PanelGroupContext.ts | 5 +- .../src/PanelResizeHandle.test.tsx | 74 ++ .../src/PanelResizeHandle.ts | 53 +- .../src/hooks/useWindowSplitterBehavior.ts | 3 +- .../useWindowSplitterPanelGroupBehavior.ts | 55 +- packages/react-resizable-panels/src/index.ts | 7 +- packages/react-resizable-panels/src/types.ts | 9 - .../src/utils/adjustLayoutByDelta.test.ts | 542 +++++------- .../src/utils/adjustLayoutByDelta.ts | 110 +-- .../src/utils/assert.ts | 2 +- .../src/utils/calculateAriaValues.test.ts | 17 +- .../src/utils/calculateAriaValues.ts | 36 +- .../src/utils/calculateDeltaPercentage.ts | 23 +- .../utils/calculateDragOffsetPercentage.ts | 16 +- .../calculateUnsafeDefaultLayout.test.ts | 13 +- .../src/utils/calculateUnsafeDefaultLayout.ts | 31 +- .../src/utils/callPanelCallbacks.ts | 57 +- .../computePercentagePanelConstraints.test.ts | 98 --- .../computePercentagePanelConstraints.ts | 56 -- .../utils/convertPercentageToPixels.test.ts | 9 - .../src/utils/convertPercentageToPixels.ts | 6 - ...nvertPixelConstraintsToPercentages.test.ts | 47 -- .../convertPixelConstraintsToPercentages.ts | 72 -- .../utils/convertPixelsToPercentage.test.ts | 9 - .../src/utils/convertPixelsToPercentage.ts | 6 - .../getPercentageSizeFromMixedSizes.test.ts | 47 -- .../utils/getPercentageSizeFromMixedSizes.ts | 15 - .../src/utils/getResizeEventCursorPosition.ts | 2 + .../src/utils/resizePanel.test.ts | 58 +- .../src/utils/resizePanel.ts | 70 +- ...shouldMonitorPixelBasedConstraints.test.ts | 23 - .../shouldMonitorPixelBasedConstraints.ts | 13 - .../src/utils/test-utils.ts | 13 +- .../utils/validatePanelConstraints.test.ts | 77 +- .../src/utils/validatePanelConstraints.ts | 93 +-- .../utils/validatePanelGroupLayout.test.ts | 169 +--- .../src/utils/validatePanelGroupLayout.ts | 30 +- .../src/vendor/react.ts | 2 + tsconfig.json | 1 + 82 files changed, 1511 insertions(+), 3501 deletions(-) delete mode 100644 packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.module.css delete mode 100644 packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx delete mode 100644 packages/react-resizable-panels-website/tests/Group-OnLayout.spec.ts delete mode 100644 packages/react-resizable-panels-website/tests/Panel-OnCollapse.spec.ts delete mode 100644 packages/react-resizable-panels-website/tests/Panel-OnResize.spec.ts delete mode 100644 packages/react-resizable-panels-website/tests/PanelGroup-PixelUnits.spec.ts delete mode 100644 packages/react-resizable-panels-website/tests/ResizeHandle-OnDragging.spec.ts create mode 100644 packages/react-resizable-panels/src/PanelResizeHandle.test.tsx delete mode 100644 packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.test.ts delete mode 100644 packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.ts delete mode 100644 packages/react-resizable-panels/src/utils/convertPercentageToPixels.test.ts delete mode 100644 packages/react-resizable-panels/src/utils/convertPercentageToPixels.ts delete mode 100644 packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.test.ts delete mode 100644 packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.ts delete mode 100644 packages/react-resizable-panels/src/utils/convertPixelsToPercentage.test.ts delete mode 100644 packages/react-resizable-panels/src/utils/convertPixelsToPercentage.ts delete mode 100644 packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.test.ts delete mode 100644 packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.ts delete mode 100644 packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.test.ts delete mode 100644 packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.ts diff --git a/README.md b/README.md index 5fb392754..2b61d7985 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ Supported input methods include mouse, touch, and keyboard (via [Window Splitter ## FAQ +### Can panel sizes be specified in pixels? + +No. Pixel-based constraints [added significant complexity](https://github.com/bvaughn/react-resizable-panels/pull/176) to the initialization and validation logic and so I've decided not to support them. You may be able to implement a version of this yourself following [a pattern like this](https://github.com/bvaughn/react-resizable-panels/issues/46#issuecomment-1368108416) but it is not officially supported by this library. + ### How can I fix layout/sizing problems with conditionally rendered panels? The `Panel` API doesn't _require_ `id` and `order` props because they aren't necessary for static layouts. When panels are conditionally rendered though, it's best to supply these values. @@ -27,13 +31,13 @@ The `Panel` API doesn't _require_ `id` and `order` props because they aren't nec {renderSideBar && ( <> - + )} - +
@@ -79,9 +83,9 @@ export function ClientComponent({ return ( - {/* ... */} + {/* ... */} - {/* ... */} + {/* ... */} ); } diff --git a/packages/react-resizable-panels-website/index.tsx b/packages/react-resizable-panels-website/index.tsx index 3293eb100..62c8b247c 100644 --- a/packages/react-resizable-panels-website/index.tsx +++ b/packages/react-resizable-panels-website/index.tsx @@ -4,7 +4,6 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import HomeRoute from "./src/routes/Home"; import ConditionalExampleRoute from "./src/routes/examples/Conditional"; -import PixelBasedLayoutsRoute from "./src/routes/examples/PixelBasedLayouts"; import ExternalPersistenceExampleRoute from "./src/routes/examples/ExternalPersistence"; import HorizontalExampleRoute from "./src/routes/examples/Horizontal"; import ImperativePanelApiExampleRoute from "./src/routes/examples/ImperativePanelApi"; @@ -25,10 +24,6 @@ const router = createBrowserRouter([ path: "/examples/conditional", element: , }, - { - path: "/examples/pixel-based-layouts", - element: , - }, { path: "/examples/external-persistence", element: , diff --git a/packages/react-resizable-panels-website/src/components/LogoAnimation.ts b/packages/react-resizable-panels-website/src/components/LogoAnimation.ts index 781cc5305..50537220c 100644 --- a/packages/react-resizable-panels-website/src/components/LogoAnimation.ts +++ b/packages/react-resizable-panels-website/src/components/LogoAnimation.ts @@ -1,3 +1,5 @@ +import { assert } from "react-resizable-panels"; + export const Targets = { bottomLeft: "bottomLeft", bottomRight: "bottomRight", @@ -107,6 +109,8 @@ const stages = frames.filter((step): step is Stage => step.type === "stage"); for (let index = 0; index < frames.length; index++) { const frame = frames[index]; + assert(frame); + switch (frame.type) { case "pause": sequence.push(frame); @@ -116,6 +120,7 @@ for (let index = 0; index < frames.length; index++) { const index = stages.indexOf(fromStage); const toStage = index + 1 < stages.length ? stages[index + 1] : stages[0]; + assert(toStage != null); const changedProperties: AnimatedProperty[] = []; diff --git a/packages/react-resizable-panels-website/src/components/useLogoAnimation.ts b/packages/react-resizable-panels-website/src/components/useLogoAnimation.ts index 501daf035..bacc8f6e9 100644 --- a/packages/react-resizable-panels-website/src/components/useLogoAnimation.ts +++ b/packages/react-resizable-panels-website/src/components/useLogoAnimation.ts @@ -1,4 +1,5 @@ import { RefObject, useEffect } from "react"; +import { assert } from "react-resizable-panels"; import { Sequence, Target } from "./LogoAnimation"; export function useLogoAnimation( @@ -31,6 +32,8 @@ export function useLogoAnimation( let accumulatedDuration = 0; for (let index = 0; index < sequence.length; index++) { segment = sequence[index]; + assert(segment); + if ( elapsed >= accumulatedDuration && elapsed <= accumulatedDuration + segment.duration 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 f6c201131..f602311c5 100644 --- a/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx +++ b/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx @@ -3,25 +3,22 @@ import { Component, ErrorInfo, PropsWithChildren, + useLayoutEffect, useRef, useState, } from "react"; import { ImperativePanelGroupHandle, ImperativePanelHandle, - MixedSizes, + assert, } from "react-resizable-panels"; - -import { urlPanelGroupToPanelGroup, urlToUrlData } from "../../utils/UrlData"; - -import DebugLog, { ImperativeDebugLogHandle } from "../examples/DebugLog"; - -import { useLayoutEffect } from "react"; import { assertImperativePanelGroupHandle, assertImperativePanelHandle, } from "../../../tests/utils/assert"; import Icon from "../../components/Icon"; +import { urlPanelGroupToPanelGroup, urlToUrlData } from "../../utils/UrlData"; +import DebugLog, { ImperativeDebugLogHandle } from "../examples/DebugLog"; import "./styles.css"; import styles from "./styles.module.css"; @@ -54,10 +51,7 @@ function EndToEndTesting() { const [panelIds, setPanelIds] = useState([]); const [panelGroupId, setPanelGroupId] = useState(""); const [panelGroupIds, setPanelGroupIds] = useState([]); - const [sizePercentage, setSizePercentage] = useState( - undefined - ); - const [sizePixels, setSizePixels] = useState(undefined); + const [size, setSize] = useState(0); const [layoutString, setLayoutString] = useState(""); const debugLogRef = useRef(null); @@ -71,16 +65,24 @@ function EndToEndTesting() { const panelIds = Array.from(panelElements).map( (element) => element.getAttribute("data-panel-id")! ); + + const firstPanelId = panelIds[0]; + assert(firstPanelId != null); + setPanelIds(panelIds); - setPanelId(panelIds[0]); + setPanelId(firstPanelId); const panelGroupElements = document.querySelectorAll("[data-panel-group]"); const panelGroupIds = Array.from(panelGroupElements).map( (element) => element.getAttribute("data-panel-group-id")! ); + + const firstPanelGroupId = panelGroupIds[0]; + assert(firstPanelGroupId != null); + setPanelGroupIds(panelGroupIds); - setPanelGroupId(panelGroupIds[0]); + setPanelGroupId(firstPanelGroupId); }; window.addEventListener("popstate", (event) => { @@ -110,11 +112,9 @@ function EndToEndTesting() { panelId ) as ImperativePanelHandle; if (panel != null) { - const { sizePercentage, sizePixels } = panel.getSize(); + const size = panel.getSize(); - panelElement.textContent = `${sizePercentage.toFixed( - 1 - )}%\n${sizePixels.toFixed(1)}px`; + panelElement.textContent = `${size.toFixed(1)}%`; } } }, 0); @@ -171,15 +171,7 @@ function EndToEndTesting() { const onSizeInputChange = (event: ChangeEvent) => { const value = event.currentTarget.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}`); - } + setSize(parseFloat(value)); }; const onCollapseButtonClick = () => { @@ -202,7 +194,7 @@ function EndToEndTesting() { const idToRefMap = idToRefMapRef.current; const panel = idToRefMap.get(panelId); if (panel && assertImperativePanelHandle(panel)) { - panel.resize({ sizePercentage, sizePixels }); + panel.resize(size); } }; @@ -214,15 +206,9 @@ function EndToEndTesting() { 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[]; + const layout = trimmedLayoutString + .split(",") + .map((text) => parseFloat(text)); panelGroup.setLayout(layout); } }; diff --git a/packages/react-resizable-panels-website/src/routes/Home/index.tsx b/packages/react-resizable-panels-website/src/routes/Home/index.tsx index 7e39aaaa9..79fddbdf7 100644 --- a/packages/react-resizable-panels-website/src/routes/Home/index.tsx +++ b/packages/react-resizable-panels-website/src/routes/Home/index.tsx @@ -13,7 +13,6 @@ const LINKS = [ { path: "overflow", title: "Overflow content" }, { path: "collapsible", title: "Collapsible panels" }, { path: "conditional", title: "Conditional panels" }, - { path: "pixel-based-layouts", title: "Pixel based layouts" }, { path: "external-persistence", title: "External persistence" }, { path: "imperative-panel-api", title: "Imperative Panel API" }, { path: "imperative-panel-group-api", title: "Imperative PanelGroup API" }, diff --git a/packages/react-resizable-panels-website/src/routes/examples/Collapsible.tsx b/packages/react-resizable-panels-website/src/routes/examples/Collapsible.tsx index c88a23884..8dabaaced 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/Collapsible.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/Collapsible.tsx @@ -1,7 +1,10 @@ import { useReducer } from "react"; - -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; - +import { + Panel, + PanelGroup, + PanelResizeHandle, + assert, +} from "react-resizable-panels"; import { TUTORIAL_CODE_CSS, TUTORIAL_CODE_HTML, @@ -11,7 +14,6 @@ import { import Code from "../../components/Code"; import Icon from "../../components/Icon"; import { Language } from "../../suspense/SyntaxParsingCache"; - import styles from "./Collapsible.module.css"; import Example from "./Example"; import sharedStyles from "./shared.module.css"; @@ -74,11 +76,11 @@ function Content() { @@ -109,7 +111,7 @@ function Content() { : styles.ResizeHandle } /> - +
{Array.from(openFiles).map((file) => (
{ const CODE = ` - + @@ -197,10 +199,13 @@ type FilesState = { openFiles: File[]; }; +const FIRST_FILE = FILES[0]; +assert(FIRST_FILE); + const initialState: FilesState = { currentFileIndex: 0, fileListCollapsed: false, - openFiles: [FILES[0]], + openFiles: [FIRST_FILE], }; function reducer(state: FilesState, action: FilesAction): FilesState { diff --git a/packages/react-resizable-panels-website/src/routes/examples/Conditional.tsx b/packages/react-resizable-panels-website/src/routes/examples/Conditional.tsx index d614cfe97..07fc31912 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/Conditional.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/Conditional.tsx @@ -66,34 +66,19 @@ function Content({ > {showLeftPanel && ( <> - +
left
)} - +
middle
{showRightPanel && ( <> - +
right
diff --git a/packages/react-resizable-panels-website/src/routes/examples/ExternalPersistence.tsx b/packages/react-resizable-panels-website/src/routes/examples/ExternalPersistence.tsx index 1dd91b33f..7019bee7a 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/ExternalPersistence.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/ExternalPersistence.tsx @@ -81,23 +81,15 @@ function Content() { direction="horizontal" storage={urlStorage} > - +
left
- +
middle
- +
right
diff --git a/packages/react-resizable-panels-website/src/routes/examples/Horizontal.tsx b/packages/react-resizable-panels-website/src/routes/examples/Horizontal.tsx index 4a3d8019f..0f47dd042 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/Horizontal.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/Horizontal.tsx @@ -33,23 +33,15 @@ function Content() { return (
- +
left
- +
middle
- +
right
@@ -59,15 +51,15 @@ function Content() { const CODE = ` - + left - + middle - + right 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 bde438953..f35c2b162 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelApi.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelApi.tsx @@ -8,10 +8,10 @@ import Icon from "../../components/Icon"; import { ResizeHandle } from "../../components/ResizeHandle"; +import Code from "../../components/Code"; import Example from "./Example"; import styles from "./ImperativePanelApi.module.css"; import sharedStyles from "./shared.module.css"; -import Code from "../../components/Code"; type Sizes = { left: number; @@ -88,7 +88,7 @@ export default function ImperativePanelApiRoute() {
  • Panel's current size in (in both percentage and pixel units) @@ -112,21 +112,13 @@ export default function ImperativePanelApiRoute() {
  • ): void`} + code={`resize(size: number): void`} language="typescript" /> Resize the panel to the specified size (either percentage or pixel units)
  • -

    - Note that the MixedSizes type above is defined as{" "} - -

    } language="tsx" @@ -144,17 +136,17 @@ function TogglesRow({ panelRef: RefObject; panelSize: number; }) { - const [sizePercentage, setSizePercentage] = useState(20); + const [size, setSize] = useState(20); const onInputChange = (event: ChangeEvent) => { const input = event.currentTarget as HTMLInputElement; - setSizePercentage(parseInt(input.value)); + setSize(parseInt(input.value)); }; const onFormSubmit = (event: FormEvent) => { event.preventDefault(); - panelRef.current?.resize({ sizePercentage }); + panelRef.current?.resize(size); }; return ( @@ -188,7 +180,7 @@ function TogglesRow({ max={100} size={2} type="number" - value={sizePercentage} + value={size} />
    @@ -236,11 +228,11 @@ function Content({ onResize({ left })} + maxSize={30} + minSize={10} + onResize={(left) => onResize({ left })} order={1} ref={leftPanelRef} > @@ -253,9 +245,9 @@ function Content({ className={sharedStyles.PanelRow} collapsible={true} id="middle" - maxSizePercentage={100} - minSizePercentage={10} - onResize={({ sizePercentage: middle }) => onResize({ middle })} + maxSize={100} + minSize={10} + onResize={(middle) => onResize({ middle })} order={2} ref={middlePanelRef} > @@ -267,11 +259,11 @@ function Content({ onResize({ right })} + maxSize={100} + minSize={10} + onResize={(right) => onResize({ right })} order={3} ref={rightPanelRef} > diff --git a/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelGroupApi.tsx b/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelGroupApi.tsx index 0f06fbc34..5f796ee65 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelGroupApi.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/ImperativePanelGroupApi.tsx @@ -1,9 +1,6 @@ import { useRef, useState } from "react"; -import type { - ImperativePanelGroupHandle, - MixedSizes, -} from "react-resizable-panels"; -import { Panel, PanelGroup } from "react-resizable-panels"; +import type { ImperativePanelGroupHandle } from "react-resizable-panels"; +import { Panel, PanelGroup, assert } from "react-resizable-panels"; import { ResizeHandle } from "../../components/ResizeHandle"; @@ -35,7 +32,7 @@ export default function ImperativePanelGroupApiRoute() {
  • Current size of panels (in both percentage and pixel units) @@ -43,20 +40,12 @@ export default function ImperativePanelGroupApiRoute() {
  • []): void`} + code={`setLayout(number[]): void`} language="typescript" /> Resize all panels (using either percentage or pixel units)
  • -

    - Note that the MixedSizes type above is defined as{" "} - -

    } language="tsx" @@ -70,17 +59,23 @@ function Content() { const panelGroupRef = useRef(null); - const onLayout = (mixedSizes: MixedSizes[]) => { - setSizes(mixedSizes.map((mixedSize) => mixedSize.sizePercentage)); + const onLayout = (sizes: number[]) => { + setSizes(sizes); }; const resetLayout = () => { const panelGroup = panelGroupRef.current; if (panelGroup) { - panelGroup.setLayout([{ sizePercentage: 50 }, { sizePercentage: 50 }]); + panelGroup.setLayout([50, 50]); } }; + const left = sizes[0]; + const right = sizes[1]; + + assert(left != null); + assert(right != null); + return ( <>
    @@ -96,15 +91,15 @@ function Content() { onLayout={onLayout} ref={panelGroupRef} > - +
    - left: {Math.round(sizes[0])} + left: {Math.round(left)}
    - +
    - right: {Math.round(sizes[1])} + right: {Math.round(right)}
    diff --git a/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx b/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx index a15f8d330..2358a55cb 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx @@ -20,31 +20,23 @@ function Content() { return (
    - +
    left
    - + - +
    top
    - + - +
    left
    - +
    right
    @@ -52,11 +44,7 @@ function Content() {
    - +
    right
    diff --git a/packages/react-resizable-panels-website/src/routes/examples/Overflow.tsx b/packages/react-resizable-panels-website/src/routes/examples/Overflow.tsx index bf1e9b2a5..ae57d4f31 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/Overflow.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/Overflow.tsx @@ -54,11 +54,7 @@ function Content() { className={styles.PanelGroup} direction={useVerticalLayout ? "vertical" : "horizontal"} > - +
    - +
    - +
    left
    - +
    middle
    - +
    right
    diff --git a/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.module.css b/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.module.css deleted file mode 100644 index 8c98da04f..000000000 --- a/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.AutoSizerWrapper { - flex: 1 1 auto; - background-color: var(--color-panel-background); - border-radius: 0.5rem; - overflow: hidden; - display: flex; -} - -.AutoSizerInner { - height: 100%; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: 1rem; -} - -.Small { - font-size: 0.8rem; - color: var(--color-dim); -} diff --git a/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx b/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx deleted file mode 100644 index 0f6b7e9af..000000000 --- a/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Panel, PanelGroup } from "react-resizable-panels"; - -import { ResizeHandle } from "../../components/ResizeHandle"; - -import { Link } from "react-router-dom"; -import AutoSizer from "react-virtualized-auto-sizer"; -import exampleStyles from "./Example.module.css"; -import styles from "./PixelBasedLayouts.module.css"; -import sharedStyles from "./shared.module.css"; - -import { PropsWithChildren } from "react"; -import Code from "../../components/Code"; -import Icon from "../../components/Icon"; - -export default function PixelBasedLayouts() { - return ( -
    -

    - - Home - - →Pixel based layouts -

    -

    - Resizable panels can specific either percentage-based{" "} - or pixel-based layout constraints, although - percentage-based constraints are generally recommended for performance - purposes. The example below shows a horizontal panel group where the - first panel is limited to a range of 100-200 pixels. -

    -

    - - Pixel units should only be used when necessary because they require the - use of a ResizerObserver. -

    -
    -
    - - -
    - -

    100px - 200px

    -
    -
    -
    - - -
    middle
    -
    - - -
    right
    -
    -
    -
    -
    - -
    -

    - Panels with pixel constraints can also be configured to collapse as - shown below. -

    -
    -
    -
    - - -
    left
    -
    - - -
    middle
    -
    - - -
    - -

    200px - 300px

    -

    collapse below 200px

    -
    -
    -
    -
    -
    -
    - -
    - ); -} - -function Size({ - children, - direction, -}: PropsWithChildren & { - direction: "horizontal" | "vertical"; -}) { - return ( - - {({ height, width }) => ( -
    -
    -

    - {direction === "horizontal" - ? Math.round(width!) - : Math.round(height!)} - px -

    - {children} -
    -
    - )} -
    - ); -} - -const CODE_HOOK = ` - - - - - - - -`; - -const CODE_HOOK_COLLAPSIBLE = ` - - - - - - - -`; diff --git a/packages/react-resizable-panels-website/src/routes/examples/Vertical.tsx b/packages/react-resizable-panels-website/src/routes/examples/Vertical.tsx index aeb542887..1b2df4c39 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/Vertical.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/Vertical.tsx @@ -35,18 +35,14 @@ function Content() {
    top
    - +
    bottom
    @@ -56,11 +52,11 @@ function Content() { const CODE = ` - + top - + bottom diff --git a/packages/react-resizable-panels-website/src/routes/examples/types.ts b/packages/react-resizable-panels-website/src/routes/examples/types.ts index 3de2d2dde..d04b369ed 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/types.ts +++ b/packages/react-resizable-panels-website/src/routes/examples/types.ts @@ -1,5 +1,3 @@ -import { MixedSizes } from "react-resizable-panels"; - export type PanelCollapseLogEntryType = "onCollapse"; export type PanelExpandLogEntryType = "onExpand"; export type PanelGroupLayoutLogEntryType = "onLayout"; @@ -21,12 +19,12 @@ export type PanelResizeHandleDraggingLogEntry = { }; export type PanelGroupLayoutLogEntry = { groupId: string; - layout: MixedSizes[]; + layout: number[]; type: PanelGroupLayoutLogEntryType; }; export type PanelResizeLogEntry = { panelId: string; - size: MixedSizes; + size: number; type: PanelResizeLogEntryType; }; diff --git a/packages/react-resizable-panels-website/src/utils/UrlData.ts b/packages/react-resizable-panels-website/src/utils/UrlData.ts index 0b4b29aec..c919261b1 100644 --- a/packages/react-resizable-panels-website/src/utils/UrlData.ts +++ b/packages/react-resizable-panels-website/src/utils/UrlData.ts @@ -9,7 +9,6 @@ import { import { ImperativePanelGroupHandle, ImperativePanelHandle, - MixedSizes, Panel, PanelGroup, PanelGroupOnLayout, @@ -26,16 +25,12 @@ import { ImperativeDebugLogHandle } from "../routes/examples/DebugLog"; type UrlPanel = { children: Array; - collapsedSizePercentage?: number; - collapsedSizePixels?: number; + collapsedSize?: number; collapsible?: boolean; - defaultSizePercentage?: number | null; - defaultSizePixels?: number | null; + defaultSize?: number | null; id?: string | null; - maxSizePercentage?: number | null; - maxSizePixels?: number | null; - minSizePercentage?: number; - minSizePixels?: number; + maxSize?: number | null; + minSize?: number; order?: number | null; style?: CSSProperties; type: "UrlPanel"; @@ -46,8 +41,7 @@ type UrlPanelGroup = { children: Array; direction: "horizontal" | "vertical"; id?: string | null; - keyboardResizeByPercentage?: number | null; - keyboardResizeByPixels?: number | null; + keyboardResizeBy?: number | null; style?: CSSProperties; type: "UrlPanelGroup"; }; @@ -109,16 +103,12 @@ function UrlPanelToData(urlPanel: ReactElement): UrlPanel { return child; } }), - collapsedSizePercentage: urlPanel.props.collapsedSizePercentage, - collapsedSizePixels: urlPanel.props.collapsedSizePixels, + collapsedSize: urlPanel.props.collapsedSize, collapsible: urlPanel.props.collapsible, - defaultSizePercentage: urlPanel.props.defaultSizePercentage, - defaultSizePixels: urlPanel.props.defaultSizePixels, + defaultSize: urlPanel.props.defaultSize, id: urlPanel.props.id, - maxSizePercentage: urlPanel.props.maxSizePercentage, - maxSizePixels: urlPanel.props.maxSizePixels, - minSizePercentage: urlPanel.props.minSizePercentage, - minSizePixels: urlPanel.props.minSizePixels, + maxSize: urlPanel.props.maxSize, + minSize: urlPanel.props.minSize, order: urlPanel.props.order, style: urlPanel.props.style, type: "UrlPanel", @@ -141,8 +131,7 @@ function UrlPanelGroupToData( }), direction: urlPanelGroup.props.direction, id: urlPanelGroup.props.id, - keyboardResizeByPercentage: urlPanelGroup.props.keyboardResizeByPercentage, - keyboardResizeByPixels: urlPanelGroup.props.keyboardResizeByPixels, + keyboardResizeBy: urlPanelGroup.props.keyboardResizeBy, style: urlPanelGroup.props.style, type: "UrlPanelGroup", }; @@ -193,7 +182,7 @@ function urlPanelToPanel( } }; - onResize = (size: MixedSizes) => { + onResize = (size: number) => { const debugLog = debugLogRef.current; if (debugLog) { debugLog.log({ @@ -217,17 +206,13 @@ function urlPanelToPanel( Panel, { className: "Panel", - collapsedSizePercentage: urlPanel.collapsedSizePercentage, - collapsedSizePixels: urlPanel.collapsedSizePixels, + collapsedSize: urlPanel.collapsedSize, collapsible: urlPanel.collapsible, - defaultSizePercentage: urlPanel.defaultSizePercentage ?? undefined, - defaultSizePixels: urlPanel.defaultSizePixels ?? undefined, + defaultSize: urlPanel.defaultSize ?? undefined, id: urlPanel.id ?? undefined, key, - maxSizePercentage: urlPanel.maxSizePercentage ?? undefined, - maxSizePixels: urlPanel.maxSizePixels ?? undefined, - minSizePercentage: urlPanel.minSizePercentage, - minSizePixels: urlPanel.minSizePixels, + maxSize: urlPanel.maxSize ?? undefined, + minSize: urlPanel.minSize, onCollapse, onExpand, onResize, @@ -262,7 +247,7 @@ export function urlPanelGroupToPanelGroup( const groupId = urlPanelGroup.id; if (groupId) { - onLayout = (layout: MixedSizes[]) => { + onLayout = (layout: number[]) => { const debugLog = debugLogRef.current; if (debugLog) { debugLog.log({ groupId, type: "onLayout", layout }); @@ -286,8 +271,7 @@ export function urlPanelGroupToPanelGroup( direction: urlPanelGroup.direction, id: urlPanelGroup.id, key: key, - keyboardResizeByPercentage: urlPanelGroup.keyboardResizeByPercentage, - keyboardResizeByPixels: urlPanelGroup.keyboardResizeByPixels, + keyboardResizeBy: urlPanelGroup.keyboardResizeBy, onLayout, ref: refSetter, style: urlPanelGroup.style, diff --git a/packages/react-resizable-panels-website/tests/Collapsing.spec.ts b/packages/react-resizable-panels-website/tests/Collapsing.spec.ts index f1a4680b0..9be115144 100644 --- a/packages/react-resizable-panels-website/tests/Collapsing.spec.ts +++ b/packages/react-resizable-panels-website/tests/Collapsing.spec.ts @@ -14,18 +14,18 @@ test.describe("collapsible prop", () => { { direction: "horizontal" }, createElement(Panel, { collapsible: true, - defaultSizePercentage: 35, - minSizePercentage: 10, + defaultSize: 35, + minSize: 10, }), createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }), createElement(Panel, { - minSizePercentage: 10, + minSize: 10, }), createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }), createElement(Panel, { collapsible: true, - defaultSizePercentage: 35, - minSizePercentage: 20, + defaultSize: 35, + minSize: 20, }) ) ); @@ -81,13 +81,13 @@ test.describe("collapsible prop", () => { PanelGroup, { direction: "horizontal" }, createElement(Panel, { - collapsedSizePercentage: 2, + collapsedSize: 2, collapsible: true, - defaultSizePercentage: 35, - minSizePercentage: 10, + defaultSize: 35, + minSize: 10, }), createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ); diff --git a/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts b/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts index 35c652579..89a97de8f 100644 --- a/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts +++ b/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts @@ -39,15 +39,15 @@ test.describe("cursor style", () => { PanelGroup, { direction }, createElement(Panel, { - defaultSizePercentage: 50, + defaultSize: 50, id: "first-panel", - minSizePercentage: 10, + minSize: 10, }), createElement(PanelResizeHandle), createElement(Panel, { - defaultSizePercentage: 50, + defaultSize: 50, id: "last-panel", - minSizePercentage: 10, + minSize: 10, }) ) ); diff --git a/packages/react-resizable-panels-website/tests/Group-OnLayout.spec.ts b/packages/react-resizable-panels-website/tests/Group-OnLayout.spec.ts deleted file mode 100644 index b0edf7bbd..000000000 --- a/packages/react-resizable-panels-website/tests/Group-OnLayout.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { expect, Page, test } from "@playwright/test"; -import { createElement } from "react"; -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; - -import { PanelGroupLayoutLogEntry } from "../src/routes/examples/types"; - -import { clearLogEntries, getLogEntries } from "./utils/debug"; -import { goToUrl } from "./utils/url"; - -async function openPage(page: Page) { - const panelGroup = createElement( - PanelGroup, - { direction: "horizontal", id: "group" }, - createElement(Panel, { - collapsible: true, - defaultSizePercentage: 20, - minSizePercentage: 10, - order: 1, - }), - createElement(PanelResizeHandle, { id: "left-handle" }), - createElement(Panel, { - defaultSizePercentage: 60, - minSizePercentage: 10, - order: 2, - }), - createElement(PanelResizeHandle, { id: "right-handle" }), - createElement(Panel, { - collapsible: true, - defaultSizePercentage: 20, - minSizePercentage: 10, - order: 3, - }) - ); - - await goToUrl(page, panelGroup); -} - -async function verifyEntries(page: Page, expectedPercentages: number[][]) { - const logEntries = await getLogEntries( - page, - "onLayout" - ); - - expect(logEntries.length).toEqual(expectedPercentages.length); - - for (let index = 0; index < expectedPercentages.length; index++) { - const actual = logEntries[index].layout.map( - ({ sizePercentage }) => sizePercentage - ); - const expected = expectedPercentages[index]; - expect(actual).toEqual(expected); - } -} - -test.describe("PanelGroup onLayout prop", () => { - test.beforeEach(async ({ page }) => { - await openPage(page); - }); - - test("should be called once on-mount", async ({ page }) => { - await verifyEntries(page, [[20, 60, 20]]); - }); - - test("should be called when the panel group is resized", async ({ page }) => { - const leftHandle = page.locator( - '[data-panel-resize-handle-id="left-handle"]' - ); - const rightHandle = page.locator( - '[data-panel-resize-handle-id="right-handle"]' - ); - - await clearLogEntries(page, "onLayout"); - - await leftHandle.focus(); - await page.keyboard.press("Home"); - await rightHandle.focus(); - await page.keyboard.press("End"); - await page.keyboard.press("ArrowLeft"); - - await verifyEntries(page, [ - [0, 80, 20], - [0, 100, 0], - [0, 90, 10], - ]); - }); -}); diff --git a/packages/react-resizable-panels-website/tests/NestedGroups.spec.ts b/packages/react-resizable-panels-website/tests/NestedGroups.spec.ts index ae2b6933e..ce1557b86 100644 --- a/packages/react-resizable-panels-website/tests/NestedGroups.spec.ts +++ b/packages/react-resizable-panels-website/tests/NestedGroups.spec.ts @@ -12,31 +12,31 @@ test.describe("Nested groups", () => { createElement( PanelGroup, { direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle), createElement( Panel, - { minSizePercentage: 10 }, + { minSize: 10 }, createElement( PanelGroup, { direction: "vertical" }, - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle), createElement( Panel, - { minSizePercentage: 10 }, + { minSize: 10 }, createElement( PanelGroup, { direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ) ) ), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ); diff --git a/packages/react-resizable-panels-website/tests/Panel-OnCollapse.spec.ts b/packages/react-resizable-panels-website/tests/Panel-OnCollapse.spec.ts deleted file mode 100644 index f12819e12..000000000 --- a/packages/react-resizable-panels-website/tests/Panel-OnCollapse.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { expect, Page, test } from "@playwright/test"; -import { createElement } from "react"; -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; - -import { - PanelCollapseLogEntry, - PanelExpandLogEntry, -} from "../src/routes/examples/types"; - -import { clearLogEntries, getLogEntries } from "./utils/debug"; -import { imperativeResizePanelGroup } from "./utils/panels"; -import { goToUrl } from "./utils/url"; - -async function openPage( - page: Page, - options: { - collapsedByDefault?: boolean; - middleCollapsible?: boolean; - } = {} -) { - const { collapsedByDefault = false, middleCollapsible = true } = options; - - const panelGroup = createElement( - PanelGroup, - { direction: "horizontal", id: "group" }, - createElement(Panel, { - collapsible: true, - defaultSizePercentage: collapsedByDefault ? 0 : 20, - id: "left", - minSizePercentage: 10, - order: 1, - }), - createElement(PanelResizeHandle, { id: "left-handle" }), - createElement(Panel, { - collapsible: middleCollapsible, - id: "middle", - minSizePercentage: 10, - order: 2, - }), - createElement(PanelResizeHandle, { id: "right-handle" }), - createElement(Panel, { - collapsible: true, - defaultSizePercentage: collapsedByDefault ? 0 : 20, - id: "right", - minSizePercentage: 10, - order: 3, - }) - ); - - await goToUrl(page, panelGroup); -} - -async function verifyEntries( - page: Page, - expected: Array<{ panelId: string; collapsed: boolean }> -) { - const logEntries = await getLogEntries< - PanelCollapseLogEntry | PanelExpandLogEntry - >(page, ["onCollapse", "onExpand"]); - - try { - expect(logEntries.length).toEqual(expected.length); - } catch (error) { - console.error(`Expected ${expected.length} entries, got:\n`, logEntries); - throw error; - } - - for (let index = 0; index < expected.length; index++) { - const { panelId: actualPanelId, type } = logEntries[index]; - const { panelId: expectedPanelId, collapsed: expectedPanelCollapsed } = - expected[index]; - - const actualPanelCollapsed = type === "onCollapse"; - - expect(actualPanelId).toEqual(expectedPanelId); - expect(actualPanelCollapsed).toEqual(expectedPanelCollapsed); - } -} - -test.describe("Panel onCollapse prop", () => { - test.beforeEach(async ({ page }) => { - await openPage(page); - }); - - test("should be called once on-mount", async ({ page }) => { - // No panels are collapsed by default. - await verifyEntries(page, [ - { panelId: "left", collapsed: false }, - { panelId: "middle", collapsed: false }, - { panelId: "right", collapsed: false }, - ]); - - // If we override via URL parameters, left and right panels should be collapsed by default. - await openPage(page, { collapsedByDefault: true }); - await verifyEntries(page, [ - { panelId: "left", collapsed: true }, - { panelId: "middle", collapsed: false }, - { panelId: "right", collapsed: true }, - ]); - }); - - test("should be called when panels are resized", async ({ page }) => { - const leftHandle = page.locator( - '[data-panel-resize-handle-id="left-handle"]' - ); - const rightHandle = page.locator( - '[data-panel-resize-handle-id="right-handle"]' - ); - - await clearLogEntries(page); - - // Resizing should not trigger onCollapse unless the panel's collapsed state changes. - await leftHandle.focus(); - await page.keyboard.press("ArrowLeft"); - await verifyEntries(page, []); - - await page.keyboard.press("Home"); - await verifyEntries(page, [{ panelId: "left", collapsed: true }]); - - await clearLogEntries(page); - - await rightHandle.focus(); - await page.keyboard.press("End"); - await verifyEntries(page, [{ panelId: "right", collapsed: true }]); - - await clearLogEntries(page); - - // Resizing should not trigger onCollapse unless the panel's collapsed state changes. - await page.keyboard.press("ArrowRight"); - await verifyEntries(page, []); - - await page.keyboard.press("ArrowLeft"); - await verifyEntries(page, [{ panelId: "right", collapsed: false }]); - }); - - test("should be called when triggering PanelGroup setLayout method", async ({ - page, - }) => { - await clearLogEntries(page); - - await imperativeResizePanelGroup(page, "group", ["70%", "30%", "0%"]); - await verifyEntries(page, [{ panelId: "right", collapsed: true }]); - - await clearLogEntries(page); - - await imperativeResizePanelGroup(page, "group", ["0%", "0%", "100%"]); - await verifyEntries(page, [ - { panelId: "left", collapsed: true }, - { panelId: "middle", collapsed: true }, - { panelId: "right", collapsed: false }, - ]); - }); -}); diff --git a/packages/react-resizable-panels-website/tests/Panel-OnResize.spec.ts b/packages/react-resizable-panels-website/tests/Panel-OnResize.spec.ts deleted file mode 100644 index 13d7814e0..000000000 --- a/packages/react-resizable-panels-website/tests/Panel-OnResize.spec.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { expect, Page, test } from "@playwright/test"; -import { createElement } from "react"; -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; - -import { PanelResizeLogEntry } from "../src/routes/examples/types"; - -import { clearLogEntries, getLogEntries } from "./utils/debug"; -import { goToUrl, updateUrl } from "./utils/url"; -import { imperativeResizePanelGroup } from "./utils/panels"; - -function createElements(numPanels: 2 | 3) { - const panels = [ - createElement(Panel, { - collapsible: true, - defaultSizePercentage: numPanels === 3 ? 20 : 40, - id: "left", - minSizePercentage: 10, - order: 1, - }), - createElement(PanelResizeHandle, { id: "left-handle" }), - createElement(Panel, { - defaultSizePercentage: 60, - id: "middle", - minSizePercentage: 10, - order: 2, - }), - ]; - - if (numPanels === 3) { - panels.push( - createElement(PanelResizeHandle, { id: "right-handle" }), - createElement(Panel, { - collapsible: true, - defaultSizePercentage: 20, - id: "right", - minSizePercentage: 10, - order: 3, - }) - ); - } - - return createElement( - PanelGroup, - { direction: "horizontal", id: "group" }, - ...panels - ); -} - -async function openPage(page: Page) { - const panelGroup = createElements(3); - - await goToUrl(page, panelGroup); -} - -async function verifyEntries( - page: Page, - expected: Array<{ panelId: string; sizePercentage: number }> -) { - const logEntries = await getLogEntries(page, "onResize"); - - expect(logEntries.length).toEqual(expected.length); - - for (let index = 0; index < expected.length; index++) { - const { panelId: actualPanelId, size: actualSize } = logEntries[index]; - const { panelId: expectedPanelId, sizePercentage: expectedPanelSize } = - expected[index]; - - expect(actualPanelId).toEqual(expectedPanelId); - expect(actualSize.sizePercentage).toEqual(expectedPanelSize); - } -} - -test.describe("Panel onResize prop", () => { - test.beforeEach(async ({ page }) => { - await openPage(page); - }); - - test("should be called once on-mount", async ({ page }) => { - await verifyEntries(page, [ - { panelId: "left", sizePercentage: 20 }, - { panelId: "middle", sizePercentage: 60 }, - { panelId: "right", sizePercentage: 20 }, - ]); - }); - - test("should be called when panels are resized", async ({ page }) => { - const leftHandle = page.locator( - '[data-panel-resize-handle-id="left-handle"]' - ); - const rightHandle = page.locator( - '[data-panel-resize-handle-id="right-handle"]' - ); - - await clearLogEntries(page); - - await leftHandle.focus(); - await page.keyboard.press("Home"); - await verifyEntries(page, [ - { panelId: "left", sizePercentage: 0 }, - { panelId: "middle", sizePercentage: 80 }, - ]); - - await clearLogEntries(page); - - await rightHandle.focus(); - await page.keyboard.press("End"); - await verifyEntries(page, [ - { panelId: "middle", sizePercentage: 100 }, - { panelId: "right", sizePercentage: 0 }, - ]); - - await clearLogEntries(page); - - await page.keyboard.press("ArrowLeft"); - await verifyEntries(page, [ - { panelId: "middle", sizePercentage: 90 }, - { panelId: "right", sizePercentage: 10 }, - ]); - }); - - test("should be called when triggering PanelGroup setLayout method", async ({ - page, - }) => { - await clearLogEntries(page); - - await imperativeResizePanelGroup(page, "group", ["10%", "20%", "70%"]); - - await verifyEntries(page, [ - { panelId: "left", sizePercentage: 10 }, - { panelId: "middle", sizePercentage: 20 }, - { panelId: "right", sizePercentage: 70 }, - ]); - }); - - test("should be called when a panel is added or removed from the group", async ({ - page, - }) => { - await verifyEntries(page, [ - { panelId: "left", sizePercentage: 20 }, - { panelId: "middle", sizePercentage: 60 }, - { panelId: "right", sizePercentage: 20 }, - ]); - - await clearLogEntries(page); - - await updateUrl(page, createElements(2)); - await verifyEntries(page, [ - { panelId: "left", sizePercentage: 40 }, - { panelId: "middle", sizePercentage: 60 }, - ]); - - await clearLogEntries(page); - - await updateUrl(page, createElements(3)); - await verifyEntries(page, [ - { panelId: "left", sizePercentage: 20 }, - { panelId: "middle", sizePercentage: 60 }, - { panelId: "right", sizePercentage: 20 }, - ]); - }); -}); diff --git a/packages/react-resizable-panels-website/tests/PanelGroup-PixelUnits.spec.ts b/packages/react-resizable-panels-website/tests/PanelGroup-PixelUnits.spec.ts deleted file mode 100644 index 14b3d3b95..000000000 --- a/packages/react-resizable-panels-website/tests/PanelGroup-PixelUnits.spec.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { Page, test } from "@playwright/test"; -import { createElement } from "react"; -import { - Panel, - PanelGroup, - PanelGroupProps, - PanelProps, - PanelResizeHandle, - PanelResizeHandleProps, -} from "react-resizable-panels"; - -import { - dragResizeBy, - imperativeResizePanel, - verifyPanelSizePixels, -} from "./utils/panels"; -import { goToUrl, updateUrl } from "./utils/url"; -import { verifySizesPixels } from "./utils/verify"; - -function createElements( - props: { - leftPanelProps?: PanelProps; - leftResizeHandleProps?: Partial; - middlePanelProps?: PanelProps; - panelGroupProps?: Partial; - rightPanelProps?: PanelProps; - rightResizeHandleProps?: Partial; - } = {} -) { - return createElement( - PanelGroup, - { - direction: "horizontal", - id: "group", - ...props.panelGroupProps, - }, - createElement(Panel, { - id: "left-panel", - minSizePixels: 10, - ...props.leftPanelProps, - }), - createElement(PanelResizeHandle, { - id: "left-resize-handle", - ...props.leftResizeHandleProps, - }), - createElement(Panel, { - id: "middle-panel", - minSizePixels: 10, - ...props.middlePanelProps, - }), - createElement(PanelResizeHandle, { - id: "right-resize-handle", - ...props.rightResizeHandleProps, - }), - createElement(Panel, { - id: "right-panel", - minSizePixels: 10, - ...props.rightPanelProps, - }) - ); -} - -async function goToUrlHelper( - page: Page, - props: { - leftPanelProps?: PanelProps; - leftResizeHandleProps?: Partial; - middlePanelProps?: PanelProps; - panelGroupProps?: Partial; - rightPanelProps?: PanelProps; - rightResizeHandleProps?: Partial; - } = {} -) { - await goToUrl(page, createElements(props)); -} - -test.describe("Pixel units", () => { - test.describe("initial layout", () => { - test("should observe max size constraint for default layout", async ({ - page, - }) => { - // Static left panel - await goToUrlHelper(page, { - leftPanelProps: { maxSizePixels: 100, minSizePixels: 50 }, - }); - const leftPanel = page.locator('[data-panel-id="left-panel"]'); - await verifyPanelSizePixels(leftPanel, 100); - - // Static middle panel - await goToUrlHelper(page, { - middlePanelProps: { maxSizePixels: 100, minSizePixels: 50 }, - }); - const middlePanel = page.locator('[data-panel-id="middle-panel"]'); - await verifyPanelSizePixels(middlePanel, 100); - - // Static right panel - await goToUrlHelper(page, { - rightPanelProps: { maxSizePixels: 100, minSizePixels: 50 }, - }); - const rightPanel = page.locator('[data-panel-id="right-panel"]'); - await verifyPanelSizePixels(rightPanel, 100); - }); - - test("should observe min size constraint for default layout", async ({ - page, - }) => { - await goToUrlHelper(page, { - leftPanelProps: { maxSizePixels: 300, minSizePixels: 200 }, - }); - - const leftPanel = page.locator("[data-panel]").first(); - await verifyPanelSizePixels(leftPanel, 200); - }); - - test("should honor min/max constraint when resizing via keyboard", async ({ - page, - }) => { - await goToUrlHelper(page, { - leftPanelProps: { maxSizePixels: 100, minSizePixels: 50 }, - }); - - const leftPanel = page.locator("[data-panel]").first(); - await verifyPanelSizePixels(leftPanel, 100); - - const resizeHandle = page - .locator("[data-panel-resize-handle-id]") - .first(); - await resizeHandle.focus(); - - await page.keyboard.press("Home"); - await verifyPanelSizePixels(leftPanel, 50); - - await page.keyboard.press("End"); - await verifyPanelSizePixels(leftPanel, 100); - }); - - test("should honor min/max constraint when resizing via mouse", async ({ - page, - }) => { - await goToUrlHelper(page, { - leftPanelProps: { maxSizePixels: 100, minSizePixels: 50 }, - }); - - const leftPanel = page.locator("[data-panel]").first(); - - await dragResizeBy(page, "left-resize-handle", -100); - await verifyPanelSizePixels(leftPanel, 50); - - await dragResizeBy(page, "left-resize-handle", 200); - await verifyPanelSizePixels(leftPanel, 100); - }); - - test("should honor min/max constraint when resizing via imperative Panel API", async ({ - page, - }) => { - await goToUrlHelper(page, { - leftPanelProps: { maxSizePixels: 100, minSizePixels: 50 }, - }); - - const leftPanel = page.locator("[data-panel]").first(); - - await imperativeResizePanel(page, "left-panel", { sizePixels: 150 }); - await verifyPanelSizePixels(leftPanel, 100); - - await imperativeResizePanel(page, "left-panel", { sizePixels: 4 }); - await verifyPanelSizePixels(leftPanel, 50); - }); - - test("should honor min/max constraint when indirectly resizing via imperative Panel API", async ({ - page, - }) => { - await goToUrlHelper(page, { - rightPanelProps: { maxSizePixels: 100, minSizePixels: 50 }, - }); - - const rightPanel = page.locator("[data-panel]").last(); - - await imperativeResizePanel(page, "middle-panel", { sizePixels: 1 }); - await verifyPanelSizePixels(rightPanel, 100); - - await imperativeResizePanel(page, "left-panel", { sizePixels: 350 }); - await verifyPanelSizePixels(rightPanel, 50); - }); - - test("should support collapsable panels", async ({ page }) => { - await goToUrlHelper(page, { - leftPanelProps: { - collapsible: true, - minSizePixels: 100, - maxSizePixels: 200, - }, - }); - - const leftPanel = page.locator("[data-panel]").first(); - - await imperativeResizePanel(page, "left-panel", { sizePixels: 25 }); - await verifyPanelSizePixels(leftPanel, 0); - - await imperativeResizePanel(page, "left-panel", { sizePixels: 100 }); - await verifyPanelSizePixels(leftPanel, 100); - }); - }); - - test("should observe min size pixel constraints if the overall group size shrinks", async ({ - page, - }) => { - await goToUrlHelper(page, { - leftPanelProps: { - defaultSizePixels: 50, - maxSizePixels: 100, - minSizePixels: 50, - }, - }); - const leftPanel = page.locator('[data-panel-id="left-panel"]'); - await verifyPanelSizePixels(leftPanel, 50); - - await page.setViewportSize({ width: 300, height: 300 }); - await new Promise((r) => setTimeout(r, 30)); - await verifyPanelSizePixels(leftPanel, 50); - - await page.setViewportSize({ width: 400, height: 300 }); - await goToUrlHelper(page, { - rightPanelProps: { - defaultSizePixels: 50, - maxSizePixels: 100, - minSizePixels: 50, - }, - }); - const rightPanel = page.locator('[data-panel-id="right-panel"]'); - await verifyPanelSizePixels(rightPanel, 50); - - await page.setViewportSize({ width: 300, height: 300 }); - await new Promise((resolve) => setTimeout(resolve, 250)); - await verifyPanelSizePixels(rightPanel, 50); - }); - - test("should observe max size pixel constraints if the overall group size expands", async ({ - page, - }) => { - await goToUrlHelper(page, { - leftPanelProps: { - defaultSizePixels: 100, - maxSizePixels: 100, - minSizePixels: 50, - }, - }); - - const leftPanel = page.locator('[data-panel-id="left-panel"]'); - - await verifyPanelSizePixels(leftPanel, 100); - - await page.setViewportSize({ width: 500, height: 300 }); - await verifyPanelSizePixels(leftPanel, 100); - - await page.setViewportSize({ width: 400, height: 300 }); - - await goToUrlHelper(page, { - rightPanelProps: { - defaultSizePixels: 100, - maxSizePixels: 100, - minSizePixels: 50, - }, - }); - - const rightPanel = page.locator('[data-panel-id="right-panel"]'); - - await verifyPanelSizePixels(rightPanel, 100); - - await page.setViewportSize({ width: 500, height: 300 }); - await new Promise((resolve) => setTimeout(resolve, 250)); - await verifyPanelSizePixels(rightPanel, 100); - }); - - test("should observe max size constraint for multiple panels", async ({ - page, - }) => { - await goToUrl( - page, - createElement( - PanelGroup, - { direction: "horizontal", id: "group" }, - createElement(Panel, { - id: "first-panel", - minSizePixels: 50, - maxSizePixels: 75, - }), - createElement(PanelResizeHandle, { - id: "first-resize-handle", - }), - createElement(Panel, { - id: "second-panel", - minSizePixels: 10, - }), - createElement(PanelResizeHandle, { - id: "second-resize-handle", - }), - createElement(Panel, { - id: "third-panel", - minSizePixels: 10, - }), - createElement(PanelResizeHandle, { - id: "third-resize-handle", - }), - createElement(Panel, { - id: "fourth-panel", - minSizePixels: 50, - maxSizePixels: 75, - }) - ) - ); - - const firstPanel = page.locator('[data-panel-id="first-panel"]'); - await verifyPanelSizePixels(firstPanel, 75); - - const fourthPanel = page.locator('[data-panel-id="fourth-panel"]'); - await verifyPanelSizePixels(fourthPanel, 75); - - await dragResizeBy(page, "second-resize-handle", -200); - await verifyPanelSizePixels(firstPanel, 50); - await verifyPanelSizePixels(fourthPanel, 75); - - await dragResizeBy(page, "second-resize-handle", 400); - await verifyPanelSizePixels(firstPanel, 50); - await verifyPanelSizePixels(fourthPanel, 50); - }); - - test("should validate persisted pixel layouts before re-applying", async ({ - page, - }) => { - let stored: { [name: string]: string } = {}; - const elements = createElements({ - panelGroupProps: { - autoSaveId: "test-group", - storage: { - getItem(name: string): string | null { - return stored[name] ?? null; - }, - setItem(name: string, value: string): void { - stored[name] = value; - }, - }, - }, - leftPanelProps: { - minSizePixels: 50, - }, - middlePanelProps: { - minSizePixels: 50, - }, - rightPanelProps: { - minSizePixels: 50, - }, - }); - await goToUrl(page, elements as any); - await verifySizesPixels(page, 132, 132, 132); - - await imperativeResizePanel(page, "left-panel", { sizePixels: 50 }); - await verifySizesPixels(page, 50, 214, 132); - - // Wait for localStorage write debounce - await new Promise((resolve) => setTimeout(resolve, 250)); - - // Unload page and resize window - await updateUrl(page, null); - await page.setViewportSize({ width: 300, height: 300 }); - - // Reload page and verify pixel validation has re-run on saved percentages - await updateUrl(page, elements); - await verifySizesPixels(page, 50, 147.3, 98.7); - }); -}); diff --git a/packages/react-resizable-panels-website/tests/ResizeHandle-OnDragging.spec.ts b/packages/react-resizable-panels-website/tests/ResizeHandle-OnDragging.spec.ts deleted file mode 100644 index 57f707207..000000000 --- a/packages/react-resizable-panels-website/tests/ResizeHandle-OnDragging.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { expect, Page, test } from "@playwright/test"; -import { createElement } from "react"; -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; - -import { PanelResizeHandleDraggingLogEntry } from "../src/routes/examples/types"; - -import { clearLogEntries, getLogEntries } from "./utils/debug"; -import { goToUrl } from "./utils/url"; - -async function openPage(page: Page) { - const panelGroup = createElement( - PanelGroup, - { direction: "horizontal", id: "group" }, - createElement(Panel, { - collapsible: true, - defaultSizePercentage: 20, - minSizePercentage: 10, - order: 1, - }), - createElement(PanelResizeHandle, { id: "left-handle" }), - createElement(Panel, { - defaultSizePercentage: 60, - minSizePercentage: 10, - order: 2, - }), - createElement(PanelResizeHandle, { id: "right-handle" }), - createElement(Panel, { - collapsible: true, - defaultSizePercentage: 20, - minSizePercentage: 10, - order: 3, - }) - ); - - await goToUrl(page, panelGroup); -} - -async function verifyEntries( - page: Page, - expected: Array<[handleId: string, isDragging: boolean]> -) { - const logEntries = await getLogEntries( - page, - "onDragging" - ); - - expect(logEntries.length).toEqual(expected.length); - - for (let index = 0; index < expected.length; index++) { - const { isDragging: isDraggingActual, resizeHandleId: handleIdActual } = - logEntries[index]; - const [handleIdExpected, isDraggingExpected] = expected[index]; - expect(handleIdExpected).toEqual(handleIdActual); - expect(isDraggingExpected).toEqual(isDraggingActual); - } -} - -test.describe("PanelResizeHandle onDragging prop", () => { - test.beforeEach(async ({ page }) => { - await openPage(page); - }); - - test("should not be called on-mount", async ({ page }) => { - await verifyEntries(page, []); - }); - - test("should be called when the panel ResizeHandle starts or stops resizing", async ({ - page, - }) => { - const leftHandle = page.locator( - '[data-panel-resize-handle-id="left-handle"]' - ); - const rightHandle = page.locator( - '[data-panel-resize-handle-id="right-handle"]' - ); - - await clearLogEntries(page, "onDragging"); - - let bounds = (await leftHandle.boundingBox())!; - await page.mouse.move(bounds.x, bounds.y); - await page.mouse.down(); - await page.mouse.move(5, 0); - await page.mouse.move(10, 0); - await page.mouse.move(15, 0); - await verifyEntries(page, [["left-handle", true]]); - - await page.mouse.up(); - await verifyEntries(page, [ - ["left-handle", true], - ["left-handle", false], - ]); - - await clearLogEntries(page, "onDragging"); - - bounds = (await rightHandle.boundingBox())!; - await page.mouse.move(bounds.x, bounds.y); - await page.mouse.down(); - await page.mouse.move(25, 0); - await verifyEntries(page, [["right-handle", true]]); - - await page.mouse.up(); - await verifyEntries(page, [ - ["right-handle", true], - ["right-handle", false], - ]); - }); -}); diff --git a/packages/react-resizable-panels-website/tests/ResizeHandle.spec.ts b/packages/react-resizable-panels-website/tests/ResizeHandle.spec.ts index 063c04f81..75a56959a 100644 --- a/packages/react-resizable-panels-website/tests/ResizeHandle.spec.ts +++ b/packages/react-resizable-panels-website/tests/ResizeHandle.spec.ts @@ -13,11 +13,11 @@ test.describe("Resize handle", () => { createElement( PanelGroup, { direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }), - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ); diff --git a/packages/react-resizable-panels-website/tests/Springy.spec.ts b/packages/react-resizable-panels-website/tests/Springy.spec.ts index 88cf662a9..3f331f70f 100644 --- a/packages/react-resizable-panels-website/tests/Springy.spec.ts +++ b/packages/react-resizable-panels-website/tests/Springy.spec.ts @@ -4,30 +4,30 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { dragResizeTo } from "./utils/panels"; import { goToUrl } from "./utils/url"; -import { verifySizesPercentages } from "./utils/verify"; +import { verifySizes } from "./utils/verify"; async function openPage(page: Page) { const panelGroup = createElement( PanelGroup, { direction: "horizontal", id: "group" }, createElement(Panel, { - defaultSizePercentage: 25, + defaultSize: 25, id: "left-panel", - minSizePercentage: 10, + minSize: 10, order: 1, }), createElement(PanelResizeHandle, { id: "left-handle" }), createElement(Panel, { id: "middle-panel", - minSizePercentage: 10, + minSize: 10, order: 2, }), createElement(PanelResizeHandle, { id: "right-handle" }), createElement(Panel, { collapsible: true, - defaultSizePercentage: 25, + defaultSize: 25, id: "right-panel", - minSizePercentage: 10, + minSize: 10, order: 4, }) ); @@ -43,7 +43,7 @@ test.describe("springy panels", () => { test("later panels should be springy when expanding then collapsing the first panel", async ({ page, }) => { - await verifySizesPercentages(page, 25, 50, 25); + await verifySizes(page, 25, 50, 25); // Test expanding the first panel await dragResizeTo( @@ -60,7 +60,7 @@ test.describe("springy panels", () => { test("earlier panels should be springy when expanding then collapsing the last panel", async ({ page, }) => { - await verifySizesPercentages(page, 25, 50, 25); + await verifySizes(page, 25, 50, 25); // Test expanding the last panel await dragResizeTo( @@ -77,7 +77,7 @@ test.describe("springy panels", () => { test("panels should remember a max spring point per drag", async ({ page, }) => { - await verifySizesPercentages(page, 25, 50, 25); + await verifySizes(page, 25, 50, 25); await dragResizeTo(page, "left-panel", { size: 70, diff --git a/packages/react-resizable-panels-website/tests/Storage.spec.ts b/packages/react-resizable-panels-website/tests/Storage.spec.ts index 913aac4aa..35dc84a5c 100644 --- a/packages/react-resizable-panels-website/tests/Storage.spec.ts +++ b/packages/react-resizable-panels-website/tests/Storage.spec.ts @@ -8,27 +8,27 @@ import { goToUrl } from "./utils/url"; const panelGroupABC = createElement( PanelGroup, { autoSaveId: "test-group", direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 10, order: 1 }), + createElement(Panel, { minSize: 10, order: 1 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10, order: 2 }), + createElement(Panel, { minSize: 10, order: 2 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10, order: 3 }) + createElement(Panel, { minSize: 10, order: 3 }) ); const panelGroupBC = createElement( PanelGroup, { autoSaveId: "test-group", direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 10, order: 2 }), + createElement(Panel, { minSize: 10, order: 2 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10, order: 3 }) + createElement(Panel, { minSize: 10, order: 3 }) ); const panelGroupAB = createElement( PanelGroup, { autoSaveId: "test-group", direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 10, order: 1 }), + createElement(Panel, { minSize: 10, order: 1 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10, order: 2 }) + createElement(Panel, { minSize: 10, order: 2 }) ); test.describe("Storage", () => { diff --git a/packages/react-resizable-panels-website/tests/WindowSplitter.spec.ts b/packages/react-resizable-panels-website/tests/WindowSplitter.spec.ts index de591bfe7..914cb32ad 100644 --- a/packages/react-resizable-panels-website/tests/WindowSplitter.spec.ts +++ b/packages/react-resizable-panels-website/tests/WindowSplitter.spec.ts @@ -20,9 +20,9 @@ async function goToDefaultUrl( createElement( PanelGroup, { direction }, - createElement(Panel, { minSizePercentage: 10, ...firstPanelProps }), + createElement(Panel, { minSize: 10, ...firstPanelProps }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ); } @@ -37,11 +37,11 @@ test.describe("Window Splitter", () => { PanelGroup, { direction: "horizontal" }, createElement(Panel, { - defaultSizePercentage: 35, - minSizePercentage: 20, + defaultSize: 35, + minSize: 20, }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 5 }) + createElement(Panel, { minSize: 5 }) ) ); @@ -59,9 +59,9 @@ test.describe("Window Splitter", () => { createElement( PanelGroup, { direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 20 }), + createElement(Panel, { minSize: 20 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 50 }) + createElement(Panel, { minSize: 50 }) ) ); @@ -80,11 +80,11 @@ test.describe("Window Splitter", () => { PanelGroup, { direction: "horizontal" }, createElement(Panel, { - maxSizePercentage: 50, - minSizePercentage: 10, + maxSize: 50, + minSize: 10, }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ); @@ -103,11 +103,11 @@ test.describe("Window Splitter", () => { PanelGroup, { direction: "horizontal" }, createElement(Panel, { - defaultSizePercentage: 65, - minSizePercentage: 10, + defaultSize: 65, + minSize: 10, }), createElement(PanelResizeHandle), - createElement(Panel, { maxSizePercentage: 40, minSizePercentage: 10 }) + createElement(Panel, { maxSize: 40, minSize: 10 }) ) ); @@ -222,7 +222,7 @@ test.describe("Window Splitter", () => { test("Enter key (not collapsible)", async ({ page }) => { await goToDefaultUrl(page, "horizontal", { collapsible: false, - minSizePercentage: 10, + minSize: 10, }); const resizeHandle = page.locator("[data-panel-resize-handle-id]"); @@ -280,12 +280,12 @@ test.describe("Window Splitter", () => { PanelGroup, { direction: "horizontal" }, createElement(Panel, { - defaultSizePercentage: 40, - maxSizePercentage: 70, - minSizePercentage: 20, + defaultSize: 40, + maxSize: 70, + minSize: 20, }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ); @@ -315,13 +315,13 @@ test.describe("Window Splitter", () => { createElement( PanelGroup, { direction: "horizontal" }, - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }), + createElement(Panel, { minSize: 10 }), createElement(PanelResizeHandle), - createElement(Panel, { minSizePercentage: 10 }) + createElement(Panel, { minSize: 10 }) ) ); diff --git a/packages/react-resizable-panels-website/tests/utils/assert.ts b/packages/react-resizable-panels-website/tests/utils/assert.ts index ec60087ba..997e3fc68 100644 --- a/packages/react-resizable-panels-website/tests/utils/assert.ts +++ b/packages/react-resizable-panels-website/tests/utils/assert.ts @@ -1,16 +1,8 @@ -import { ImperativePanelGroupHandle } from "react-resizable-panels"; -import { ImperativePanelHandle } from "react-resizable-panels"; - -export function assert( - expectedCondition: boolean, - message: string = "Assertion failed!" -): asserts expectedCondition { - if (!expectedCondition) { - console.error(message); - - throw Error(message); - } -} +import { assert } from "react-resizable-panels"; +import { + ImperativePanelGroupHandle, + ImperativePanelHandle, +} from "react-resizable-panels"; export function assertImperativePanelHandle( value: any diff --git a/packages/react-resizable-panels-website/tests/utils/panels.ts b/packages/react-resizable-panels-website/tests/utils/panels.ts index 11e7fc0e5..f50c4f18e 100644 --- a/packages/react-resizable-panels-website/tests/utils/panels.ts +++ b/packages/react-resizable-panels-website/tests/utils/panels.ts @@ -1,8 +1,7 @@ import { Locator, Page, expect } from "@playwright/test"; -import { assert } from "./assert"; +import { assert } from "react-resizable-panels"; import { getBodyCursorStyle } from "./cursor"; -import { verifyFuzzySizesPercentages } from "./verify"; -import { MixedSizes } from "react-resizable-panels"; +import { verifyFuzzySizes } from "./verify"; type Operation = { expectedCursor?: string; @@ -38,7 +37,7 @@ export async function dragResizeBy( export async function dragResizeTo( page: Page, panelId: string, - ...operations: Operation[] + ...operationsArray: Operation[] ) { const panels = page.locator("[data-panel-id]"); @@ -83,11 +82,14 @@ export async function dragResizeTo( await page.mouse.move(pageX, pageY); await page.mouse.down(); - for (let i = 0; i < operations.length; i++) { + for (let index = 0; index < operationsArray.length; index++) { pageX = Math.min(pageXMax - 1, Math.max(pageXMin + 1, pageX)); pageY = Math.min(pageYMax - 1, Math.max(pageYMin + 1, pageY)); - const { expectedSizes, expectedCursor, size: nextSize } = operations[i]; + const operations = operationsArray[index]; + assert(operations); + + const { expectedSizes, expectedCursor, size: nextSize } = operations; const prevSize = (await panel.getAttribute("data-panel-size"))!; const isExpanding = parseFloat(prevSize) < nextSize; @@ -134,7 +136,7 @@ export async function dragResizeTo( if (expectedSizes != null) { // This resizing approach isn't incredibly precise, // so we should allow for minor variations in panel sizes. - await verifyFuzzySizesPercentages(page, 0.25, ...expectedSizes); + await verifyFuzzySizes(page, 0.25, ...expectedSizes); } if (expectedCursor != null) { @@ -150,18 +152,14 @@ export async function dragResizeTo( export async function imperativeResizePanel( page: Page, panelId: string, - size: Partial + size: number ) { const panelIdSelect = page.locator("#panelIdSelect"); await panelIdSelect.selectOption(panelId); const sizeInput = page.locator("#sizeInput"); await sizeInput.focus(); - await sizeInput.fill( - size.sizePercentage != null - ? `${size.sizePercentage}%` - : `${size.sizePixels}px` - ); + await sizeInput.fill(`${size}%`); const resizeButton = page.locator("#resizeButton"); await resizeButton.click(); diff --git a/packages/react-resizable-panels-website/tests/utils/url.ts b/packages/react-resizable-panels-website/tests/utils/url.ts index c0e11285e..8d5d237ba 100644 --- a/packages/react-resizable-panels-website/tests/utils/url.ts +++ b/packages/react-resizable-panels-website/tests/utils/url.ts @@ -27,7 +27,7 @@ export async function updateUrl( await page.evaluate( ([encodedString]) => { const url = new URL(window.location.href); - url.searchParams.set("urlPanelGroup", encodedString); + url.searchParams.set("urlPanelGroup", encodedString ?? ""); window.history.pushState( { urlPanelGroup: encodedString }, diff --git a/packages/react-resizable-panels-website/tests/utils/verify.ts b/packages/react-resizable-panels-website/tests/utils/verify.ts index 95aaf1bce..42a3c64c0 100644 --- a/packages/react-resizable-panels-website/tests/utils/verify.ts +++ b/packages/react-resizable-panels-website/tests/utils/verify.ts @@ -2,12 +2,10 @@ import { expect, Page } from "@playwright/test"; import { PanelGroupLayoutLogEntry } from "../../src/routes/examples/types"; +import { assert } from "react-resizable-panels"; import { getLogEntries } from "./debug"; -export async function verifySizesPercentages( - page: Page, - ...expectedSizes: number[] -) { +export async function verifySizes(page: Page, ...expectedSizes: number[]) { const panels = page.locator("[data-panel-id]"); const count = await panels.count(); @@ -18,37 +16,13 @@ export async function verifySizesPercentages( const textContent = (await panel.textContent()) || ""; const expectedSize = expectedSizes[index]; - const rows = textContent.split("\n"); - const actualSize = - rows.length === 2 ? parseFloat(rows[0].replace("%", "")) : NaN; + const actualSize = parseFloat(textContent.replace("%", "")); expect(actualSize).toBe(expectedSize); } } -export async function verifySizesPixels( - page: Page, - ...expectedSizesPixels: number[] -) { - const panels = page.locator("[data-panel-id]"); - - const count = await panels.count(); - expect(count).toBe(expectedSizesPixels.length); - - for (let index = 0; index < count; index++) { - const panel = await panels.nth(index); - const textContent = (await panel.textContent()) || ""; - - const expectedSizePixels = expectedSizesPixels[index]; - const rows = textContent.split("\n"); - const actualSizePixels = - rows.length === 2 ? parseFloat(rows[1].replace("px", "")) : NaN; - - expect(actualSizePixels).toBe(expectedSizePixels); - } -} - -export async function verifyFuzzySizesPercentages( +export async function verifyFuzzySizes( page: Page, precision: number, ...expectedSizes: number[] @@ -57,15 +31,19 @@ export async function verifyFuzzySizesPercentages( page, "onLayout" ); - const actualSizes = logEntries[logEntries.length - 1].layout.map( - ({ sizePercentage }) => sizePercentage - ); + const logEntry = logEntries[logEntries.length - 1]; + assert(logEntry); + + const actualSizes = logEntry.layout; expect(actualSizes).toHaveLength(expectedSizes.length); for (let index = 0; index < actualSizes.length; index++) { const actualSize = actualSizes[index]; + assert(actualSize); + const expectedSize = expectedSizes[index]; + assert(expectedSize); expect(actualSize).toBeGreaterThanOrEqual(expectedSize - precision); expect(actualSize).toBeLessThanOrEqual(expectedSize + precision); diff --git a/packages/react-resizable-panels/.eslintrc.cjs b/packages/react-resizable-panels/.eslintrc.cjs index 860b1b47e..4a3162a93 100644 --- a/packages/react-resizable-panels/.eslintrc.cjs +++ b/packages/react-resizable-panels/.eslintrc.cjs @@ -9,6 +9,7 @@ module.exports = { plugins: ["@typescript-eslint", "no-restricted-imports", "react-hooks"], root: true, rules: { + "@typescript-eslint/no-non-null-assertion": "error", "no-restricted-imports": [ "error", { diff --git a/packages/react-resizable-panels/CHANGELOG.md b/packages/react-resizable-panels/CHANGELOG.md index e16a8ecd9..d3e681115 100644 --- a/packages/react-resizable-panels/CHANGELOG.md +++ b/packages/react-resizable-panels/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.0.0 + +- Remove support for pixel-based Panel constraints; (props like `defaultSizePercentage` should now be `defaultSize`) +- Replaced `dataAttributes` prop with `...rest` prop that supports all HTML attributes + ## 0.0.63 - Change default (not-yet-registered) Panel flex-grow style from 0 to 1 diff --git a/packages/react-resizable-panels/package.json b/packages/react-resizable-panels/package.json index 33066417c..41b088b28 100644 --- a/packages/react-resizable-panels/package.json +++ b/packages/react-resizable-panels/package.json @@ -1,6 +1,6 @@ { "name": "react-resizable-panels", - "version": "0.0.63", + "version": "1.0.0", "description": "React components for resizable panel groups/layouts", "author": "Brian Vaughn ", "license": "MIT", diff --git a/packages/react-resizable-panels/src/Panel.test.tsx b/packages/react-resizable-panels/src/Panel.test.tsx index ca57ab06b..c55975966 100644 --- a/packages/react-resizable-panels/src/Panel.test.tsx +++ b/packages/react-resizable-panels/src/Panel.test.tsx @@ -1,12 +1,8 @@ import { Root, createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; -import { - ImperativePanelHandle, - MixedSizes, - Panel, - PanelGroup, - PanelResizeHandle, -} from "."; +import { ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle } from "."; +import { assert } from "./utils/assert"; +import { getPanelElement } from "./utils/dom/getPanelElement"; import { mockPanelGroupOffsetWidthAndHeight, verifyExpandedPanelGroupLayout, @@ -66,7 +62,7 @@ describe("PanelGroup", () => { let leftPanelRef = createRef(); let rightPanelRef = createRef(); - let mostRecentLayout: MixedSizes[] | null; + let mostRecentLayout: number[] | null; beforeEach(() => { leftPanelRef = createRef(); @@ -74,67 +70,65 @@ describe("PanelGroup", () => { mostRecentLayout = null; - const onLayout = (layout: MixedSizes[]) => { + const onLayout = (layout: number[]) => { mostRecentLayout = layout; }; act(() => { root.render( - + - + ); }); }); it("should expand and collapse the first panel in a group", () => { - verifyExpandedPanelGroupLayout(mostRecentLayout!, [50, 50]); + assert(mostRecentLayout); + + verifyExpandedPanelGroupLayout(mostRecentLayout, [50, 50]); act(() => { - leftPanelRef.current!.collapse(); + leftPanelRef.current?.collapse(); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [0, 100]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [0, 100]); act(() => { - leftPanelRef.current!.expand(); + leftPanelRef.current?.expand(); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [50, 50]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [50, 50]); }); it("should expand and collapse the last panel in a group", () => { - verifyExpandedPanelGroupLayout(mostRecentLayout!, [50, 50]); + assert(mostRecentLayout); + + verifyExpandedPanelGroupLayout(mostRecentLayout, [50, 50]); act(() => { - rightPanelRef.current!.collapse(); + rightPanelRef.current?.collapse(); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [100, 0]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [100, 0]); act(() => { - rightPanelRef.current!.expand(); + rightPanelRef.current?.expand(); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [50, 50]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [50, 50]); }); it("should re-expand to the most recent size before collapsing", () => { - verifyExpandedPanelGroupLayout(mostRecentLayout!, [50, 50]); + assert(mostRecentLayout); + + verifyExpandedPanelGroupLayout(mostRecentLayout, [50, 50]); act(() => { - leftPanelRef.current!.resize({ sizePercentage: 30 }); + leftPanelRef.current?.resize(30); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [30, 70]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [30, 70]); act(() => { - leftPanelRef.current!.collapse(); + leftPanelRef.current?.collapse(); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [0, 100]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [0, 100]); act(() => { - leftPanelRef.current!.expand(); + leftPanelRef.current?.expand(); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [30, 70]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [30, 70]); }); }); @@ -143,7 +137,7 @@ describe("PanelGroup", () => { let middlePanelRef = createRef(); let rightPanelRef = createRef(); - let mostRecentLayout: MixedSizes[] | null; + let mostRecentLayout: number[] | null; beforeEach(() => { leftPanelRef = createRef(); @@ -152,45 +146,51 @@ describe("PanelGroup", () => { mostRecentLayout = null; - const onLayout = (layout: MixedSizes[]) => { + const onLayout = (layout: number[]) => { mostRecentLayout = layout; }; act(() => { root.render( - + - + - + ); }); }); it("should resize the first panel in a group", () => { - verifyExpandedPanelGroupLayout(mostRecentLayout!, [20, 60, 20]); + assert(mostRecentLayout); + + verifyExpandedPanelGroupLayout(mostRecentLayout, [20, 60, 20]); act(() => { - leftPanelRef.current!.resize({ sizePercentage: 40 }); + leftPanelRef.current?.resize(40); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [40, 40, 20]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [40, 40, 20]); }); it("should resize the middle panel in a group", () => { - verifyExpandedPanelGroupLayout(mostRecentLayout!, [20, 60, 20]); + assert(mostRecentLayout); + + verifyExpandedPanelGroupLayout(mostRecentLayout, [20, 60, 20]); act(() => { - middlePanelRef.current!.resize({ sizePercentage: 40 }); + middlePanelRef.current?.resize(40); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [20, 40, 40]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [20, 40, 40]); }); it("should resize the last panel in a group", () => { - verifyExpandedPanelGroupLayout(mostRecentLayout!, [20, 60, 20]); + assert(mostRecentLayout); + + verifyExpandedPanelGroupLayout(mostRecentLayout, [20, 60, 20]); act(() => { - rightPanelRef.current!.resize({ sizePercentage: 40 }); + rightPanelRef.current?.resize(40); }); - verifyExpandedPanelGroupLayout(mostRecentLayout!, [20, 40, 40]); + verifyExpandedPanelGroupLayout(mostRecentLayout, [20, 40, 40]); }); }); }); @@ -207,7 +207,7 @@ describe("PanelGroup", () => { act(() => { root.render( - + ); }); @@ -217,7 +217,7 @@ describe("PanelGroup", () => { act(() => { root.render( - + ); }); @@ -235,6 +235,247 @@ describe("PanelGroup", () => { }); }); + it("should support ...rest attributes", () => { + act(() => { + root.render( + + + + + + ); + }); + + const element = getPanelElement("panel"); + assert(element); + expect(element.tabIndex).toBe(123); + expect(element.getAttribute("data-test-name")).toBe("foo"); + expect(element.title).toBe("bar"); + }); + + describe("callbacks", () => { + describe("onCollapse", () => { + it("should be called on mount if a panels initial size is 0", () => { + let onCollapseLeft = jest.fn(); + let onCollapseRight = jest.fn(); + + act(() => { + root.render( + + + + + + ); + }); + + expect(onCollapseLeft).toHaveBeenCalledTimes(1); + expect(onCollapseRight).not.toHaveBeenCalled(); + }); + + it("should be called when a panel is collapsed", () => { + let onCollapse = jest.fn(); + + let panelRef = createRef(); + + act(() => { + root.render( + + + + + + ); + }); + + expect(onCollapse).not.toHaveBeenCalled(); + + act(() => { + panelRef.current?.collapse(); + }); + + expect(onCollapse).toHaveBeenCalledTimes(1); + }); + }); + + describe("onExpand", () => { + it("should be called on mount if a collapsible panels initial size is not 0", () => { + let onExpandLeft = jest.fn(); + let onExpandRight = jest.fn(); + + act(() => { + root.render( + + + + + + ); + }); + + expect(onExpandLeft).toHaveBeenCalledTimes(1); + expect(onExpandRight).not.toHaveBeenCalled(); + }); + + it("should be called when a collapsible panel is expanded", () => { + let onExpand = jest.fn(); + + let panelRef = createRef(); + + act(() => { + root.render( + + + + + + ); + }); + + expect(onExpand).not.toHaveBeenCalled(); + + act(() => { + panelRef.current?.resize(25); + }); + + expect(onExpand).toHaveBeenCalledTimes(1); + }); + }); + + describe("onResize", () => { + it("should be called on mount", () => { + let onResizeLeft = jest.fn(); + let onResizeMiddle = jest.fn(); + let onResizeRight = jest.fn(); + + act(() => { + root.render( + + + + + + + + ); + }); + + expect(onResizeLeft).toHaveBeenCalledTimes(1); + expect(onResizeLeft).toHaveBeenCalledWith(25, undefined); + expect(onResizeMiddle).toHaveBeenCalledTimes(1); + expect(onResizeMiddle).toHaveBeenCalledWith(50, undefined); + expect(onResizeRight).toHaveBeenCalledTimes(1); + expect(onResizeRight).toHaveBeenCalledWith(25, undefined); + }); + + it("should be called when a panel is added or removed from the group", () => { + let onResizeLeft = jest.fn(); + let onResizeMiddle = jest.fn(); + let onResizeRight = jest.fn(); + + act(() => { + root.render( + + + + ); + }); + + expect(onResizeLeft).not.toHaveBeenCalled(); + expect(onResizeMiddle).toHaveBeenCalledWith(100, undefined); + expect(onResizeRight).not.toHaveBeenCalled(); + + onResizeLeft.mockReset(); + onResizeMiddle.mockReset(); + onResizeRight.mockReset(); + + act(() => { + root.render( + + + + + + + + ); + }); + + expect(onResizeLeft).toHaveBeenCalledTimes(1); + expect(onResizeLeft).toHaveBeenCalledWith(25, undefined); + expect(onResizeMiddle).toHaveBeenCalledTimes(1); + expect(onResizeMiddle).toHaveBeenCalledWith(50, 100); + expect(onResizeRight).toHaveBeenCalledTimes(1); + expect(onResizeRight).toHaveBeenCalledWith(25, undefined); + + onResizeLeft.mockReset(); + onResizeMiddle.mockReset(); + onResizeRight.mockReset(); + + act(() => { + root.render( + + + + + + ); + }); + + expect(onResizeLeft).not.toHaveBeenCalled(); + expect(onResizeMiddle).toHaveBeenCalledTimes(1); + expect(onResizeMiddle).toHaveBeenCalledWith(75, 50); + expect(onResizeRight).not.toHaveBeenCalled(); + }); + }); + }); + describe("DEV warnings", () => { it("should warn about server rendered panels with no default size", () => { jest.resetModules(); @@ -254,15 +495,15 @@ describe("PanelGroup", () => { // No warning expected if default sizes provided renderToStaticMarkup( - + - + ); }); expectWarning( - "Panel defaultSizePercentage or defaultSizePixels prop recommended to avoid layout shift after server rendering" + "Panel defaultSize prop recommended to avoid layout shift after server rendering" ); act(() => { @@ -274,30 +515,13 @@ describe("PanelGroup", () => { }); }); - it("should warn if both pixel and percentage units are specified", () => { - // We just spot check this here; validatePanelConstraints() has its own in-depth tests - expectWarning( - "should not specify both percentage and pixel units for: min size" - ); - - expectWarning("Pixel based constraints require ResizeObserver"); - - act(() => { - root.render( - - - - ); - }); - }); - it("should warn if invalid sizes are specified declaratively", () => { expectWarning("default size should not be less than 0"); act(() => { root.render( - + diff --git a/packages/react-resizable-panels/src/Panel.ts b/packages/react-resizable-panels/src/Panel.ts index 861f8b33a..be9ba9d27 100644 --- a/packages/react-resizable-panels/src/Panel.ts +++ b/packages/react-resizable-panels/src/Panel.ts @@ -3,10 +3,10 @@ import { isDevelopment } from "#is-development"; import { PanelGroupContext } from "./PanelGroupContext"; import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect"; import useUniqueId from "./hooks/useUniqueId"; -import { DataAttributes, MixedSizes } from "./types"; import { ElementType, ForwardedRef, + HTMLAttributes, PropsWithChildren, createElement, forwardRef, @@ -18,8 +18,8 @@ import { export type PanelOnCollapse = () => void; export type PanelOnExpand = () => void; export type PanelOnResize = ( - mixedSizes: MixedSizes, - prevMixedSizes: MixedSizes | undefined + size: number, + prevSize: number | undefined ) => void; export type PanelCallbacks = { @@ -29,15 +29,11 @@ export type PanelCallbacks = { }; export type PanelConstraints = { - collapsedSizePercentage?: number | undefined; - collapsedSizePixels?: number | undefined; + collapsedSize?: number | undefined; collapsible?: boolean | undefined; - defaultSizePercentage?: number | undefined; - defaultSizePixels?: number | undefined; - maxSizePercentage?: number | undefined; - maxSizePixels?: number | undefined; - minSizePercentage?: number | undefined; - minSizePixels?: number | undefined; + defaultSize?: number | undefined; + maxSize?: number | undefined; + minSize?: number | undefined; }; export type PanelData = { @@ -52,54 +48,46 @@ export type ImperativePanelHandle = { collapse: () => void; expand: () => void; getId(): string; - getSize(): MixedSizes; + getSize(): number; isCollapsed: () => boolean; isExpanded: () => boolean; - resize: (size: Partial) => void; + resize: (size: number) => void; }; -export type PanelProps = PropsWithChildren<{ - className?: string; - collapsedSizePercentage?: number | undefined; - collapsedSizePixels?: number | undefined; - collapsible?: boolean | undefined; - dataAttributes?: DataAttributes; - defaultSizePercentage?: number | undefined; - defaultSizePixels?: number | undefined; - id?: string; - maxSizePercentage?: number | undefined; - maxSizePixels?: number | undefined; - minSizePercentage?: number | undefined; - minSizePixels?: number | undefined; - onCollapse?: PanelOnCollapse; - onExpand?: PanelOnExpand; - onResize?: PanelOnResize; - order?: number; - style?: object; - tagName?: ElementType; -}>; +export type PanelProps = Omit, "id" | "onResize"> & + PropsWithChildren<{ + className?: string; + collapsedSize?: number | undefined; + collapsible?: boolean | undefined; + defaultSize?: number | undefined; + id?: string; + maxSize?: number | undefined; + minSize?: number | undefined; + onCollapse?: PanelOnCollapse; + onExpand?: PanelOnExpand; + onResize?: PanelOnResize; + order?: number; + style?: object; + tagName?: ElementType; + }>; export function PanelWithForwardedRef({ children, className: classNameFromProps = "", - collapsedSizePercentage, - collapsedSizePixels, + collapsedSize, collapsible, - dataAttributes, - defaultSizePercentage, - defaultSizePixels, + defaultSize, forwardedRef, id: idFromProps, - maxSizePercentage, - maxSizePixels, - minSizePercentage, - minSizePixels, + maxSize, + minSize, onCollapse, onExpand, onResize, order, style: styleFromProps, tagName: Type = "div", + ...rest }: PanelProps & { forwardedRef: ForwardedRef; }) { @@ -131,15 +119,11 @@ export function PanelWithForwardedRef({ onResize, }, constraints: { - collapsedSizePercentage, - collapsedSizePixels, + collapsedSize, collapsible, - defaultSizePercentage, - defaultSizePixels, - maxSizePercentage, - maxSizePixels, - minSizePercentage, - minSizePixels, + defaultSize, + maxSize, + minSize, }, id: panelId, idIsFromProps: idFromProps !== undefined, @@ -156,14 +140,10 @@ export function PanelWithForwardedRef({ // but effects don't run on the server, so we can't do it there if (isDevelopment) { if (!devWarningsRef.current.didLogMissingDefaultSizeWarning) { - if ( - !isBrowser && - defaultSizePercentage == null && - defaultSizePixels == null - ) { + if (!isBrowser && defaultSize == null) { devWarningsRef.current.didLogMissingDefaultSizeWarning = true; console.warn( - `WARNING: Panel defaultSizePercentage or defaultSizePixels prop recommended to avoid layout shift after server rendering` + `WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering` ); } } @@ -180,15 +160,11 @@ export function PanelWithForwardedRef({ callbacks.onExpand = onExpand; callbacks.onResize = onResize; - constraints.collapsedSizePercentage = collapsedSizePercentage; - constraints.collapsedSizePixels = collapsedSizePixels; + constraints.collapsedSize = collapsedSize; constraints.collapsible = collapsible; - constraints.defaultSizePercentage = defaultSizePercentage; - constraints.defaultSizePixels = defaultSizePixels; - constraints.maxSizePercentage = maxSizePercentage; - constraints.maxSizePixels = maxSizePixels; - constraints.minSizePercentage = minSizePercentage; - constraints.minSizePixels = minSizePixels; + constraints.defaultSize = defaultSize; + constraints.maxSize = maxSize; + constraints.minSize = minSize; }); useIsomorphicLayoutEffect(() => { @@ -222,8 +198,8 @@ export function PanelWithForwardedRef({ isExpanded() { return !isPanelCollapsed(panelDataRef.current); }, - resize: (mixedSizes: Partial) => { - resizePanel(panelDataRef.current, mixedSizes); + resize: (size: number) => { + resizePanel(panelDataRef.current, size); }, }), [ @@ -239,6 +215,8 @@ export function PanelWithForwardedRef({ const style = getPanelStyle(panelDataRef.current); return createElement(Type, { + ...rest, + children, className: classNameFromProps, style: { @@ -246,8 +224,6 @@ export function PanelWithForwardedRef({ ...styleFromProps, }, - ...dataAttributes, - // CSS selectors "data-panel": "", "data-panel-id": panelId, diff --git a/packages/react-resizable-panels/src/PanelGroup.test.tsx b/packages/react-resizable-panels/src/PanelGroup.test.tsx index 9d7b830ce..31380fed1 100644 --- a/packages/react-resizable-panels/src/PanelGroup.test.tsx +++ b/packages/react-resizable-panels/src/PanelGroup.test.tsx @@ -2,11 +2,13 @@ import { Root, createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { ImperativePanelGroupHandle, - MixedSizes, + ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle, } from "."; +import { assert } from "./utils/assert"; +import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement"; import { mockPanelGroupOffsetWidthAndHeight } from "./utils/test-utils"; import { createRef } from "./vendor/react"; @@ -67,62 +69,124 @@ describe("PanelGroup", () => { root.render(); }); - expect(ref.current!.getId()).toBe("one"); + expect(ref.current?.getId()).toBe("one"); act(() => { root.render(); }); - expect(ref.current!.getId()).toBe("two"); + expect(ref.current?.getId()).toBe("two"); }); it("should get and set layouts", () => { const ref = createRef(); - let mostRecentLayout: MixedSizes[] | null = null; + let mostRecentLayout: number[] | null = null; - const onLayout = (layout: MixedSizes[]) => { + const onLayout = (layout: number[]) => { mostRecentLayout = layout; }; act(() => { root.render( - + - + ); }); - expect(mostRecentLayout).toEqual([ - { - sizePercentage: 50, - sizePixels: 500, - }, - { - sizePercentage: 50, - sizePixels: 500, - }, - ]); + expect(mostRecentLayout).toEqual([50, 50]); act(() => { - ref.current!.setLayout([ - { sizePercentage: 25 }, - { sizePercentage: 75 }, - ]); + ref.current?.setLayout([25, 75]); }); - expect(mostRecentLayout).toEqual([ - { - sizePercentage: 25, - sizePixels: 250, - }, - { - sizePercentage: 75, - sizePixels: 750, - }, - ]); + expect(mostRecentLayout).toEqual([25, 75]); + }); + }); + + it("should support ...rest attributes", () => { + act(() => { + root.render( + + + + + + ); + }); + + const element = getPanelGroupElement("group"); + assert(element); + expect(element.tabIndex).toBe(123); + expect(element.getAttribute("data-test-name")).toBe("foo"); + expect(element.title).toBe("bar"); + }); + + describe("callbacks", () => { + describe("onLayout", () => { + it("should be called with the initial group layout on mount", () => { + let onLayout = jest.fn(); + + act(() => { + root.render( + + + + + + ); + }); + + expect(onLayout).toHaveBeenCalledTimes(1); + expect(onLayout).toHaveBeenCalledWith([35, 65]); + }); + + it("should be called any time the group layout changes", () => { + let onLayout = jest.fn(); + let panelGroupRef = createRef(); + let panelRef = createRef(); + + act(() => { + root.render( + + + + + + ); + }); + + onLayout.mockReset(); + + act(() => { + panelGroupRef.current?.setLayout([25, 75]); + }); + + expect(onLayout).toHaveBeenCalledTimes(1); + expect(onLayout).toHaveBeenCalledWith([25, 75]); + + onLayout.mockReset(); + + act(() => { + panelRef.current?.resize(50); + }); + + expect(onLayout).toHaveBeenCalledTimes(1); + expect(onLayout).toHaveBeenCalledWith([50, 50]); + }); }); }); @@ -131,7 +195,7 @@ describe("PanelGroup", () => { act(() => { root.render( - + ); }); @@ -143,9 +207,9 @@ describe("PanelGroup", () => { act(() => { root.render( - + - + ); }); @@ -172,9 +236,9 @@ describe("PanelGroup", () => { act(() => { root.render( - + - + ); }); @@ -190,9 +254,9 @@ describe("PanelGroup", () => { id="group-without-handle" ref={ref} > - + - + ); }); @@ -200,10 +264,7 @@ describe("PanelGroup", () => { expectWarning("Invalid layout total size: 60%, 80%"); act(() => { - ref.current!.setLayout([ - { sizePercentage: 60 }, - { sizePercentage: 80 }, - ]); + ref.current?.setLayout([60, 80]); }); }); }); diff --git a/packages/react-resizable-panels/src/PanelGroup.ts b/packages/react-resizable-panels/src/PanelGroup.ts index 23000bb4e..80dc7817b 100644 --- a/packages/react-resizable-panels/src/PanelGroup.ts +++ b/packages/react-resizable-panels/src/PanelGroup.ts @@ -4,35 +4,30 @@ import { DragState, PanelGroupContext, ResizeEvent } from "./PanelGroupContext"; import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect"; import useUniqueId from "./hooks/useUniqueId"; import { useWindowSplitterPanelGroupBehavior } from "./hooks/useWindowSplitterPanelGroupBehavior"; -import { DataAttributes, Direction, MixedSizes } from "./types"; +import { Direction } from "./types"; import { adjustLayoutByDelta } from "./utils/adjustLayoutByDelta"; import { areEqual } from "./utils/arrays"; +import { assert } from "./utils/assert"; import { calculateDeltaPercentage } from "./utils/calculateDeltaPercentage"; import { calculateUnsafeDefaultLayout } from "./utils/calculateUnsafeDefaultLayout"; import { callPanelCallbacks } from "./utils/callPanelCallbacks"; import { compareLayouts } from "./utils/compareLayouts"; import { computePanelFlexBoxStyle } from "./utils/computePanelFlexBoxStyle"; -import { computePercentagePanelConstraints } from "./utils/computePercentagePanelConstraints"; -import { convertPercentageToPixels } from "./utils/convertPercentageToPixels"; import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor"; import debounce from "./utils/debounce"; import { determinePivotIndices } from "./utils/determinePivotIndices"; -import { calculateAvailablePanelSizeInPixels } from "./utils/dom/calculateAvailablePanelSizeInPixels"; -import { getPanelElementsForGroup } from "./utils/dom/getPanelElementsForGroup"; -import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement"; import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement"; import { isKeyDown, isMouseEvent, isTouchEvent } from "./utils/events"; -import { getPercentageSizeFromMixedSizes } from "./utils/getPercentageSizeFromMixedSizes"; import { getResizeEventCursorPosition } from "./utils/getResizeEventCursorPosition"; import { initializeDefaultStorage } from "./utils/initializeDefaultStorage"; import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization"; -import { shouldMonitorPixelBasedConstraints } from "./utils/shouldMonitorPixelBasedConstraints"; import { validatePanelConstraints } from "./utils/validatePanelConstraints"; import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout"; import { CSSProperties, ElementType, ForwardedRef, + HTMLAttributes, PropsWithChildren, createElement, forwardRef, @@ -48,8 +43,8 @@ const LOCAL_STORAGE_DEBOUNCE_INTERVAL = 100; export type ImperativePanelGroupHandle = { getId: () => string; - getLayout: () => MixedSizes[]; - setLayout: (layout: Partial[]) => void; + getLayout: () => number[]; + setLayout: (layout: number[]) => void; }; export type PanelGroupStorage = { @@ -57,7 +52,7 @@ export type PanelGroupStorage = { setItem(name: string, value: string): void; }; -export type PanelGroupOnLayout = (layout: MixedSizes[]) => void; +export type PanelGroupOnLayout = (layout: number[]) => void; const defaultStorage: PanelGroupStorage = { getItem: (name: string) => { @@ -70,19 +65,18 @@ const defaultStorage: PanelGroupStorage = { }, }; -export type PanelGroupProps = PropsWithChildren<{ - autoSaveId?: string | null; - className?: string; - dataAttributes?: DataAttributes; - direction: Direction; - id?: string | null; - keyboardResizeByPercentage?: number | null; - keyboardResizeByPixels?: number | null; - onLayout?: PanelGroupOnLayout | null; - storage?: PanelGroupStorage; - style?: CSSProperties; - tagName?: ElementType; -}>; +export type PanelGroupProps = Omit, "id"> & + PropsWithChildren<{ + autoSaveId?: string | null; + className?: string; + direction: Direction; + id?: string | null; + keyboardResizeBy?: number | null; + onLayout?: PanelGroupOnLayout | null; + storage?: PanelGroupStorage; + style?: CSSProperties; + tagName?: ElementType; + }>; const debounceMap: { [key: string]: typeof savePanelGroupLayout; @@ -92,16 +86,15 @@ function PanelGroupWithForwardedRef({ autoSaveId = null, children, className: classNameFromProps = "", - dataAttributes, direction, forwardedRef, - id: idFromProps, + id: idFromProps = null, onLayout = null, - keyboardResizeByPercentage = null, - keyboardResizeByPixels = null, + keyboardResizeBy = null, storage = defaultStorage, style: styleFromProps, tagName: Type = "div", + ...rest }: PanelGroupProps & { forwardedRef: ForwardedRef; }) { @@ -109,10 +102,9 @@ function PanelGroupWithForwardedRef({ const [dragState, setDragState] = useState(null); const [layout, setLayout] = useState([]); + const [panelDataArray, setPanelDataArray] = useState([]); - const panelIdToLastNotifiedMixedSizesMapRef = useRef< - Record - >({}); + const panelIdToLastNotifiedSizeMapRef = useRef>({}); const panelSizeBeforeCollapseRef = useRef>(new Map()); const prevDeltaRef = useRef(0); @@ -121,8 +113,7 @@ function PanelGroupWithForwardedRef({ direction: Direction; dragState: DragState | null; id: string; - keyboardResizeByPercentage: number | null; - keyboardResizeByPixels: number | null; + keyboardResizeBy: number | null; onLayout: PanelGroupOnLayout | null; storage: PanelGroupStorage; }>({ @@ -130,8 +121,7 @@ function PanelGroupWithForwardedRef({ direction, dragState, id: groupId, - keyboardResizeByPercentage, - keyboardResizeByPixels, + keyboardResizeBy, onLayout, storage, }); @@ -139,9 +129,11 @@ function PanelGroupWithForwardedRef({ const eagerValuesRef = useRef<{ layout: number[]; panelDataArray: PanelData[]; + panelDataArrayChanged: boolean; }>({ layout, panelDataArray: [], + panelDataArrayChanged: false, }); const devWarningsRef = useRef<{ @@ -159,34 +151,15 @@ function PanelGroupWithForwardedRef({ () => ({ getId: () => committedValuesRef.current.id, getLayout: () => { - const { id: groupId } = committedValuesRef.current; const { layout } = eagerValuesRef.current; - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); - - return layout.map((sizePercentage) => { - return { - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - }; - }); + return layout; }, - setLayout: (mixedSizes: Partial[]) => { - const { id: groupId, onLayout } = committedValuesRef.current; + setLayout: (unsafeLayout: number[]) => { + const { onLayout } = committedValuesRef.current; const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); - - const unsafeLayout = mixedSizes.map( - (mixedSize) => - getPercentageSizeFromMixedSizes(mixedSize, groupSizePixels)! - ); - const safeLayout = validatePanelGroupLayout({ - groupSizePixels, layout: unsafeLayout, panelConstraints: panelDataArray.map( (panelData) => panelData.constraints @@ -199,22 +172,13 @@ function PanelGroupWithForwardedRef({ eagerValuesRef.current.layout = safeLayout; if (onLayout) { - onLayout( - safeLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) - ); + onLayout(safeLayout); } callPanelCallbacks( - groupId, panelDataArray, safeLayout, - panelIdToLastNotifiedMixedSizesMapRef.current + panelIdToLastNotifiedSizeMapRef.current ); } }, @@ -229,9 +193,6 @@ function PanelGroupWithForwardedRef({ committedValuesRef.current.id = groupId; committedValuesRef.current.onLayout = onLayout; committedValuesRef.current.storage = storage; - - // panelDataArray and layout are updated in-sync with scheduled state updates. - // TODO [217] Move these values into a separate ref }); useWindowSplitterPanelGroupBehavior({ @@ -252,77 +213,21 @@ function PanelGroupWithForwardedRef({ return; } + let debouncedSave = debounceMap[autoSaveId]; + // Limit the frequency of localStorage updates. - if (!debounceMap[autoSaveId]) { - debounceMap[autoSaveId] = debounce( + if (debouncedSave == null) { + debouncedSave = debounce( savePanelGroupLayout, LOCAL_STORAGE_DEBOUNCE_INTERVAL ); - } - debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage); - } - }, [autoSaveId, layout, storage]); - - useIsomorphicLayoutEffect(() => { - const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; - - const constraints = panelDataArray.map(({ constraints }) => constraints); - if (!shouldMonitorPixelBasedConstraints(constraints)) { - // Avoid the overhead of ResizeObserver if no pixel constraints require monitoring - return; - } - - if (typeof ResizeObserver === "undefined") { - console.warn( - `WARNING: Pixel based constraints require ResizeObserver but it is not supported by the current browser.` - ); - } else { - const resizeObserver = new ResizeObserver(() => { - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); - - const { onLayout } = committedValuesRef.current; - - const nextLayout = validatePanelGroupLayout({ - groupSizePixels, - layout: prevLayout, - panelConstraints: panelDataArray.map( - (panelData) => panelData.constraints - ), - }); - - if (!areEqual(prevLayout, nextLayout)) { - setLayout(nextLayout); - eagerValuesRef.current.layout = nextLayout; - - if (onLayout) { - onLayout( - nextLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) - ); - } - - callPanelCallbacks( - groupId, - panelDataArray, - nextLayout, - panelIdToLastNotifiedMixedSizesMapRef.current - ); - } - }); - - resizeObserver.observe(getPanelGroupElement(groupId)!); + debounceMap[autoSaveId] = debouncedSave; + } - return () => { - resizeObserver.disconnect(); - }; + debouncedSave(autoSaveId, panelDataArray, layout, storage); } - }, [groupId]); + }, [autoSaveId, layout, storage]); // DEV warnings useEffect(() => { @@ -362,17 +267,17 @@ function PanelGroupWithForwardedRef({ (panelData) => panelData.constraints ); - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); - for ( let panelIndex = 0; panelIndex < panelConstraints.length; panelIndex++ ) { + const panelData = panelDataArray[panelIndex]; + assert(panelData); + const isValid = validatePanelConstraints({ - groupSizePixels, panelConstraints, - panelId: panelDataArray[panelIndex].id, + panelId: panelData.id, panelIndex, }); @@ -387,177 +292,139 @@ function PanelGroupWithForwardedRef({ }); // External APIs are safe to memoize via committed values ref - const collapsePanel = useCallback( - (panelData: PanelData) => { - const { onLayout } = committedValuesRef.current; - const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; + const collapsePanel = useCallback((panelData: PanelData) => { + 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 { - collapsedSizePercentage, - panelSizePercentage, - pivotIndices, - groupSizePixels, - } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout); - - if (panelSizePercentage !== collapsedSizePercentage) { - // Store size before collapse; - // This is the size that gets restored if the expand() API is used. - panelSizeBeforeCollapseRef.current.set( - panelData.id, - panelSizePercentage - ); + const { + collapsedSize = 0, + panelSize, + pivotIndices, + } = panelDataHelper(panelDataArray, panelData, prevLayout); - const isLastPanel = - panelDataArray.indexOf(panelData) === panelDataArray.length - 1; - const delta = isLastPanel - ? panelSizePercentage - collapsedSizePercentage - : collapsedSizePercentage - panelSizePercentage; - - const nextLayout = adjustLayoutByDelta({ - delta, - groupSizePixels, - layout: prevLayout, - panelConstraints: panelConstraintsArray, - pivotIndices, - trigger: "imperative-api", - }); + assert(panelSize != null); - if (!compareLayouts(prevLayout, nextLayout)) { - setLayout(nextLayout); + if (panelSize !== collapsedSize) { + // Store size before collapse; + // This is the size that gets restored if the expand() API is used. + panelSizeBeforeCollapseRef.current.set(panelData.id, panelSize); - eagerValuesRef.current.layout = nextLayout; + const isLastPanel = + findPanelDataIndex(panelDataArray, panelData) === + panelDataArray.length - 1; + const delta = isLastPanel + ? panelSize - collapsedSize + : collapsedSize - panelSize; - if (onLayout) { - onLayout( - nextLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) - ); - } + const nextLayout = adjustLayoutByDelta({ + delta, + layout: prevLayout, + panelConstraints: panelConstraintsArray, + pivotIndices, + trigger: "imperative-api", + }); - callPanelCallbacks( - groupId, - panelDataArray, - nextLayout, - panelIdToLastNotifiedMixedSizesMapRef.current - ); + if (!compareLayouts(prevLayout, nextLayout)) { + setLayout(nextLayout); + + eagerValuesRef.current.layout = nextLayout; + + if (onLayout) { + onLayout(nextLayout); } + + callPanelCallbacks( + panelDataArray, + nextLayout, + panelIdToLastNotifiedSizeMapRef.current + ); } } - }, - [groupId] - ); + } + }, []); // 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) => { + 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, + minSize = 0, + pivotIndices, + } = panelDataHelper(panelDataArray, panelData, prevLayout); + + if (panelSize === collapsedSize) { + // Restore this panel to the size it was before it was collapsed, if possible. + const prevPanelSize = panelSizeBeforeCollapseRef.current.get( + panelData.id ); - const { - collapsedSizePercentage, - panelSizePercentage, - minSizePercentage, - pivotIndices, - groupSizePixels, - } = panelDataHelper(groupId, panelDataArray, panelData, prevLayout); - - if (panelSizePercentage === collapsedSizePercentage) { - // Restore this panel to the size it was before it was collapsed, if possible. - const prevPanelSizePercentage = - panelSizeBeforeCollapseRef.current.get(panelData.id); - - const baseSizePercentage = - prevPanelSizePercentage != null && - prevPanelSizePercentage >= minSizePercentage - ? prevPanelSizePercentage - : minSizePercentage; - - const isLastPanel = - panelDataArray.indexOf(panelData) === panelDataArray.length - 1; - const delta = isLastPanel - ? panelSizePercentage - baseSizePercentage - : baseSizePercentage - panelSizePercentage; - - const nextLayout = adjustLayoutByDelta({ - delta, - groupSizePixels, - layout: prevLayout, - panelConstraints: panelConstraintsArray, - pivotIndices, - trigger: "imperative-api", - }); + const baseSize = + prevPanelSize != null && prevPanelSize >= minSize + ? prevPanelSize + : minSize; - if (!compareLayouts(prevLayout, nextLayout)) { - setLayout(nextLayout); + const isLastPanel = + findPanelDataIndex(panelDataArray, panelData) === + panelDataArray.length - 1; + const delta = isLastPanel ? panelSize - baseSize : baseSize - panelSize; - eagerValuesRef.current.layout = nextLayout; + const nextLayout = adjustLayoutByDelta({ + delta, + layout: prevLayout, + panelConstraints: panelConstraintsArray, + pivotIndices, + trigger: "imperative-api", + }); - if (onLayout) { - onLayout( - nextLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) - ); - } + if (!compareLayouts(prevLayout, nextLayout)) { + setLayout(nextLayout); - callPanelCallbacks( - groupId, - panelDataArray, - nextLayout, - panelIdToLastNotifiedMixedSizesMapRef.current - ); + eagerValuesRef.current.layout = nextLayout; + + if (onLayout) { + onLayout(nextLayout); } + + callPanelCallbacks( + panelDataArray, + nextLayout, + panelIdToLastNotifiedSizeMapRef.current + ); } } - }, - [groupId] - ); + } + }, []); // External APIs are safe to memoize via committed values ref - const getPanelSize = useCallback( - (panelData: PanelData) => { - const { layout, panelDataArray } = eagerValuesRef.current; + const getPanelSize = useCallback((panelData: PanelData) => { + const { layout, panelDataArray } = eagerValuesRef.current; - const { panelSizePercentage, panelSizePixels } = panelDataHelper( - groupId, - panelDataArray, - panelData, - layout - ); + const { panelSize } = panelDataHelper(panelDataArray, panelData, layout); - return { - sizePercentage: panelSizePercentage, - sizePixels: panelSizePixels, - }; - }, - [groupId] - ); + assert(panelSize != null); + + return panelSize; + }, []); // This API should never read from committedValuesRef const getPanelStyle = useCallback( (panelData: PanelData) => { const { panelDataArray } = eagerValuesRef.current; - const panelIndex = panelDataArray.indexOf(panelData); + const panelIndex = findPanelDataIndex(panelDataArray, panelData); return computePanelFlexBoxStyle({ dragState, @@ -570,41 +437,35 @@ function PanelGroupWithForwardedRef({ ); // External APIs are safe to memoize via committed values ref - const isPanelCollapsed = useCallback( - (panelData: PanelData) => { - const { layout, panelDataArray } = eagerValuesRef.current; + const isPanelCollapsed = useCallback((panelData: PanelData) => { + const { layout, panelDataArray } = eagerValuesRef.current; - const { collapsedSizePercentage, collapsible, panelSizePercentage } = - panelDataHelper(groupId, panelDataArray, panelData, layout); + const { collapsedSize, collapsible, panelSize } = panelDataHelper( + panelDataArray, + panelData, + layout + ); - return ( - collapsible === true && panelSizePercentage === collapsedSizePercentage - ); - }, - [groupId] - ); + return collapsible === true && panelSize === collapsedSize; + }, []); // External APIs are safe to memoize via committed values ref - const isPanelExpanded = useCallback( - (panelData: PanelData) => { - const { layout, panelDataArray } = eagerValuesRef.current; + const isPanelExpanded = useCallback((panelData: PanelData) => { + const { layout, panelDataArray } = eagerValuesRef.current; - const { collapsedSizePercentage, collapsible, panelSizePercentage } = - panelDataHelper(groupId, panelDataArray, panelData, layout); + const { + collapsedSize = 0, + collapsible, + panelSize, + } = panelDataHelper(panelDataArray, panelData, layout); - return !collapsible || panelSizePercentage > collapsedSizePercentage; - }, - [groupId] - ); + assert(panelSize != null); + + return !collapsible || panelSize > collapsedSize; + }, []); const registerPanel = useCallback((panelData: PanelData) => { - const { - autoSaveId, - id: groupId, - onLayout, - storage, - } = committedValuesRef.current; - const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; + const { panelDataArray } = eagerValuesRef.current; panelDataArray.push(panelData); panelDataArray.sort((panelA, panelB) => { @@ -621,77 +482,57 @@ function PanelGroupWithForwardedRef({ } }); - // Wait until all panels have registered before we try to compute layout; - // doing it earlier is both wasteful and may trigger misleading warnings in development mode. - const panelElements = getPanelElementsForGroup(groupId); - if (panelElements.length !== panelDataArray.length) { - return; - } + eagerValuesRef.current.panelDataArrayChanged = true; + }, []); - // If this panel has been configured to persist sizing information, - // default size should be restored from local storage if possible. - let unsafeLayout: number[] | null = null; - if (autoSaveId) { - unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage); - } + // (Re)calculate group layout whenever panels are registered or unregistered. + // eslint-disable-next-line react-hooks/exhaustive-deps + useIsomorphicLayoutEffect(() => { + if (eagerValuesRef.current.panelDataArrayChanged) { + eagerValuesRef.current.panelDataArrayChanged = false; - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); - if (groupSizePixels <= 0) { - if ( - shouldMonitorPixelBasedConstraints( - panelDataArray.map(({ constraints }) => constraints) - ) - ) { - // Wait until the group has rendered a non-zero size before computing layout. - return; + const { autoSaveId, onLayout, storage } = committedValuesRef.current; + const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; + + // If this panel has been configured to persist sizing information, + // default size should be restored from local storage if possible. + let unsafeLayout: number[] | null = null; + if (autoSaveId) { + unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage); } - } - if (unsafeLayout == null) { - unsafeLayout = calculateUnsafeDefaultLayout({ - groupSizePixels, - panelDataArray, + if (unsafeLayout == null) { + unsafeLayout = calculateUnsafeDefaultLayout({ + panelDataArray, + }); + } + + // Validate even saved layouts in case something has changed since last render + // e.g. for pixel groups, this could be the size of the window + const nextLayout = validatePanelGroupLayout({ + layout: unsafeLayout, + panelConstraints: panelDataArray.map( + (panelData) => panelData.constraints + ), }); - } - // Validate even saved layouts in case something has changed since last render - // e.g. for pixel groups, this could be the size of the window - const nextLayout = validatePanelGroupLayout({ - groupSizePixels, - layout: unsafeLayout, - panelConstraints: panelDataArray.map( - (panelData) => panelData.constraints - ), - }); + if (!areEqual(prevLayout, nextLayout)) { + setLayout(nextLayout); - // Offscreen mode makes this a bit weird; - // Panels unregister when hidden and re-register when shown again, - // but the overall layout doesn't change between these two cases. - setLayout(nextLayout); - - eagerValuesRef.current.layout = nextLayout; - - if (!areEqual(prevLayout, nextLayout)) { - if (onLayout) { - onLayout( - nextLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) + eagerValuesRef.current.layout = nextLayout; + + if (onLayout) { + onLayout(nextLayout); + } + + callPanelCallbacks( + panelDataArray, + nextLayout, + panelIdToLastNotifiedSizeMapRef.current ); } - - callPanelCallbacks( - groupId, - panelDataArray, - nextLayout, - panelIdToLastNotifiedMixedSizesMapRef.current - ); } - }, []); + }); const registerResizeHandle = useCallback((dragHandleId: string) => { return function resizeHandler(event: ResizeEvent) { @@ -701,8 +542,7 @@ function PanelGroupWithForwardedRef({ direction, dragState, id: groupId, - keyboardResizeByPercentage, - keyboardResizeByPixels, + keyboardResizeBy, onLayout, } = committedValuesRef.current; const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; @@ -713,14 +553,10 @@ function PanelGroupWithForwardedRef({ let delta = calculateDeltaPercentage( event, - groupId, dragHandleId, direction, - dragState!, - { - percentage: keyboardResizeByPercentage, - pixels: keyboardResizeByPixels, - } + dragState, + keyboardResizeBy ); if (delta === 0) { return; @@ -732,14 +568,12 @@ function PanelGroupWithForwardedRef({ delta = -delta; } - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); const panelConstraints = panelDataArray.map( (panelData) => panelData.constraints ); const nextLayout = adjustLayoutByDelta({ delta, - groupSizePixels, layout: initialLayout ?? prevLayout, panelConstraints, pivotIndices, @@ -782,22 +616,13 @@ function PanelGroupWithForwardedRef({ eagerValuesRef.current.layout = nextLayout; if (onLayout) { - onLayout( - nextLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) - ); + onLayout(nextLayout); } callPanelCallbacks( - groupId, panelDataArray, nextLayout, - panelIdToLastNotifiedMixedSizesMapRef.current + panelIdToLastNotifiedSizeMapRef.current ); } }; @@ -805,7 +630,7 @@ function PanelGroupWithForwardedRef({ // External APIs are safe to memoize via committed values ref const resizePanel = useCallback( - (panelData: PanelData, mixedSizes: Partial) => { + (panelData: PanelData, unsafePanelSize: number) => { const { onLayout } = committedValuesRef.current; const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; @@ -814,23 +639,23 @@ function PanelGroupWithForwardedRef({ (panelData) => panelData.constraints ); - const { groupSizePixels, panelSizePercentage, pivotIndices } = - panelDataHelper(groupId, panelDataArray, panelData, prevLayout); + const { panelSize, pivotIndices } = panelDataHelper( + panelDataArray, + panelData, + prevLayout + ); - const sizePercentage = getPercentageSizeFromMixedSizes( - mixedSizes, - groupSizePixels - )!; + assert(panelSize != null); const isLastPanel = - panelDataArray.indexOf(panelData) === panelDataArray.length - 1; + findPanelDataIndex(panelDataArray, panelData) === + panelDataArray.length - 1; const delta = isLastPanel - ? panelSizePercentage - sizePercentage - : sizePercentage - panelSizePercentage; + ? panelSize - unsafePanelSize + : unsafePanelSize - panelSize; const nextLayout = adjustLayoutByDelta({ delta, - groupSizePixels, layout: prevLayout, panelConstraints: panelConstraintsArray, pivotIndices, @@ -843,26 +668,17 @@ function PanelGroupWithForwardedRef({ eagerValuesRef.current.layout = nextLayout; if (onLayout) { - onLayout( - nextLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) - ); + onLayout(nextLayout); } callPanelCallbacks( - groupId, panelDataArray, nextLayout, - panelIdToLastNotifiedMixedSizesMapRef.current + panelIdToLastNotifiedSizeMapRef.current ); } }, - [groupId] + [] ); const startDragging = useCallback( @@ -870,7 +686,8 @@ function PanelGroupWithForwardedRef({ const { direction } = committedValuesRef.current; const { layout } = eagerValuesRef.current; - const handleElement = getResizeHandleElement(dragHandleId)!; + const handleElement = getResizeHandleElement(dragHandleId); + assert(handleElement); const initialCursorPosition = getResizeEventCursorPosition( direction, @@ -892,102 +709,21 @@ function PanelGroupWithForwardedRef({ setDragState(null); }, []); - const unregisterPanelRef = useRef<{ - pendingPanelIds: Set; - timeout: NodeJS.Timeout | null; - }>({ - pendingPanelIds: new Set(), - timeout: null, - }); const unregisterPanel = useCallback((panelData: PanelData) => { - const { id: groupId, onLayout } = committedValuesRef.current; - const { layout: prevLayout, panelDataArray } = eagerValuesRef.current; + const { panelDataArray } = eagerValuesRef.current; - const index = panelDataArray.indexOf(panelData); + const index = findPanelDataIndex(panelDataArray, panelData); if (index >= 0) { panelDataArray.splice(index, 1); - unregisterPanelRef.current.pendingPanelIds.add(panelData.id); - } - - if (unregisterPanelRef.current.timeout != null) { - clearTimeout(unregisterPanelRef.current.timeout); - } - - // Batch panel unmounts so that we only calculate layout once; - // This is more efficient and avoids misleading warnings in development mode. - // We can't check the DOM to detect this because Panel elements have not yet been removed. - unregisterPanelRef.current.timeout = setTimeout(() => { - const { pendingPanelIds } = unregisterPanelRef.current; - const map = panelIdToLastNotifiedMixedSizesMapRef.current; // TRICKY - // Strict effects mode - let unmountDueToStrictMode = false; - pendingPanelIds.forEach((panelId) => { - pendingPanelIds.delete(panelId); - - if (panelDataArray.find(({ id }) => id === panelId) == null) { - unmountDueToStrictMode = true; - - // TRICKY - // When a panel is removed from the group, we should delete the most recent prev-size entry for it. - // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted. - // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount. - delete map[panelData.id]; - } - }); - - if (!unmountDueToStrictMode) { - return; - } - - if (panelDataArray.length === 0) { - // The group is unmounting; skip layout calculation. - return; - } - - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); + // When a panel is removed from the group, we should delete the most recent prev-size entry for it. + // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted. + // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount. + delete panelIdToLastNotifiedSizeMapRef.current[panelData.id]; - let unsafeLayout: number[] = calculateUnsafeDefaultLayout({ - groupSizePixels, - panelDataArray, - }); - - // Validate even saved layouts in case something has changed since last render - // e.g. for pixel groups, this could be the size of the window - const nextLayout = validatePanelGroupLayout({ - groupSizePixels, - layout: unsafeLayout, - panelConstraints: panelDataArray.map( - (panelData) => panelData.constraints - ), - }); - - if (!areEqual(prevLayout, nextLayout)) { - setLayout(nextLayout); - - eagerValuesRef.current.layout = nextLayout; - - if (onLayout) { - onLayout( - nextLayout.map((sizePercentage) => ({ - sizePercentage, - sizePixels: convertPercentageToPixels( - sizePercentage, - groupSizePixels - ), - })) - ); - } - - callPanelCallbacks( - groupId, - panelDataArray, - nextLayout, - panelIdToLastNotifiedMixedSizesMapRef.current - ); - } - }, 0); + eagerValuesRef.current.panelDataArrayChanged = true; + } }, []); const context = useMemo( @@ -1039,6 +775,8 @@ function PanelGroupWithForwardedRef({ PanelGroupContext.Provider, { value: context }, createElement(Type, { + ...rest, + children, className: classNameFromProps, style: { @@ -1046,8 +784,6 @@ function PanelGroupWithForwardedRef({ ...styleFromProps, }, - ...dataAttributes, - // CSS selectors "data-panel-group": "", "data-panel-group-direction": direction, @@ -1066,8 +802,14 @@ export const PanelGroup = forwardRef< PanelGroupWithForwardedRef.displayName = "PanelGroup"; PanelGroup.displayName = "forwardRef(PanelGroup)"; +function findPanelDataIndex(panelDataArray: PanelData[], panelData: PanelData) { + return panelDataArray.findIndex( + (prevPanelData) => + prevPanelData === panelData || prevPanelData.id === panelData.id + ); +} + function panelDataHelper( - groupId: string, panelDataArray: PanelData[], panelData: PanelData, layout: number[] @@ -1076,34 +818,19 @@ function panelDataHelper( (panelData) => panelData.constraints ); - const panelIndex = panelDataArray.indexOf(panelData); + const panelIndex = findPanelDataIndex(panelDataArray, panelData); const panelConstraints = panelConstraintsArray[panelIndex]; - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); - - const percentagePanelConstraints = computePercentagePanelConstraints( - panelConstraintsArray, - panelIndex, - groupSizePixels - ); - const isLastPanel = panelIndex === panelDataArray.length - 1; const pivotIndices = isLastPanel ? [panelIndex - 1, panelIndex] : [panelIndex, panelIndex + 1]; - const panelSizePercentage = layout[panelIndex]; - const panelSizePixels = convertPercentageToPixels( - panelSizePercentage, - groupSizePixels - ); + const panelSize = layout[panelIndex]; return { - ...percentagePanelConstraints, - collapsible: panelConstraints.collapsible, - panelSizePercentage, - panelSizePixels, - groupSizePixels, + ...panelConstraints, + panelSize, pivotIndices, }; } diff --git a/packages/react-resizable-panels/src/PanelGroupContext.ts b/packages/react-resizable-panels/src/PanelGroupContext.ts index e6af4a066..094061344 100644 --- a/packages/react-resizable-panels/src/PanelGroupContext.ts +++ b/packages/react-resizable-panels/src/PanelGroupContext.ts @@ -1,5 +1,4 @@ import { PanelData } from "./Panel"; -import { MixedSizes } from "./types"; import { CSSProperties, createContext } from "./vendor/react"; export type ResizeEvent = KeyboardEvent | MouseEvent | TouchEvent; @@ -17,14 +16,14 @@ export const PanelGroupContext = createContext<{ direction: "horizontal" | "vertical"; dragState: DragState | null; expandPanel: (panelData: PanelData) => void; - getPanelSize: (panelData: PanelData) => MixedSizes; + getPanelSize: (panelData: PanelData) => number; getPanelStyle: (panelData: PanelData) => CSSProperties; groupId: string; isPanelCollapsed: (panelData: PanelData) => boolean; isPanelExpanded: (panelData: PanelData) => boolean; registerPanel: (panelData: PanelData) => void; registerResizeHandle: (dragHandleId: string) => ResizeHandler; - resizePanel: (panelData: PanelData, mixedSizes: Partial) => void; + resizePanel: (panelData: PanelData, size: number) => void; startDragging: (dragHandleId: string, event: ResizeEvent) => void; stopDragging: () => void; unregisterPanel: (panelData: PanelData) => void; diff --git a/packages/react-resizable-panels/src/PanelResizeHandle.test.tsx b/packages/react-resizable-panels/src/PanelResizeHandle.test.tsx new file mode 100644 index 000000000..aa61debbb --- /dev/null +++ b/packages/react-resizable-panels/src/PanelResizeHandle.test.tsx @@ -0,0 +1,74 @@ +import { Root, createRoot } from "react-dom/client"; +import { act } from "react-dom/test-utils"; +import { Panel, PanelGroup, PanelResizeHandle } from "."; +import { assert } from "./utils/assert"; +import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement"; + +describe("PanelResizeHandle", () => { + let expectedWarnings: string[] = []; + let root: Root; + + beforeEach(() => { + // @ts-expect-error + global.IS_REACT_ACT_ENVIRONMENT = true; + + const container = document.createElement("div"); + document.body.appendChild(container); + + expectedWarnings = []; + root = createRoot(container); + + jest.spyOn(console, "warn").mockImplementation((actualMessage: string) => { + const match = expectedWarnings.findIndex((expectedMessage) => { + return actualMessage.includes(expectedMessage); + }); + + if (match >= 0) { + expectedWarnings.splice(match, 1); + return; + } + + throw Error(`Unexpected warning: ${actualMessage}`); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + + act(() => { + root.unmount(); + }); + + expect(expectedWarnings).toHaveLength(0); + }); + + it("should support ...rest attributes", () => { + act(() => { + root.render( + + + + + + ); + }); + + const element = getResizeHandleElement("handle"); + assert(element); + expect(element.tabIndex).toBe(123); + expect(element.getAttribute("data-test-name")).toBe("foo"); + expect(element.title).toBe("bar"); + }); + + describe("callbacks", () => { + describe("onDragging", () => { + // TODO: Test this + }); + }); +}); diff --git a/packages/react-resizable-panels/src/PanelResizeHandle.ts b/packages/react-resizable-panels/src/PanelResizeHandle.ts index 520b61dce..b2942c01e 100644 --- a/packages/react-resizable-panels/src/PanelResizeHandle.ts +++ b/packages/react-resizable-panels/src/PanelResizeHandle.ts @@ -3,8 +3,9 @@ import { createElement, CSSProperties, ElementType, + HTMLAttributes, + PropsWithChildren, MouseEvent as ReactMouseEvent, - ReactNode, TouchEvent, useCallback, useContext, @@ -19,31 +20,32 @@ import { ResizeEvent, ResizeHandler, } from "./PanelGroupContext"; +import { assert } from "./utils/assert"; import { getCursorStyle } from "./utils/cursor"; -import { DataAttributes } from "./types"; export type PanelResizeHandleOnDragging = (isDragging: boolean) => void; -export type PanelResizeHandleProps = { - children?: ReactNode; - className?: string; - dataAttributes?: DataAttributes; - disabled?: boolean; - id?: string | null; - onDragging?: PanelResizeHandleOnDragging; - style?: CSSProperties; - tagName?: ElementType; -}; +export type PanelResizeHandleProps = Omit, "id"> & + PropsWithChildren<{ + className?: string; + disabled?: boolean; + id?: string | null; + onDragging?: PanelResizeHandleOnDragging; + style?: CSSProperties; + tabIndex?: number; + tagName?: ElementType; + }>; export function PanelResizeHandle({ children = null, className: classNameFromProps = "", - dataAttributes, disabled = false, - id: idFromProps = null, + id: idFromProps, onDragging, style: styleFromProps = {}, + tabIndex = 0, tagName: Type = "div", + ...rest }: PanelResizeHandleProps) { const divElementRef = useRef(null); @@ -83,8 +85,9 @@ export function PanelResizeHandle({ const stopDraggingAndBlur = useCallback(() => { // Clicking on the drag handle shouldn't leave it focused; // That would cause the PanelGroup to think it was still active. - const div = divElementRef.current!; - div.blur(); + const divElement = divElementRef.current; + assert(divElement); + divElement.blur(); stopDragging(); @@ -116,7 +119,9 @@ export function PanelResizeHandle({ resizeHandler(event); }; - const divElement = divElementRef.current!; + const divElement = divElementRef.current; + assert(divElement); + const targetDocument = divElement.ownerDocument; targetDocument.body.addEventListener("contextmenu", stopDraggingAndBlur); @@ -152,6 +157,8 @@ export function PanelResizeHandle({ }; return createElement(Type, { + ...rest, + children, className: classNameFromProps, onBlur: () => setIsFocused(false), @@ -159,7 +166,9 @@ export function PanelResizeHandle({ onMouseDown: (event: ReactMouseEvent) => { startDragging(resizeHandleId, event.nativeEvent); - const { onDragging } = callbacksRef.current!; + const callbacks = callbacksRef.current; + assert(callbacks); + const { onDragging } = callbacks; if (onDragging) { onDragging(true); } @@ -170,7 +179,9 @@ export function PanelResizeHandle({ onTouchStart: (event: TouchEvent) => { startDragging(resizeHandleId, event.nativeEvent); - const { onDragging } = callbacksRef.current!; + const callbacks = callbacksRef.current; + assert(callbacks); + const { onDragging } = callbacks; if (onDragging) { onDragging(true); } @@ -181,9 +192,7 @@ export function PanelResizeHandle({ ...style, ...styleFromProps, }, - tabIndex: 0, - - ...dataAttributes, + tabIndex, // CSS selectors "data-panel-group-direction": direction, diff --git a/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts b/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts index 37a1ac6b8..1d040ea1d 100644 --- a/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts +++ b/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts @@ -46,7 +46,8 @@ export function useWindowSplitterResizeHandlerBehavior({ case "F6": { event.preventDefault(); - const groupId = handleElement.getAttribute("data-panel-group-id")!; + const groupId = handleElement.getAttribute("data-panel-group-id"); + assert(groupId); const handles = getResizeHandleElementsForGroup(groupId); const index = getResizeHandleElementIndex(groupId, handleId); diff --git a/packages/react-resizable-panels/src/hooks/useWindowSplitterPanelGroupBehavior.ts b/packages/react-resizable-panels/src/hooks/useWindowSplitterPanelGroupBehavior.ts index 866f6954d..8cd38f564 100644 --- a/packages/react-resizable-panels/src/hooks/useWindowSplitterPanelGroupBehavior.ts +++ b/packages/react-resizable-panels/src/hooks/useWindowSplitterPanelGroupBehavior.ts @@ -5,12 +5,9 @@ import { adjustLayoutByDelta } from "../utils/adjustLayoutByDelta"; import { assert } from "../utils/assert"; import { calculateAriaValues } from "../utils/calculateAriaValues"; import { determinePivotIndices } from "../utils/determinePivotIndices"; -import { calculateAvailablePanelSizeInPixels } from "../utils/dom/calculateAvailablePanelSizeInPixels"; -import { getAvailableGroupSizePixels } from "../utils/dom/getAvailableGroupSizePixels"; import { getPanelGroupElement } from "../utils/dom/getPanelGroupElement"; import { getResizeHandleElementsForGroup } from "../utils/dom/getResizeHandleElementsForGroup"; import { getResizeHandlePanelIds } from "../utils/dom/getResizeHandlePanelIds"; -import { getPercentageSizeFromMixedSizes } from "../utils/getPercentageSizeFromMixedSizes"; import { fuzzyNumbersEqual } from "../utils/numbers/fuzzyNumbersEqual"; import { RefObject, useEffect, useRef } from "../vendor/react"; import useIsomorphicLayoutEffect from "./useIsomorphicEffect"; @@ -43,12 +40,10 @@ export function useWindowSplitterPanelGroupBehavior({ }); useIsomorphicLayoutEffect(() => { - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); const resizeHandleElements = getResizeHandleElementsForGroup(groupId); for (let index = 0; index < panelDataArray.length - 1; index++) { const { valueMax, valueMin, valueNow } = calculateAriaValues({ - groupSizePixels, layout, panelsArray: panelDataArray, pivotIndices: [index, index + 1], @@ -68,10 +63,10 @@ export function useWindowSplitterPanelGroupBehavior({ } } } else { - resizeHandleElement.setAttribute( - "aria-controls", - panelDataArray[index].id - ); + const panelData = panelDataArray[index]; + assert(panelData); + + resizeHandleElement.setAttribute("aria-controls", panelData.id); resizeHandleElement.setAttribute( "aria-valuemax", "" + Math.round(valueMax) @@ -82,7 +77,7 @@ export function useWindowSplitterPanelGroupBehavior({ ); resizeHandleElement.setAttribute( "aria-valuenow", - "" + Math.round(valueNow) + valueNow != null ? "" + Math.round(valueNow) : "" ); } } @@ -98,14 +93,20 @@ export function useWindowSplitterPanelGroupBehavior({ }, [groupId, layout, panelDataArray]); useEffect(() => { - const { panelDataArray } = eagerValuesRef.current!; + const eagerValues = eagerValuesRef.current; + assert(eagerValues); + + const { panelDataArray } = eagerValues; const groupElement = getPanelGroupElement(groupId); assert(groupElement != null, `No group found for id "${groupId}"`); const handles = getResizeHandleElementsForGroup(groupId); + assert(handles); + const cleanupFunctions = handles.map((handle) => { - const handleId = handle.getAttribute("data-panel-resize-handle-id")!; + const handleId = handle.getAttribute("data-panel-resize-handle-id"); + assert(handleId); const [idBefore, idAfter] = getResizeHandlePanelIds( groupId, @@ -130,33 +131,21 @@ export function useWindowSplitterPanelGroupBehavior({ ); if (index >= 0) { const panelData = panelDataArray[index]; + assert(panelData); + const size = layout[index]; - if (size != null && panelData.constraints.collapsible) { - const groupSizePixels = getAvailableGroupSizePixels(groupId); - - const collapsedSize = - getPercentageSizeFromMixedSizes( - { - sizePercentage: - panelData.constraints.collapsedSizePercentage, - sizePixels: panelData.constraints.collapsedSizePixels, - }, - groupSizePixels - ) ?? 0; - const minSize = - getPercentageSizeFromMixedSizes( - { - sizePercentage: panelData.constraints.minSizePercentage, - sizePixels: panelData.constraints.minSizePixels, - }, - groupSizePixels - ) ?? 0; + const { + collapsedSize = 0, + collapsible, + minSize = 0, + } = panelData.constraints; + + if (size != null && collapsible) { const nextLayout = adjustLayoutByDelta({ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size, - groupSizePixels, layout, panelConstraints: panelDataArray.map( (panelData) => panelData.constraints diff --git a/packages/react-resizable-panels/src/index.ts b/packages/react-resizable-panels/src/index.ts index e3dc264ba..e369ca833 100644 --- a/packages/react-resizable-panels/src/index.ts +++ b/packages/react-resizable-panels/src/index.ts @@ -1,8 +1,7 @@ import { Panel } from "./Panel"; import { PanelGroup } from "./PanelGroup"; import { PanelResizeHandle } from "./PanelResizeHandle"; - -import type { MixedSizes } from "./types"; +import { assert } from "./utils/assert"; import type { ImperativePanelHandle, @@ -26,7 +25,6 @@ export { // TypeScript types ImperativePanelGroupHandle, ImperativePanelHandle, - MixedSizes, PanelGroupOnLayout, PanelGroupProps, PanelGroupStorage, @@ -37,6 +35,9 @@ export { PanelResizeHandleOnDragging, PanelResizeHandleProps, + // Utiltiy methods + assert, + // React components Panel, PanelGroup, diff --git a/packages/react-resizable-panels/src/types.ts b/packages/react-resizable-panels/src/types.ts index 487245b25..ef9ffc8ce 100644 --- a/packages/react-resizable-panels/src/types.ts +++ b/packages/react-resizable-panels/src/types.ts @@ -1,13 +1,4 @@ export type Direction = "horizontal" | "vertical"; -export type MixedSizes = { - sizePercentage: number; - sizePixels: number; -}; - export type ResizeEvent = KeyboardEvent | MouseEvent | TouchEvent; export type ResizeHandler = (event: ResizeEvent) => void; - -export type DataAttributes = { - [attribute: string]: string | number | boolean | undefined; -}; diff --git a/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.test.ts b/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.test.ts index 37e6a3027..5541eed48 100644 --- a/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.test.ts +++ b/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.test.ts @@ -5,7 +5,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [{}, {}], pivotIndices: [0, 1], @@ -18,7 +17,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [{}, {}], pivotIndices: [0, 1], @@ -28,7 +26,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [{}, {}], pivotIndices: [0, 1], @@ -41,16 +38,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ { - minSizePercentage: 20, - maxSizePercentage: 60, + minSize: 20, + maxSize: 60, }, { - minSizePercentage: 10, - maxSizePercentage: 90, + minSize: 10, + maxSize: 90, }, ], pivotIndices: [0, 1], @@ -63,14 +59,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, ], pivotIndices: [0, 1], @@ -83,14 +78,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 40, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, ], pivotIndices: [0, 1], @@ -105,13 +99,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 5, - groupSizePixels: NaN, layout: [10, 90], panelConstraints: [ { - collapsedSizePercentage: 10, + collapsedSize: 10, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, {}, ], @@ -127,13 +120,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 5, - groupSizePixels: NaN, layout: [10, 90], panelConstraints: [ { - collapsedSizePercentage: 10, + collapsedSize: 10, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, {}, ], @@ -149,13 +141,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 5, - groupSizePixels: NaN, layout: [75, 25], panelConstraints: [ {}, { collapsible: true, - minSizePercentage: 25, + minSize: 25, }, ], pivotIndices: [0, 1], @@ -167,22 +158,20 @@ describe("adjustLayoutByDelta", () => { // Edge case // Expanding from a collapsed state to less than the min size via imperative API should do nothing it("[1++,2]", () => { - // collapsed 4%, min size 6%, max size 15% expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 1_000, layout: [4, 96], panelConstraints: [ { - collapsedSizePixels: 40, + collapsedSize: 4, collapsible: true, - defaultSizePixels: 150, - maxSizePixels: 150, - minSizePixels: 60, + defaultSize: 15, + maxSize: 15, + minSize: 6, }, { - minSizePercentage: 50, + minSize: 5, }, ], pivotIndices: [0, 1], @@ -194,23 +183,20 @@ describe("adjustLayoutByDelta", () => { // Edge case // Expanding from a collapsed state to less than the min size via keyboard should snap to min size it("[1++,2]", () => { - // collapsed 4%, min size 6%, max size 15% - expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 1_000, layout: [4, 96], panelConstraints: [ { - collapsedSizePixels: 40, + collapsedSize: 4, collapsible: true, - defaultSizePixels: 150, - maxSizePixels: 150, - minSizePixels: 60, + defaultSize: 15, + maxSize: 15, + minSize: 6, }, { - minSizePercentage: 50, + minSize: 5, }, ], pivotIndices: [0, 1], @@ -222,22 +208,20 @@ describe("adjustLayoutByDelta", () => { // Edge case // Expanding from a collapsed state to greater than the max size it("[1++,2]", () => { - // collapsed 4%, min size 6%, max size 15% expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 1_000, layout: [4, 96], panelConstraints: [ { - collapsedSizePixels: 40, + collapsedSize: 4, collapsible: true, - defaultSizePixels: 150, - maxSizePixels: 150, - minSizePixels: 60, + defaultSize: 15, + maxSize: 15, + minSize: 6, }, { - minSizePercentage: 50, + minSize: 5, }, ], pivotIndices: [0, 1], @@ -252,17 +236,16 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [5, 95], panelConstraints: [ { - collapsedSizePixels: 5, + collapsedSize: 5, collapsible: true, - maxSizePixels: 50, - minSizePixels: 25, + maxSize: 50, + minSize: 25, }, { - minSizePercentage: 50, + minSize: 50, }, ], pivotIndices: [0, 1], @@ -277,17 +260,16 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [5, 95], panelConstraints: [ { - collapsedSizePixels: 5, + collapsedSize: 5, collapsible: true, - maxSizePixels: 50, - minSizePixels: 25, + maxSize: 50, + minSize: 25, }, { - minSizePercentage: 50, + minSize: 50, }, ], pivotIndices: [0, 1], @@ -302,14 +284,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [0, 100], panelConstraints: [ { - collapsedSizePixels: 0, + collapsedSize: 0, collapsible: true, - maxSizePixels: 50, - minSizePixels: 0, + maxSize: 50, + minSize: 0, }, {}, ], @@ -323,7 +304,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -1, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [{}, {}], pivotIndices: [0, 1], @@ -336,7 +316,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -25, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [{}, {}], pivotIndices: [0, 1], @@ -349,7 +328,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [{}, {}], pivotIndices: [0, 1], @@ -362,16 +340,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ { - minSizePercentage: 20, - maxSizePercentage: 60, + minSize: 20, + maxSize: 60, }, { - minSizePercentage: 10, - maxSizePercentage: 90, + minSize: 10, + maxSize: 90, }, ], pivotIndices: [0, 1], @@ -384,13 +361,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -25, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, {}, ], @@ -404,13 +380,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -30, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, {}, ], @@ -422,13 +397,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -36, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, {}, ], @@ -444,15 +418,14 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -30, - groupSizePixels: 100, layout: [50, 50], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, - { maxSizePercentage: 80 }, + { maxSize: 80 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -466,14 +439,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -5, - groupSizePixels: NaN, layout: [90, 10], panelConstraints: [ {}, { - collapsedSizePercentage: 10, + collapsedSize: 10, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, ], pivotIndices: [0, 1], @@ -488,13 +460,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -5, - groupSizePixels: NaN, layout: [25, 75], panelConstraints: [ { - collapsedSizePercentage: 10, + collapsedSize: 10, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, {}, ], @@ -508,7 +479,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [0, 1], @@ -521,7 +491,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [0, 1], @@ -534,7 +503,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [0, 1], @@ -547,7 +515,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 75, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [0, 1], @@ -560,13 +527,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [ - { maxSizePercentage: 35 }, - { minSizePercentage: 25 }, - {}, - ], + panelConstraints: [{ maxSize: 35 }, { minSize: 25 }, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -578,13 +540,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [ - { maxSizePercentage: 35 }, - { minSizePercentage: 25 }, - {}, - ], + panelConstraints: [{ maxSize: 35 }, { minSize: 25 }, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -595,16 +552,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 5, - groupSizePixels: 100, layout: [25, 40, 35], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, - { minSizePercentage: 25 }, + { minSize: 25 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -616,16 +572,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 26, - groupSizePixels: 100, layout: [25, 40, 35], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, - { minSizePercentage: 25 }, + { minSize: 25 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -637,16 +592,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 80, - groupSizePixels: 100, layout: [25, 40, 35], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 25, + minSize: 25, }, - { minSizePercentage: 25 }, + { minSize: 25 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -658,7 +612,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -1, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [0, 1], @@ -671,7 +624,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -25, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [0, 1], @@ -684,9 +636,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -1, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [{ minSizePercentage: 20 }, {}, {}], + panelConstraints: [{ minSize: 20 }, {}, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -697,9 +648,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [{ minSizePercentage: 20 }, {}, {}], + panelConstraints: [{ minSize: 20 }, {}, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -710,14 +660,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -5, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [ { // Implied min size 10 }, - { maxSizePercentage: 70 }, - { maxSizePercentage: 20 }, + { maxSize: 70 }, + { maxSize: 20 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -729,14 +678,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -20, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [ { // Implied min size 10 }, - { maxSizePercentage: 70 }, - { maxSizePercentage: 20 }, + { maxSize: 70 }, + { maxSize: 20 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -748,13 +696,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePixels: 15, + minSize: 15, }, {}, {}, @@ -769,13 +716,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -20, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePixels: 15, + minSize: 15, }, {}, {}, @@ -790,17 +736,16 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -20, - groupSizePixels: 100, layout: [45, 50, 5], panelConstraints: [ {}, { - maxSizePercentage: 50, + maxSize: 50, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePixels: 15, + minSize: 15, }, ], pivotIndices: [0, 1], @@ -813,7 +758,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -1, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [1, 2], @@ -826,7 +770,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -25, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [1, 2], @@ -839,7 +782,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [1, 2], @@ -852,7 +794,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -75, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [1, 2], @@ -865,9 +806,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 5, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [{}, {}, { minSizePercentage: 15 }], + panelConstraints: [{}, {}, { minSize: 15 }], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -878,9 +818,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 20, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [{}, {}, { minSizePercentage: 15 }], + panelConstraints: [{}, {}, { minSize: 15 }], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -891,13 +830,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 5, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [ - {}, - {}, - { collapsible: true, minSizePercentage: 20 }, - ], + panelConstraints: [{}, {}, { collapsible: true, minSize: 20 }], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -908,13 +842,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 10, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [ - {}, - {}, - { collapsible: true, minSizePercentage: 20 }, - ], + panelConstraints: [{}, {}, { collapsible: true, minSize: 20 }], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -923,13 +852,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 16, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [ - {}, - {}, - { collapsible: true, minSizePercentage: 20 }, - ], + panelConstraints: [{}, {}, { collapsible: true, minSize: 20 }], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -940,7 +864,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [1, 2], @@ -953,7 +876,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [{}, {}, {}], pivotIndices: [1, 2], @@ -966,9 +888,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -20, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [{}, { minSizePercentage: 40 }, {}], + panelConstraints: [{}, { minSize: 40 }, {}], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -979,9 +900,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 50, 25], - panelConstraints: [{}, {}, { maxSizePercentage: 30 }], + panelConstraints: [{}, {}, { maxSize: 30 }], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -992,14 +912,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -35, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, ], @@ -1011,14 +930,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -40, - groupSizePixels: 100, layout: [25, 50, 25], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, ], @@ -1032,13 +950,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 0, 75], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, {}, @@ -1051,13 +968,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -20, - groupSizePixels: 100, layout: [25, 0, 75], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, {}, @@ -1068,11 +984,23 @@ describe("adjustLayoutByDelta", () => { ).toEqual([5, 0, 95]); }); + // Edge case + it("[1,2--,3]", () => { + expect( + adjustLayoutByDelta({ + delta: -100, + layout: [100 / 3, 100 / 3, 100 / 3], + panelConstraints: [{}, {}, {}], + pivotIndices: [1, 2], + trigger: "mouse-or-touch", + }) + ).toEqual([0, 0, 100]); + }); + it("[1++,2,3,4]", () => { expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [0, 1], @@ -1085,7 +1013,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [0, 1], @@ -1098,7 +1025,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [0, 1], @@ -1111,7 +1037,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 75, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [0, 1], @@ -1124,9 +1049,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 25, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{ maxSizePercentage: 35 }, {}, {}, {}], + panelConstraints: [{ maxSize: 35 }, {}, {}, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -1137,13 +1061,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 100, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, - { minSizePercentage: 10 }, - { minSizePercentage: 10 }, - { minSizePercentage: 10 }, + { minSize: 10 }, + { minSize: 10 }, + { minSize: 10 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -1155,24 +1078,23 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], pivotIndices: [0, 1], @@ -1183,24 +1105,23 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 15, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], pivotIndices: [0, 1], @@ -1213,24 +1134,23 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 40, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], pivotIndices: [0, 1], @@ -1243,24 +1163,23 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 100, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], pivotIndices: [0, 1], @@ -1273,7 +1192,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -1, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [0, 1], @@ -1286,7 +1204,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -25, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [0, 1], @@ -1299,9 +1216,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{ minSizePercentage: 20 }, {}, {}, {}], + panelConstraints: [{ minSize: 20 }, {}, {}, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -1312,9 +1228,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -25, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, { maxSizePercentage: 35 }, {}, {}], + panelConstraints: [{}, { maxSize: 35 }, {}, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -1325,13 +1240,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, {}, @@ -1345,13 +1259,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -15, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, {}, @@ -1367,15 +1280,14 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, - { maxSizePercentage: 35 }, + { maxSize: 35 }, {}, {}, ], @@ -1387,15 +1299,14 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -15, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, - { maxSizePercentage: 35 }, + { maxSize: 35 }, {}, {}, ], @@ -1415,15 +1326,14 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, - { maxSizePercentage: 30 }, + { maxSize: 30 }, {}, {}, ], @@ -1443,16 +1353,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, - { maxSizePercentage: 30 }, - { maxSizePercentage: 35 }, + { maxSize: 30 }, + { maxSize: 35 }, {}, ], pivotIndices: [0, 1], @@ -1467,17 +1376,16 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, - { maxSizePercentage: 35 }, - { maxSizePercentage: 35 }, - { maxSizePercentage: 35 }, + { maxSize: 35 }, + { maxSize: 35 }, + { maxSize: 35 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -1490,17 +1398,16 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -20, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, - { maxSizePercentage: 35 }, - { maxSizePercentage: 35 }, - { maxSizePercentage: 35 }, + { maxSize: 35 }, + { maxSize: 35 }, + { maxSize: 35 }, ], pivotIndices: [0, 1], trigger: "imperative-api", @@ -1512,7 +1419,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [1, 2], @@ -1525,7 +1431,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [1, 2], @@ -1538,7 +1443,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [1, 2], @@ -1551,9 +1455,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, { maxSizePercentage: 35 }, {}, {}], + panelConstraints: [{}, { maxSize: 35 }, {}, {}], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -1564,9 +1467,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, {}, { minSizePercentage: 20 }, {}], + panelConstraints: [{}, {}, { minSize: 20 }, {}], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -1577,16 +1479,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, {}, {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 10, + minSize: 10, }, ], pivotIndices: [1, 2], @@ -1599,15 +1500,14 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 10, + minSize: 10, }, {}, ], @@ -1621,17 +1521,16 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 10, + minSize: 10, }, - { minSizePercentage: 10 }, + { minSize: 10 }, ], pivotIndices: [1, 2], trigger: "imperative-api", @@ -1643,7 +1542,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -25, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [1, 2], @@ -1656,7 +1554,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [1, 2], @@ -1669,9 +1566,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, { minSizePercentage: 20 }, {}, {}], + panelConstraints: [{}, { minSize: 20 }, {}, {}], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -1682,9 +1578,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{ minSizePercentage: 20 }, {}, {}, {}], + panelConstraints: [{ minSize: 20 }, {}, {}, {}], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -1695,14 +1590,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [ - { minSizePercentage: 20 }, - { minSizePercentage: 20 }, - {}, - {}, - ], + panelConstraints: [{ minSize: 20 }, { minSize: 20 }, {}, {}], pivotIndices: [1, 2], trigger: "imperative-api", }) @@ -1713,13 +1602,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -5, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, {}, @@ -1735,13 +1623,12 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, {}, @@ -1757,14 +1644,13 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, {}, @@ -1779,7 +1665,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [2, 3], @@ -1792,7 +1677,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [2, 3], @@ -1805,9 +1689,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, {}, { maxSizePercentage: 40 }, {}], + panelConstraints: [{}, {}, { maxSize: 40 }, {}], pivotIndices: [2, 3], trigger: "imperative-api", }) @@ -1818,9 +1701,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 30, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, {}, {}, { minSizePercentage: 10 }], + panelConstraints: [{}, {}, {}, { minSize: 10 }], pivotIndices: [2, 3], trigger: "imperative-api", }) @@ -1831,16 +1713,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 5, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, {}, {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], pivotIndices: [2, 3], @@ -1853,16 +1734,15 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ {}, {}, {}, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], pivotIndices: [2, 3], @@ -1875,7 +1755,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -10, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [2, 3], @@ -1888,7 +1767,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -40, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [2, 3], @@ -1901,7 +1779,6 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -100, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [{}, {}, {}, {}], pivotIndices: [2, 3], @@ -1914,12 +1791,11 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ - { minSizePercentage: 10 }, - { minSizePercentage: 10 }, - { minSizePercentage: 10 }, + { minSize: 10 }, + { minSize: 10 }, + { minSize: 10 }, {}, ], pivotIndices: [2, 3], @@ -1932,9 +1808,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, {}, {}, { maxSizePercentage: 40 }], + panelConstraints: [{}, {}, {}, { maxSize: 40 }], pivotIndices: [2, 3], trigger: "imperative-api", }) @@ -1945,9 +1820,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -50, - groupSizePixels: 100, layout: [25, 25, 25, 25], - panelConstraints: [{}, { minSizePercentage: 5 }, {}, {}], + panelConstraints: [{}, { minSize: 5 }, {}, {}], pivotIndices: [2, 3], trigger: "imperative-api", }) @@ -1958,23 +1832,22 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -100, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, {}, ], @@ -1988,19 +1861,18 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: -100, - groupSizePixels: 100, layout: [25, 25, 25, 25], panelConstraints: [ { - minSizePercentage: 20, + minSize: 20, }, { - collapsedSizePercentage: 5, + collapsedSize: 5, collapsible: true, - minSizePercentage: 20, + minSize: 20, }, { - minSizePercentage: 20, + minSize: 20, }, {}, ], @@ -2015,9 +1887,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 100, layout: [50, 50], - panelConstraints: [{ maxSizePercentage: 50 }, {}], + panelConstraints: [{ maxSize: 50 }, {}], pivotIndices: [0, 1], trigger: "imperative-api", }) @@ -2026,9 +1897,8 @@ describe("adjustLayoutByDelta", () => { expect( adjustLayoutByDelta({ delta: 1, - groupSizePixels: 100, layout: [50, 50], - panelConstraints: [{}, { minSizePercentage: 50 }], + panelConstraints: [{}, { minSize: 50 }], pivotIndices: [0, 1], trigger: "imperative-api", }) diff --git a/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.ts b/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.ts index 13cb3a3e8..330ba7729 100644 --- a/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.ts +++ b/packages/react-resizable-panels/src/utils/adjustLayoutByDelta.ts @@ -1,5 +1,5 @@ import { PanelConstraints } from "../Panel"; -import { computePercentagePanelConstraints } from "./computePercentagePanelConstraints"; +import { assert } from "./assert"; import { fuzzyCompareNumbers } from "./numbers/fuzzyCompareNumbers"; import { fuzzyNumbersEqual } from "./numbers/fuzzyNumbersEqual"; import { resizePanel } from "./resizePanel"; @@ -7,14 +7,12 @@ import { resizePanel } from "./resizePanel"; // All units must be in percentages; pixel values should be pre-converted export function adjustLayoutByDelta({ delta, - groupSizePixels, layout: prevLayout, - panelConstraints, + panelConstraints: panelConstraintsArray, pivotIndices, trigger, }: { delta: number; - groupSizePixels: number; layout: number[]; panelConstraints: PanelConstraints[]; pivotIndices: number[]; @@ -26,6 +24,10 @@ export function adjustLayoutByDelta({ const nextLayout = [...prevLayout]; + const [firstPivotIndex, secondPivotIndex] = pivotIndices; + assert(firstPivotIndex != null); + assert(secondPivotIndex != null); + let deltaApplied = 0; //const DEBUG = []; @@ -49,21 +51,22 @@ export function adjustLayoutByDelta({ if (trigger === "keyboard") { { // Check if we should expand a collapsed panel - const index = delta < 0 ? pivotIndices[1]! : pivotIndices[0]!; - const constraints = panelConstraints[index]!; + const index = delta < 0 ? secondPivotIndex : firstPivotIndex; + const panelConstraints = panelConstraintsArray[index]; + assert(panelConstraints); + //DEBUG.push(`edge case check 1: ${index}`); //DEBUG.push(` -> collapsible? ${constraints.collapsible}`); - if (constraints.collapsible) { - const prevSize = prevLayout[index]!; - const { collapsedSizePercentage, minSizePercentage } = - computePercentagePanelConstraints( - panelConstraints, - index, - groupSizePixels - ); - - if (fuzzyNumbersEqual(prevSize, collapsedSizePercentage)) { - const localDelta = minSizePercentage - prevSize; + if (panelConstraints.collapsible) { + const prevSize = prevLayout[index]; + assert(prevSize != null); + + const panelConstraints = panelConstraintsArray[index]; + assert(panelConstraints); + const { collapsedSize = 0, minSize = 0 } = panelConstraints; + + if (fuzzyNumbersEqual(prevSize, collapsedSize)) { + const localDelta = minSize - prevSize; //DEBUG.push(` -> expand delta: ${localDelta}`); if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) { @@ -76,21 +79,23 @@ export function adjustLayoutByDelta({ { // Check if we should collapse a panel at its minimum size - const index = delta < 0 ? pivotIndices[0]! : pivotIndices[1]!; - const constraints = panelConstraints[index]!; + const index = delta < 0 ? firstPivotIndex : secondPivotIndex; + const panelConstraints = panelConstraintsArray[index]; + assert(panelConstraints); + const { collapsible } = panelConstraints; + //DEBUG.push(`edge case check 2: ${index}`); - //DEBUG.push(` -> collapsible? ${constraints.collapsible}`); - if (constraints.collapsible) { - const prevSize = prevLayout[index]!; - const { collapsedSizePercentage, minSizePercentage } = - computePercentagePanelConstraints( - panelConstraints, - index, - groupSizePixels - ); - - if (fuzzyNumbersEqual(prevSize, minSizePercentage)) { - const localDelta = prevSize - collapsedSizePercentage; + //DEBUG.push(` -> collapsible? ${collapsible}`); + if (collapsible) { + const prevSize = prevLayout[index]; + assert(prevSize != null); + + const panelConstraints = panelConstraintsArray[index]; + assert(panelConstraints); + const { collapsedSize = 0, minSize = 0 } = panelConstraints; + + if (fuzzyNumbersEqual(prevSize, minSize)) { + const localDelta = prevSize - collapsedSize; //DEBUG.push(` -> expand delta: ${localDelta}`); if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) { @@ -113,15 +118,16 @@ export function adjustLayoutByDelta({ const increment = delta < 0 ? 1 : -1; - let index = delta < 0 ? pivotIndices[1]! : pivotIndices[0]!; + let index = delta < 0 ? secondPivotIndex : firstPivotIndex; let maxAvailableDelta = 0; //DEBUG.push("pre calc..."); while (true) { const prevSize = prevLayout[index]; + assert(prevSize != null); + const maxSafeSize = resizePanel({ - groupSizePixels, - panelConstraints, + panelConstraints: panelConstraintsArray, panelIndex: index, size: 100, }); @@ -131,7 +137,7 @@ export function adjustLayoutByDelta({ maxAvailableDelta += delta; index += increment; - if (index < 0 || index >= panelConstraints.length) { + if (index < 0 || index >= panelConstraintsArray.length) { break; } } @@ -146,16 +152,17 @@ export function adjustLayoutByDelta({ { // Delta added to a panel needs to be subtracted from other panels (within the constraints that those panels allow). - const pivotIndex = delta < 0 ? pivotIndices[0]! : pivotIndices[1]!; + const pivotIndex = delta < 0 ? firstPivotIndex : secondPivotIndex; let index = pivotIndex; - while (index >= 0 && index < panelConstraints.length) { + while (index >= 0 && index < panelConstraintsArray.length) { const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied); - const prevSize = prevLayout[index]!; + const prevSize = prevLayout[index]; + assert(prevSize != null); + const unsafeSize = prevSize - deltaRemaining; const safeSize = resizePanel({ - groupSizePixels, - panelConstraints, + panelConstraints: panelConstraintsArray, panelIndex: index, size: unsafeSize, }); @@ -196,12 +203,14 @@ export function adjustLayoutByDelta({ { // Now distribute the applied delta to the panels in the other direction - const pivotIndex = delta < 0 ? pivotIndices[1]! : pivotIndices[0]!; + const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex; - const unsafeSize = prevLayout[pivotIndex]! + deltaApplied; + const prevSize = prevLayout[pivotIndex]; + assert(prevSize != null); + + const unsafeSize = prevSize + deltaApplied; const safeSize = resizePanel({ - groupSizePixels, - panelConstraints, + panelConstraints: panelConstraintsArray, panelIndex: pivotIndex, size: unsafeSize, }); @@ -213,14 +222,15 @@ export function adjustLayoutByDelta({ if (!fuzzyNumbersEqual(safeSize, unsafeSize)) { let deltaRemaining = unsafeSize - safeSize; - const pivotIndex = delta < 0 ? pivotIndices[1]! : pivotIndices[0]!; + const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex; let index = pivotIndex; - while (index >= 0 && index < panelConstraints.length) { - const prevSize = nextLayout[index]!; + while (index >= 0 && index < panelConstraintsArray.length) { + const prevSize = nextLayout[index]; + assert(prevSize != null); + const unsafeSize = prevSize + deltaRemaining; const safeSize = resizePanel({ - groupSizePixels, - panelConstraints, + panelConstraints: panelConstraintsArray, panelIndex: index, size: unsafeSize, }); @@ -248,9 +258,7 @@ export function adjustLayoutByDelta({ //DEBUG.push(""); const totalSize = nextLayout.reduce((total, size) => size + total, 0); - deltaApplied = 100 - totalSize; //DEBUG.push(`total size: ${totalSize}`); - //DEBUG.push(` deltaApplied: ${deltaApplied}`); //console.log(DEBUG.join("\n")); if (!fuzzyNumbersEqual(totalSize, 100)) { diff --git a/packages/react-resizable-panels/src/utils/assert.ts b/packages/react-resizable-panels/src/utils/assert.ts index 8b6014ee5..e3a02ff73 100644 --- a/packages/react-resizable-panels/src/utils/assert.ts +++ b/packages/react-resizable-panels/src/utils/assert.ts @@ -1,5 +1,5 @@ export function assert( - expectedCondition: boolean, + expectedCondition: any, message: string = "Assertion failed!" ): asserts expectedCondition { if (!expectedCondition) { diff --git a/packages/react-resizable-panels/src/utils/calculateAriaValues.test.ts b/packages/react-resizable-panels/src/utils/calculateAriaValues.test.ts index 22bfa418b..c907169c6 100644 --- a/packages/react-resizable-panels/src/utils/calculateAriaValues.test.ts +++ b/packages/react-resizable-panels/src/utils/calculateAriaValues.test.ts @@ -27,7 +27,6 @@ describe("calculateAriaValues", () => { it("should work correctly for panels with no min/max constraints", () => { expect( calculateAriaValues({ - groupSizePixels: 1_000, layout: [50, 50], panelsArray: [createPanelData(), createPanelData()], pivotIndices: [0, 1], @@ -40,7 +39,6 @@ describe("calculateAriaValues", () => { expect( calculateAriaValues({ - groupSizePixels: 1_000, layout: [20, 50, 30], panelsArray: [createPanelData(), createPanelData(), createPanelData()], pivotIndices: [0, 1], @@ -53,7 +51,6 @@ describe("calculateAriaValues", () => { expect( calculateAriaValues({ - groupSizePixels: 1_000, layout: [20, 50, 30], panelsArray: [createPanelData(), createPanelData(), createPanelData()], pivotIndices: [1, 2], @@ -68,12 +65,11 @@ describe("calculateAriaValues", () => { it("should work correctly for panels with min/max constraints", () => { expect( calculateAriaValues({ - groupSizePixels: 1_000, layout: [25, 75], panelsArray: [ createPanelData({ - maxSizePercentage: 35, - minSizePercentage: 10, + maxSize: 35, + minSize: 10, }), createPanelData(), ], @@ -87,17 +83,16 @@ describe("calculateAriaValues", () => { expect( calculateAriaValues({ - groupSizePixels: 1_000, layout: [25, 50, 25], panelsArray: [ createPanelData({ - maxSizePercentage: 35, - minSizePercentage: 10, + maxSize: 35, + minSize: 10, }), createPanelData(), createPanelData({ - maxSizePercentage: 35, - minSizePercentage: 10, + maxSize: 35, + minSize: 10, }), ], pivotIndices: [1, 2], diff --git a/packages/react-resizable-panels/src/utils/calculateAriaValues.ts b/packages/react-resizable-panels/src/utils/calculateAriaValues.ts index 7b6ae2965..0c3186887 100644 --- a/packages/react-resizable-panels/src/utils/calculateAriaValues.ts +++ b/packages/react-resizable-panels/src/utils/calculateAriaValues.ts @@ -1,13 +1,11 @@ import { PanelData } from "../Panel"; -import { getPercentageSizeFromMixedSizes } from "./getPercentageSizeFromMixedSizes"; +import { assert } from "./assert"; export function calculateAriaValues({ - groupSizePixels, layout, panelsArray, pivotIndices, }: { - groupSizePixels: number; layout: number[]; panelsArray: PanelData[]; pivotIndices: number[]; @@ -17,35 +15,15 @@ export function calculateAriaValues({ let totalMinSize = 0; let totalMaxSize = 0; + const firstIndex = pivotIndices[0]; + assert(firstIndex != null); + // A panel's effective min/max sizes also need to account for other panel's sizes. panelsArray.forEach((panelData, index) => { const { constraints } = panelData; - const { - maxSizePercentage, - maxSizePixels, - minSizePercentage, - minSizePixels, - } = constraints; - - const minSize = - getPercentageSizeFromMixedSizes( - { - sizePercentage: minSizePercentage, - sizePixels: minSizePixels, - }, - groupSizePixels - ) ?? 0; - - const maxSize = - getPercentageSizeFromMixedSizes( - { - sizePercentage: maxSizePercentage, - sizePixels: maxSizePixels, - }, - groupSizePixels - ) ?? 100; + const { maxSize = 100, minSize = 0 } = constraints; - if (index === pivotIndices[0]) { + if (index === firstIndex) { currentMinSize = minSize; currentMaxSize = maxSize; } else { @@ -57,7 +35,7 @@ export function calculateAriaValues({ const valueMax = Math.min(currentMaxSize, 100 - totalMinSize); const valueMin = Math.max(currentMinSize, 100 - totalMaxSize); - const valueNow = layout[pivotIndices[0]]; + const valueNow = layout[firstIndex]; return { valueMax, diff --git a/packages/react-resizable-panels/src/utils/calculateDeltaPercentage.ts b/packages/react-resizable-panels/src/utils/calculateDeltaPercentage.ts index 6a847d22a..de53f6d92 100644 --- a/packages/react-resizable-panels/src/utils/calculateDeltaPercentage.ts +++ b/packages/react-resizable-panels/src/utils/calculateDeltaPercentage.ts @@ -1,35 +1,24 @@ import { DragState, ResizeEvent } from "../PanelGroupContext"; import { Direction } from "../types"; -import { getPanelGroupElement } from "../utils/dom/getPanelGroupElement"; import { calculateDragOffsetPercentage } from "./calculateDragOffsetPercentage"; import { isKeyDown } from "./events"; // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX export function calculateDeltaPercentage( event: ResizeEvent, - groupId: string, dragHandleId: string, direction: Direction, - initialDragState: DragState, - keyboardResizeByOptions: { - percentage: number | null; - pixels: number | null; - } + initialDragState: DragState | null, + keyboardResizeBy: number | null ): number { if (isKeyDown(event)) { const isHorizontal = direction === "horizontal"; - const groupElement = getPanelGroupElement(groupId)!; - const rect = groupElement.getBoundingClientRect(); - const groupSizeInPixels = isHorizontal ? rect.width : rect.height; - let delta = 0; if (event.shiftKey) { delta = 100; - } else if (keyboardResizeByOptions.percentage != null) { - delta = keyboardResizeByOptions.percentage; - } else if (keyboardResizeByOptions.pixels != null) { - delta = keyboardResizeByOptions.pixels / groupSizeInPixels; + } else if (keyboardResizeBy != null) { + delta = keyboardResizeBy; } else { delta = 10; } @@ -58,6 +47,10 @@ export function calculateDeltaPercentage( return movement; } else { + if (initialDragState == null) { + return 0; + } + return calculateDragOffsetPercentage( event, dragHandleId, diff --git a/packages/react-resizable-panels/src/utils/calculateDragOffsetPercentage.ts b/packages/react-resizable-panels/src/utils/calculateDragOffsetPercentage.ts index ff33d3270..b8e278168 100644 --- a/packages/react-resizable-panels/src/utils/calculateDragOffsetPercentage.ts +++ b/packages/react-resizable-panels/src/utils/calculateDragOffsetPercentage.ts @@ -1,7 +1,8 @@ import { DragState, ResizeEvent } from "../PanelGroupContext"; import { Direction } from "../types"; -import { getPanelGroupElement } from "../utils/dom/getPanelGroupElement"; -import { getResizeHandleElement } from "../utils/dom/getResizeHandleElement"; +import { assert } from "./assert"; +import { getPanelGroupElement } from "./dom/getPanelGroupElement"; +import { getResizeHandleElement } from "./dom/getResizeHandleElement"; import { getResizeEventCursorPosition } from "./getResizeEventCursorPosition"; export function calculateDragOffsetPercentage( @@ -12,14 +13,19 @@ export function calculateDragOffsetPercentage( ): number { const isHorizontal = direction === "horizontal"; - const handleElement = getResizeHandleElement(dragHandleId)!; - const groupId = handleElement.getAttribute("data-panel-group-id")!; + const handleElement = getResizeHandleElement(dragHandleId); + assert(handleElement); + + const groupId = handleElement.getAttribute("data-panel-group-id"); + assert(groupId); let { initialCursorPosition } = initialDragState; const cursorPosition = getResizeEventCursorPosition(direction, event); - const groupElement = getPanelGroupElement(groupId)!; + const groupElement = getPanelGroupElement(groupId); + assert(groupElement); + const groupRect = groupElement.getBoundingClientRect(); const groupSizeInPixels = isHorizontal ? groupRect.width : groupRect.height; diff --git a/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.test.ts b/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.test.ts index b34bcb43e..1f6b439c2 100644 --- a/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.test.ts +++ b/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.test.ts @@ -28,7 +28,6 @@ describe("calculateUnsafeDefaultLayout", () => { it("should assign even sizes for every panel by default", () => { expectToBeCloseToArray( calculateUnsafeDefaultLayout({ - groupSizePixels: 100_000, panelDataArray: [createPanelData()], }), [100] @@ -36,7 +35,6 @@ describe("calculateUnsafeDefaultLayout", () => { expectToBeCloseToArray( calculateUnsafeDefaultLayout({ - groupSizePixels: 100_000, panelDataArray: [createPanelData(), createPanelData()], }), [50, 50] @@ -44,7 +42,6 @@ describe("calculateUnsafeDefaultLayout", () => { expectToBeCloseToArray( calculateUnsafeDefaultLayout({ - groupSizePixels: 100_000, panelDataArray: [ createPanelData(), createPanelData(), @@ -58,13 +55,12 @@ describe("calculateUnsafeDefaultLayout", () => { it("should respect default panel size constraints", () => { expectToBeCloseToArray( calculateUnsafeDefaultLayout({ - groupSizePixels: 100_000, panelDataArray: [ createPanelData({ - defaultSizePercentage: 15, + defaultSize: 15, }), createPanelData({ - defaultSizePercentage: 85, + defaultSize: 85, }), ], }), @@ -75,14 +71,13 @@ describe("calculateUnsafeDefaultLayout", () => { it("should ignore min and max panel size constraints", () => { expectToBeCloseToArray( calculateUnsafeDefaultLayout({ - groupSizePixels: 100_000, panelDataArray: [ createPanelData({ - minSizePercentage: 40, + minSize: 40, }), createPanelData(), createPanelData({ - maxSizePercentage: 10, + maxSize: 10, }), ], }), diff --git a/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.ts b/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.ts index f7caea026..434e05e2f 100644 --- a/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.ts +++ b/packages/react-resizable-panels/src/utils/calculateUnsafeDefaultLayout.ts @@ -1,16 +1,14 @@ import { PanelData } from "../Panel"; -import { computePercentagePanelConstraints } from "./computePercentagePanelConstraints"; +import { assert } from "./assert"; export function calculateUnsafeDefaultLayout({ - groupSizePixels, panelDataArray, }: { - groupSizePixels: number; panelDataArray: PanelData[]; }): number[] { const layout = Array(panelDataArray.length); - const panelDataConstraints = panelDataArray.map( + const panelConstraintsArray = panelDataArray.map( (panelData) => panelData.constraints ); @@ -19,27 +17,24 @@ export function calculateUnsafeDefaultLayout({ // Distribute default sizes first for (let index = 0; index < panelDataArray.length; index++) { - const { defaultSizePercentage } = computePercentagePanelConstraints( - panelDataConstraints, - index, - groupSizePixels - ); + const panelConstraints = panelConstraintsArray[index]; + assert(panelConstraints); + const { defaultSize } = panelConstraints; - if (defaultSizePercentage != null) { + if (defaultSize != null) { numPanelsWithSizes++; - layout[index] = defaultSizePercentage; - remainingSize -= defaultSizePercentage; + layout[index] = defaultSize; + remainingSize -= defaultSize; } } // Remaining size should be distributed evenly between panels without default sizes for (let index = 0; index < panelDataArray.length; index++) { - const { defaultSizePercentage } = computePercentagePanelConstraints( - panelDataConstraints, - index, - groupSizePixels - ); - if (defaultSizePercentage != null) { + const panelConstraints = panelConstraintsArray[index]; + assert(panelConstraints); + const { defaultSize } = panelConstraints; + + if (defaultSize != null) { continue; } diff --git a/packages/react-resizable-panels/src/utils/callPanelCallbacks.ts b/packages/react-resizable-panels/src/utils/callPanelCallbacks.ts index 0c01b6b54..f76a6fa49 100644 --- a/packages/react-resizable-panels/src/utils/callPanelCallbacks.ts +++ b/packages/react-resizable-panels/src/utils/callPanelCallbacks.ts @@ -1,67 +1,33 @@ import { PanelData } from "../Panel"; -import { MixedSizes } from "../types"; -import { calculateAvailablePanelSizeInPixels } from "../utils/dom/calculateAvailablePanelSizeInPixels"; -import { convertPercentageToPixels } from "./convertPercentageToPixels"; -import { getPercentageSizeFromMixedSizes } from "./getPercentageSizeFromMixedSizes"; +import { assert } from "./assert"; // Layout should be pre-converted into percentages export function callPanelCallbacks( - groupId: string, panelsArray: PanelData[], layout: number[], - panelIdToLastNotifiedMixedSizesMap: Record + panelIdToLastNotifiedSizeMap: Record ) { - const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId); - - layout.forEach((sizePercentage, index) => { + layout.forEach((size, index) => { const panelData = panelsArray[index]; - if (!panelData) { - // Handle initial mount (when panels are registered too late to be in the panels array) - // The subsequent render+effects will handle the resize notification - return; - } + assert(panelData); const { callbacks, constraints, id: panelId } = panelData; - const { collapsible } = constraints; - - const mixedSizes: MixedSizes = { - sizePercentage, - sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels), - }; + const { collapsedSize = 0, collapsible } = constraints; - const lastNotifiedMixedSizes = panelIdToLastNotifiedMixedSizesMap[panelId]; - if ( - lastNotifiedMixedSizes == null || - mixedSizes.sizePercentage !== lastNotifiedMixedSizes.sizePercentage || - mixedSizes.sizePixels !== lastNotifiedMixedSizes.sizePixels - ) { - panelIdToLastNotifiedMixedSizesMap[panelId] = mixedSizes; + const lastNotifiedSize = panelIdToLastNotifiedSizeMap[panelId]; + if (lastNotifiedSize == null || size !== lastNotifiedSize) { + panelIdToLastNotifiedSizeMap[panelId] = size; const { onCollapse, onExpand, onResize } = callbacks; if (onResize) { - onResize(mixedSizes, lastNotifiedMixedSizes); + onResize(size, lastNotifiedSize); } if (collapsible && (onCollapse || onExpand)) { - const collapsedSize = - getPercentageSizeFromMixedSizes( - { - sizePercentage: constraints.collapsedSizePercentage, - sizePixels: constraints.collapsedSizePixels, - }, - groupSizePixels - ) ?? 0; - - const size = getPercentageSizeFromMixedSizes( - mixedSizes, - groupSizePixels - ); - if ( onExpand && - (lastNotifiedMixedSizes == null || - lastNotifiedMixedSizes.sizePercentage === collapsedSize) && + (lastNotifiedSize == null || lastNotifiedSize === collapsedSize) && size !== collapsedSize ) { onExpand(); @@ -69,8 +35,7 @@ export function callPanelCallbacks( if ( onCollapse && - (lastNotifiedMixedSizes == null || - lastNotifiedMixedSizes.sizePercentage !== collapsedSize) && + (lastNotifiedSize == null || lastNotifiedSize !== collapsedSize) && size === collapsedSize ) { onCollapse(); diff --git a/packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.test.ts b/packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.test.ts deleted file mode 100644 index 0c8d485e6..000000000 --- a/packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { PanelConstraints } from "../Panel"; -import { computePercentagePanelConstraints } from "./computePercentagePanelConstraints"; - -describe("computePercentagePanelConstraints", () => { - it("should compute reasonable defaults with no constraints", () => { - expect(computePercentagePanelConstraints([{}, {}], 0, 100)) - .toMatchInlineSnapshot(` -{ - "collapsedSizePercentage": 0, - "defaultSizePercentage": undefined, - "maxSizePercentage": 100, - "minSizePercentage": 0, -} -`); - - expect(computePercentagePanelConstraints([{}, {}], 1, 100)) - .toMatchInlineSnapshot(` -{ - "collapsedSizePercentage": 0, - "defaultSizePercentage": undefined, - "maxSizePercentage": 100, - "minSizePercentage": 0, -} -`); - }); - - it("should compute percentage based constraints based on a mix of pixels and percentages", () => { - const constraints: PanelConstraints[] = [ - { - maxSizePixels: 20, - minSizePixels: 10, - }, - { - minSizePixels: 10, - }, - { - minSizePixels: 10, - }, - ]; - - expect(computePercentagePanelConstraints(constraints, 0, 100)) - .toMatchInlineSnapshot(` -{ - "collapsedSizePercentage": 0, - "defaultSizePercentage": undefined, - "maxSizePercentage": 20, - "minSizePercentage": 10, -} -`); - - expect(computePercentagePanelConstraints(constraints, 1, 100)) - .toMatchInlineSnapshot(` -{ - "collapsedSizePercentage": 0, - "defaultSizePercentage": undefined, - "maxSizePercentage": 80, - "minSizePercentage": 10, -} -`); - - expect(computePercentagePanelConstraints(constraints, 2, 100)) - .toMatchInlineSnapshot(` -{ - "collapsedSizePercentage": 0, - "defaultSizePercentage": undefined, - "maxSizePercentage": 80, - "minSizePercentage": 10, -} -`); - }); - - it("should compute reasonable percentage based constraints from pixels if group size is negative", () => { - jest.spyOn(console, "warn").mockImplementation(() => {}); - - expect( - computePercentagePanelConstraints( - [ - { - minSizePixels: 25, - maxSizePixels: 100, - }, - ], - - 0, - -100 - ) - ).toMatchInlineSnapshot(` -{ - "collapsedSizePercentage": 0, - "defaultSizePercentage": undefined, - "maxSizePercentage": 0, - "minSizePercentage": 0, -} -`); - - expect(console.warn).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.ts b/packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.ts deleted file mode 100644 index 3014d3531..000000000 --- a/packages/react-resizable-panels/src/utils/computePercentagePanelConstraints.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { PanelConstraints } from "../Panel"; -import { convertPixelConstraintsToPercentages } from "./convertPixelConstraintsToPercentages"; - -export function computePercentagePanelConstraints( - panelConstraintsArray: PanelConstraints[], - panelIndex: number, - groupSizePixels: number -): { - collapsedSizePercentage: number; - defaultSizePercentage: number | undefined; - maxSizePercentage: number; - minSizePercentage: number; -} { - // All panel constraints, excluding the current one - let totalMinConstraints = 0; - let totalMaxConstraints = 0; - - for (let index = 0; index < panelConstraintsArray.length; index++) { - if (index !== panelIndex) { - const { collapsible } = panelConstraintsArray[index]!; - const { collapsedSizePercentage, maxSizePercentage, minSizePercentage } = - convertPixelConstraintsToPercentages( - panelConstraintsArray[index]!, - groupSizePixels - ); - - totalMaxConstraints += maxSizePercentage; - totalMinConstraints += collapsible - ? collapsedSizePercentage - : minSizePercentage; - } - } - - const { - collapsedSizePercentage, - defaultSizePercentage, - maxSizePercentage, - minSizePercentage, - } = convertPixelConstraintsToPercentages( - panelConstraintsArray[panelIndex]!, - groupSizePixels - ); - - return { - collapsedSizePercentage, - defaultSizePercentage, - maxSizePercentage: - panelConstraintsArray.length > 1 - ? Math.min(maxSizePercentage, 100 - totalMinConstraints) - : maxSizePercentage, - minSizePercentage: - panelConstraintsArray.length > 1 - ? Math.max(minSizePercentage, 100 - totalMaxConstraints) - : minSizePercentage, - }; -} diff --git a/packages/react-resizable-panels/src/utils/convertPercentageToPixels.test.ts b/packages/react-resizable-panels/src/utils/convertPercentageToPixels.test.ts deleted file mode 100644 index fa294087c..000000000 --- a/packages/react-resizable-panels/src/utils/convertPercentageToPixels.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { convertPercentageToPixels } from "./convertPercentageToPixels"; - -describe("convertPercentageToPixels", () => { - it("should convert percentages to pixels", () => { - expect(convertPercentageToPixels(0, 100_000)).toBe(0); - expect(convertPercentageToPixels(50, 100_000)).toBe(50_000); - expect(convertPercentageToPixels(100, 100_000)).toBe(100_000); - }); -}); diff --git a/packages/react-resizable-panels/src/utils/convertPercentageToPixels.ts b/packages/react-resizable-panels/src/utils/convertPercentageToPixels.ts deleted file mode 100644 index dc351451f..000000000 --- a/packages/react-resizable-panels/src/utils/convertPercentageToPixels.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function convertPercentageToPixels( - percentage: number, - groupSizePixels: number -): number { - return (percentage / 100) * groupSizePixels; -} diff --git a/packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.test.ts b/packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.test.ts deleted file mode 100644 index 1065e577c..000000000 --- a/packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { convertPixelConstraintsToPercentages } from "./convertPixelConstraintsToPercentages"; - -describe("convertPixelConstraintsToPercentages", () => { - it("should respect percentage panel constraints if group size is negative", () => { - jest.spyOn(console, "warn").mockImplementation(() => {}); - - expect( - convertPixelConstraintsToPercentages( - { - minSizePercentage: 25, - defaultSizePercentage: 50, - maxSizePercentage: 75, - }, - -100 - ) - ).toEqual({ - collapsedSizePercentage: 0, - defaultSizePercentage: 50, - maxSizePercentage: 75, - minSizePercentage: 25, - }); - - expect(console.warn).toHaveBeenCalledTimes(0); - }); - - // Edge case test (issues/206) - it("should ignore pixel panel constraints if group size is negative", () => { - jest.spyOn(console, "warn").mockImplementation(() => {}); - - expect( - convertPixelConstraintsToPercentages( - { - minSizePixels: 25, - maxSizePixels: 75, - }, - -100 - ) - ).toEqual({ - collapsedSizePercentage: 0, - defaultSizePercentage: undefined, - maxSizePercentage: 0, - minSizePercentage: 0, - }); - - expect(console.warn).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.ts b/packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.ts deleted file mode 100644 index ce682370b..000000000 --- a/packages/react-resizable-panels/src/utils/convertPixelConstraintsToPercentages.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PanelConstraints } from "../Panel"; -import { convertPixelsToPercentage } from "./convertPixelsToPercentage"; - -export function convertPixelConstraintsToPercentages( - panelConstraints: PanelConstraints, - groupSizePixels: number -): { - collapsedSizePercentage: number; - defaultSizePercentage: number | undefined; - maxSizePercentage: number; - minSizePercentage: number; -} { - let { - collapsedSizePercentage = 0, - collapsedSizePixels, - defaultSizePercentage, - defaultSizePixels, - maxSizePercentage = 100, - maxSizePixels, - minSizePercentage = 0, - minSizePixels, - } = panelConstraints; - - const hasPixelConstraints = - collapsedSizePixels != null || - defaultSizePixels != null || - minSizePixels != null || - maxSizePixels != null; - - if (hasPixelConstraints && groupSizePixels <= 0) { - console.warn(`WARNING: Invalid group size: ${groupSizePixels}px`); - - return { - collapsedSizePercentage: 0, - defaultSizePercentage, - maxSizePercentage: 0, - minSizePercentage: 0, - }; - } - - if (collapsedSizePixels != null) { - collapsedSizePercentage = convertPixelsToPercentage( - collapsedSizePixels, - groupSizePixels - ); - } - if (defaultSizePixels != null) { - defaultSizePercentage = convertPixelsToPercentage( - defaultSizePixels, - groupSizePixels - ); - } - if (minSizePixels != null) { - minSizePercentage = convertPixelsToPercentage( - minSizePixels, - groupSizePixels - ); - } - if (maxSizePixels != null) { - maxSizePercentage = convertPixelsToPercentage( - maxSizePixels, - groupSizePixels - ); - } - - return { - collapsedSizePercentage, - defaultSizePercentage, - maxSizePercentage, - minSizePercentage, - }; -} diff --git a/packages/react-resizable-panels/src/utils/convertPixelsToPercentage.test.ts b/packages/react-resizable-panels/src/utils/convertPixelsToPercentage.test.ts deleted file mode 100644 index e8e15818a..000000000 --- a/packages/react-resizable-panels/src/utils/convertPixelsToPercentage.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { convertPixelsToPercentage } from "./convertPixelsToPercentage"; - -describe("convertPixelsToPercentage", () => { - it("should convert pixels to percentages", () => { - expect(convertPixelsToPercentage(0, 100_000)).toBe(0); - expect(convertPixelsToPercentage(50_000, 100_000)).toBe(50); - expect(convertPixelsToPercentage(100_000, 100_000)).toBe(100); - }); -}); diff --git a/packages/react-resizable-panels/src/utils/convertPixelsToPercentage.ts b/packages/react-resizable-panels/src/utils/convertPixelsToPercentage.ts deleted file mode 100644 index e83bffb71..000000000 --- a/packages/react-resizable-panels/src/utils/convertPixelsToPercentage.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function convertPixelsToPercentage( - pixels: number, - groupSizePixels: number -): number { - return (pixels / groupSizePixels) * 100; -} diff --git a/packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.test.ts b/packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.test.ts deleted file mode 100644 index 541bf4bc6..000000000 --- a/packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { getPercentageSizeFromMixedSizes } from "./getPercentageSizeFromMixedSizes"; - -describe("getPercentageSizeFromMixedSizes", () => { - it("should return percentage sizes as-is", () => { - expect( - getPercentageSizeFromMixedSizes( - { - sizePercentage: 50, - }, - 100_000 - ) - ).toBe(50); - expect( - getPercentageSizeFromMixedSizes( - { - sizePercentage: 25, - sizePixels: 100, - }, - 100_000 - ) - ).toBe(25); - }); - - it("should convert pixels to percentages", () => { - expect( - getPercentageSizeFromMixedSizes( - { - sizePixels: 50_000, - }, - 100_000 - ) - ).toBe(50); - expect( - getPercentageSizeFromMixedSizes( - { - sizePercentage: 25, - sizePixels: 50_000, - }, - 100_000 - ) - ).toBe(25); - }); - - it("should return undefined if neither pixel nor percentage sizes specified", () => { - expect(getPercentageSizeFromMixedSizes({}, 100_000)).toBeUndefined(); - }); -}); diff --git a/packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.ts b/packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.ts deleted file mode 100644 index c59f3d229..000000000 --- a/packages/react-resizable-panels/src/utils/getPercentageSizeFromMixedSizes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MixedSizes } from "../types"; -import { convertPixelsToPercentage } from "./convertPixelsToPercentage"; - -export function getPercentageSizeFromMixedSizes( - { sizePercentage, sizePixels }: Partial, - groupSizePixels: number -): number | undefined { - if (sizePercentage != null) { - return sizePercentage; - } else if (sizePixels != null) { - return convertPixelsToPercentage(sizePixels, groupSizePixels); - } - - return undefined; -} diff --git a/packages/react-resizable-panels/src/utils/getResizeEventCursorPosition.ts b/packages/react-resizable-panels/src/utils/getResizeEventCursorPosition.ts index 888fd2939..5dbc0f0d3 100644 --- a/packages/react-resizable-panels/src/utils/getResizeEventCursorPosition.ts +++ b/packages/react-resizable-panels/src/utils/getResizeEventCursorPosition.ts @@ -1,5 +1,6 @@ import { ResizeEvent } from "../PanelGroupContext"; import { Direction } from "../types"; +import { assert } from "./assert"; import { isMouseEvent, isTouchEvent } from "./events"; export function getResizeEventCursorPosition( @@ -12,6 +13,7 @@ export function getResizeEventCursorPosition( return isHorizontal ? event.clientX : event.clientY; } else if (isTouchEvent(event)) { const firstTouch = event.touches[0]; + assert(firstTouch); return isHorizontal ? firstTouch.screenX : firstTouch.screenY; } else { throw Error(`Unsupported event type "${event.type}"`); diff --git a/packages/react-resizable-panels/src/utils/resizePanel.test.ts b/packages/react-resizable-panels/src/utils/resizePanel.test.ts index 941c8ed30..f3ed6dd19 100644 --- a/packages/react-resizable-panels/src/utils/resizePanel.test.ts +++ b/packages/react-resizable-panels/src/utils/resizePanel.test.ts @@ -4,12 +4,11 @@ describe("resizePanel", () => { it("should not collapse (or expand) until a panel size dips below the halfway point between min size and collapsed size", () => { expect( resizePanel({ - groupSizePixels: 100, panelConstraints: [ { collapsible: true, - collapsedSizePercentage: 10, - minSizePercentage: 20, + collapsedSize: 10, + minSize: 20, }, ], panelIndex: 0, @@ -19,12 +18,11 @@ describe("resizePanel", () => { expect( resizePanel({ - groupSizePixels: 100, panelConstraints: [ { collapsible: true, - collapsedSizePercentage: 10, - minSizePercentage: 20, + collapsedSize: 10, + minSize: 20, }, ], panelIndex: 0, @@ -34,11 +32,10 @@ describe("resizePanel", () => { expect( resizePanel({ - groupSizePixels: 100, panelConstraints: [ { collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], panelIndex: 0, @@ -48,11 +45,10 @@ describe("resizePanel", () => { expect( resizePanel({ - groupSizePixels: 100, panelConstraints: [ { collapsible: true, - minSizePercentage: 20, + minSize: 20, }, ], panelIndex: 0, @@ -60,46 +56,4 @@ describe("resizePanel", () => { }) ).toBe(0); }); - - // Edge case test (issues/206) - it("should respect percentage panel constraints if group size is negative", () => { - jest.spyOn(console, "warn").mockImplementation(() => {}); - - expect( - resizePanel({ - groupSizePixels: -100, - panelConstraints: [ - { - minSizePercentage: 25, - maxSizePercentage: 75, - }, - ], - panelIndex: 0, - size: 50, - }) - ).toBe(50); - - expect(console.warn).toHaveBeenCalledTimes(0); - }); - - // Edge case test (issues/206) - it("should handle pixel panel constraints if group size is negative", () => { - jest.spyOn(console, "warn").mockImplementation(() => {}); - - expect( - resizePanel({ - groupSizePixels: -100, - panelConstraints: [ - { - minSizePixels: 25, - maxSizePixels: 75, - }, - ], - panelIndex: 0, - size: 50, - }) - ).toBe(0); - - expect(console.warn).toHaveBeenCalledTimes(1); - }); }); diff --git a/packages/react-resizable-panels/src/utils/resizePanel.ts b/packages/react-resizable-panels/src/utils/resizePanel.ts index 7cadaeb9f..1ad31c2a9 100644 --- a/packages/react-resizable-panels/src/utils/resizePanel.ts +++ b/packages/react-resizable-panels/src/utils/resizePanel.ts @@ -1,66 +1,44 @@ import { PanelConstraints } from "../Panel"; -import { computePercentagePanelConstraints } from "./computePercentagePanelConstraints"; +import { PRECISION } from "../constants"; +import { assert } from "./assert"; import { fuzzyCompareNumbers } from "./numbers/fuzzyCompareNumbers"; // Panel size must be in percentages; pixel values should be pre-converted export function resizePanel({ - groupSizePixels, - panelConstraints, + panelConstraints: panelConstraintsArray, panelIndex, size, }: { - groupSizePixels: number; panelConstraints: PanelConstraints[]; panelIndex: number; size: number; }) { - const hasPixelConstraints = panelConstraints.some( - ({ - collapsedSizePixels, - defaultSizePixels, - minSizePixels, - maxSizePixels, - }) => - collapsedSizePixels != null || - defaultSizePixels != null || - minSizePixels != null || - maxSizePixels != null - ); - - if (hasPixelConstraints && groupSizePixels <= 0) { - console.warn(`WARNING: Invalid group size: ${groupSizePixels}px`); - - return 0; - } - - let { collapsible } = panelConstraints[panelIndex]!; - - const { collapsedSizePercentage, maxSizePercentage, minSizePercentage } = - computePercentagePanelConstraints( - panelConstraints, - panelIndex, - groupSizePixels - ); - - if (minSizePercentage != null) { - if (fuzzyCompareNumbers(size, minSizePercentage) < 0) { - if (collapsible) { - // Collapsible panels should snap closed or open only once they cross the halfway point between collapsed and min size. - const halfwayPoint = (collapsedSizePercentage + minSizePercentage) / 2; - if (fuzzyCompareNumbers(size, halfwayPoint) < 0) { - size = collapsedSizePercentage; - } else { - size = minSizePercentage; - } + const panelConstraints = panelConstraintsArray[panelIndex]; + assert(panelConstraints != null); + + let { + collapsedSize = 0, + collapsible, + maxSize = 100, + minSize = 0, + } = panelConstraints; + + if (fuzzyCompareNumbers(size, minSize) < 0) { + if (collapsible) { + // Collapsible panels should snap closed or open only once they cross the halfway point between collapsed and min size. + const halfwayPoint = (collapsedSize + minSize) / 2; + if (fuzzyCompareNumbers(size, halfwayPoint) < 0) { + size = collapsedSize; } else { - size = minSizePercentage; + size = minSize; } + } else { + size = minSize; } } - if (maxSizePercentage != null) { - size = Math.min(maxSizePercentage, size); - } + size = Math.min(maxSize, size); + size = parseFloat(size.toFixed(PRECISION)); return size; } diff --git a/packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.test.ts b/packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.test.ts deleted file mode 100644 index 66bc59605..000000000 --- a/packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { shouldMonitorPixelBasedConstraints } from "./shouldMonitorPixelBasedConstraints"; - -describe("shouldMonitorPixelBasedConstraints", () => { - it("should identify min, max, or collapsed size pixel constraints", () => { - expect( - shouldMonitorPixelBasedConstraints([{}, { collapsedSizePixels: 100 }, {}]) - ).toBe(true); - - expect( - shouldMonitorPixelBasedConstraints([{ minSizePixels: 100 }, {}, {}]) - ).toBe(true); - - expect( - shouldMonitorPixelBasedConstraints([{}, {}, { maxSizePixels: 100 }]) - ).toBe(true); - }); - - it("should ignore default size constraints", () => { - expect( - shouldMonitorPixelBasedConstraints([{ defaultSizePixels: 100 }]) - ).toBe(false); - }); -}); diff --git a/packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.ts b/packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.ts deleted file mode 100644 index 710bed5ec..000000000 --- a/packages/react-resizable-panels/src/utils/shouldMonitorPixelBasedConstraints.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PanelConstraints } from "../Panel"; - -export function shouldMonitorPixelBasedConstraints( - constraints: PanelConstraints[] -): boolean { - return constraints.some((constraints) => { - return ( - constraints.collapsedSizePixels !== undefined || - constraints.maxSizePixels !== undefined || - constraints.minSizePixels !== undefined - ); - }); -} diff --git a/packages/react-resizable-panels/src/utils/test-utils.ts b/packages/react-resizable-panels/src/utils/test-utils.ts index a99fdaf95..c0d3b7a6f 100644 --- a/packages/react-resizable-panels/src/utils/test-utils.ts +++ b/packages/react-resizable-panels/src/utils/test-utils.ts @@ -1,4 +1,4 @@ -import { MixedSizes } from "../types"; +import { assert } from "./assert"; const util = require("util"); @@ -11,6 +11,8 @@ export function expectToBeCloseToArray( try { actualNumbers.forEach((actualNumber, index) => { const expectedNumber = expectedNumbers[index]; + assert(expectedNumber != null); + expect(actualNumber).toBeCloseTo(expectedNumber, 1); }); } catch (error) { @@ -74,13 +76,10 @@ export function mockPanelGroupOffsetWidthAndHeight( } export function verifyExpandedPanelGroupLayout( - actualLayout: MixedSizes[], - expectedPercentages: number[] + actualLayout: number[], + expectedLayout: number[] ) { - expect(actualLayout).toHaveLength(expectedPercentages.length); - expect(actualLayout.map(({ sizePercentage }) => sizePercentage)).toEqual( - expectedPercentages - ); + expect(actualLayout).toEqual(expectedLayout); } export function verifyExpectedWarnings( diff --git a/packages/react-resizable-panels/src/utils/validatePanelConstraints.test.ts b/packages/react-resizable-panels/src/utils/validatePanelConstraints.test.ts index ae05f3c6e..9fef8828c 100644 --- a/packages/react-resizable-panels/src/utils/validatePanelConstraints.test.ts +++ b/packages/react-resizable-panels/src/utils/validatePanelConstraints.test.ts @@ -5,7 +5,6 @@ describe("validatePanelConstraints", () => { it("should not warn if there are no validation errors", () => { verifyExpectedWarnings(() => { validatePanelConstraints({ - groupSizePixels: 100_000, panelConstraints: [{}], panelIndex: 0, panelId: "test", @@ -13,60 +12,13 @@ describe("validatePanelConstraints", () => { }); }); - it("should warn about conflicting percentages and pixels", () => { - verifyExpectedWarnings(() => { - validatePanelConstraints({ - groupSizePixels: 100_000, - panelConstraints: [ - { - collapsedSizePercentage: 5, - collapsedSizePixels: 10, - }, - ], - panelIndex: 0, - panelId: "test", - }); - }, "should not specify both percentage and pixel units for: collapsed size"); - - verifyExpectedWarnings(() => { - validatePanelConstraints({ - groupSizePixels: 100_000, - panelConstraints: [ - { - maxSizePercentage: 5, - maxSizePixels: 10, - minSizePercentage: 5, - minSizePixels: 10, - }, - ], - panelIndex: 0, - panelId: "test", - }); - }, "should not specify both percentage and pixel units for: max size, min size"); - - verifyExpectedWarnings(() => { - validatePanelConstraints({ - groupSizePixels: 100_000, - panelConstraints: [ - { - defaultSizePercentage: 5, - defaultSizePixels: 10, - }, - ], - panelIndex: 0, - panelId: "test", - }); - }, "should not specify both percentage and pixel units for: default size"); - }); - it("should warn about conflicting min/max sizes", () => { verifyExpectedWarnings(() => { validatePanelConstraints({ - groupSizePixels: 100_000, panelConstraints: [ { - maxSizePercentage: 5, - minSizePercentage: 10, + maxSize: 5, + minSize: 10, }, ], panelIndex: 0, @@ -78,11 +30,10 @@ describe("validatePanelConstraints", () => { it("should warn about conflicting collapsed and min sizes", () => { verifyExpectedWarnings(() => { validatePanelConstraints({ - groupSizePixels: 100_000, panelConstraints: [ { - collapsedSizePercentage: 15, - minSizePercentage: 10, + collapsedSize: 15, + minSize: 10, }, ], panelIndex: 0, @@ -94,11 +45,10 @@ describe("validatePanelConstraints", () => { it("should warn about conflicting default and min/max sizes", () => { verifyExpectedWarnings(() => { validatePanelConstraints({ - groupSizePixels: 100_000, panelConstraints: [ { - defaultSizePercentage: -1, - minSizePercentage: 10, + defaultSize: -1, + minSize: 10, }, ], panelIndex: 0, @@ -108,11 +58,10 @@ describe("validatePanelConstraints", () => { verifyExpectedWarnings(() => { validatePanelConstraints({ - groupSizePixels: 100_000, panelConstraints: [ { - defaultSizePercentage: 5, - minSizePercentage: 10, + defaultSize: 5, + minSize: 10, }, ], panelIndex: 0, @@ -122,11 +71,10 @@ describe("validatePanelConstraints", () => { verifyExpectedWarnings(() => { validatePanelConstraints({ - groupSizePixels: 100_000, panelConstraints: [ { - defaultSizePercentage: 101, - maxSizePercentage: 10, + defaultSize: 101, + maxSize: 10, }, ], panelIndex: 0, @@ -136,11 +84,10 @@ describe("validatePanelConstraints", () => { verifyExpectedWarnings(() => { validatePanelConstraints({ - groupSizePixels: 100_000, panelConstraints: [ { - defaultSizePercentage: 15, - maxSizePercentage: 10, + defaultSize: 15, + maxSize: 10, }, ], panelIndex: 0, diff --git a/packages/react-resizable-panels/src/utils/validatePanelConstraints.ts b/packages/react-resizable-panels/src/utils/validatePanelConstraints.ts index 849c5db70..58f258c2e 100644 --- a/packages/react-resizable-panels/src/utils/validatePanelConstraints.ts +++ b/packages/react-resizable-panels/src/utils/validatePanelConstraints.ts @@ -1,14 +1,12 @@ import { isDevelopment } from "#is-development"; import { PanelConstraints } from "../Panel"; -import { computePercentagePanelConstraints } from "./computePercentagePanelConstraints"; +import { assert } from "./assert"; export function validatePanelConstraints({ - groupSizePixels, - panelConstraints, + panelConstraints: panelConstraintsArray, panelId, panelIndex, }: { - groupSizePixels: number; panelConstraints: PanelConstraints[]; panelId: string | undefined; panelIndex: number; @@ -16,77 +14,38 @@ export function validatePanelConstraints({ if (isDevelopment) { const warnings = []; - { - const { - collapsedSizePercentage, - collapsedSizePixels, - defaultSizePercentage, - defaultSizePixels, - maxSizePercentage, - maxSizePixels, - minSizePercentage, - minSizePixels, - } = panelConstraints[panelIndex]!; + const panelConstraints = panelConstraintsArray[panelIndex]; + assert(panelConstraints); - const conflictingUnits: string[] = []; + const { + collapsedSize = 0, + defaultSize, + maxSize = 100, + minSize = 0, + } = panelConstraints; - if (collapsedSizePercentage != null && collapsedSizePixels != null) { - conflictingUnits.push("collapsed size"); - } - if (defaultSizePercentage != null && defaultSizePixels != null) { - conflictingUnits.push("default size"); - } - if (maxSizePercentage != null && maxSizePixels != null) { - conflictingUnits.push("max size"); - } - if (minSizePercentage != null && minSizePixels != null) { - conflictingUnits.push("min size"); - } - - if (conflictingUnits.length > 0) { - warnings.push( - `should not specify both percentage and pixel units for: ${conflictingUnits.join( - ", " - )}` - ); - } - } - - { - const { - collapsedSizePercentage, - defaultSizePercentage, - maxSizePercentage, - minSizePercentage, - } = computePercentagePanelConstraints( - panelConstraints, - panelIndex, - groupSizePixels + if (minSize > maxSize) { + warnings.push( + `min size (${minSize}%) should not be greater than max size (${maxSize}%)` ); + } - if (minSizePercentage > maxSizePercentage) { - warnings.push( - `min size (${minSizePercentage}%) should not be greater than max size (${maxSizePercentage}%)` - ); + if (defaultSize != null) { + if (defaultSize < 0) { + warnings.push("default size should not be less than 0"); + } else if (defaultSize < minSize) { + warnings.push("default size should not be less than min size"); } - if (defaultSizePercentage != null) { - if (defaultSizePercentage < 0) { - warnings.push("default size should not be less than 0"); - } else if (defaultSizePercentage < minSizePercentage) { - warnings.push("default size should not be less than min size"); - } - - if (defaultSizePercentage > 100) { - warnings.push("default size should not be greater than 100"); - } else if (defaultSizePercentage > maxSizePercentage) { - warnings.push("default size should not be greater than max size"); - } + if (defaultSize > 100) { + warnings.push("default size should not be greater than 100"); + } else if (defaultSize > maxSize) { + warnings.push("default size should not be greater than max size"); } + } - if (collapsedSizePercentage > minSizePercentage) { - warnings.push("collapsed size should not be greater than min size"); - } + if (collapsedSize > minSize) { + warnings.push("collapsed size should not be greater than min size"); } if (warnings.length > 0) { diff --git a/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.test.ts b/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.test.ts index d017f539e..0c756899a 100644 --- a/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.test.ts +++ b/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.test.ts @@ -5,39 +5,34 @@ describe("validatePanelGroupLayout", () => { it("should accept requested layout if there are no constraints provided", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [10, 60, 30], panelConstraints: [{}, {}, {}], }) ).toEqual([10, 60, 30]); }); - it("should reject layouts that do not total 100%", () => { - verifyExpectedWarnings( - () => - validatePanelGroupLayout({ - groupSizePixels: NaN, - layout: [10, 20, 30], - panelConstraints: [{}, {}, {}], - }), - "Invalid layout total size" - ); + it("should normalize layouts that do not total 100%", () => { + let layout; + verifyExpectedWarnings(() => { + layout = validatePanelGroupLayout({ + layout: [10, 20, 20], + panelConstraints: [{}, {}, {}], + }); + }, "Invalid layout total size"); + expect(layout).toEqual([20, 40, 40]); - verifyExpectedWarnings( - () => - validatePanelGroupLayout({ - groupSizePixels: NaN, - layout: [50, 100, 150], - panelConstraints: [{}, {}, {}], - }), - "Invalid layout total size" - ); + verifyExpectedWarnings(() => { + layout = validatePanelGroupLayout({ + layout: [50, 100, 50], + panelConstraints: [{}, {}, {}], + }); + }, "Invalid layout total size"); + expect(layout).toEqual([25, 50, 25]); }); it("should reject layouts that do not match the number of panels", () => { expect(() => validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [10, 20, 30], panelConstraints: [{}, {}], }) @@ -45,7 +40,6 @@ describe("validatePanelGroupLayout", () => { expect(() => validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [50, 50], panelConstraints: [{}, {}, {}], }) @@ -56,11 +50,10 @@ describe("validatePanelGroupLayout", () => { it("should adjust the layout to account for minimum percentage sizes", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [25, 75], panelConstraints: [ { - minSizePercentage: 35, + minSize: 35, }, {}, ], @@ -68,33 +61,17 @@ describe("validatePanelGroupLayout", () => { ).toEqual([35, 65]); }); - it("should adjust the layout to account for minimum pixel sizes", () => { - expect( - validatePanelGroupLayout({ - groupSizePixels: 400, - layout: [20, 80], - panelConstraints: [ - { - minSizePixels: 100, - }, - {}, - ], - }) - ).toEqual([25, 75]); - }); - it("should account for multiple panels with minimum size constraints", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [20, 60, 20], panelConstraints: [ { - minSizePercentage: 25, + minSize: 25, }, {}, { - minSizePercentage: 25, + minSize: 25, }, ], }) @@ -106,38 +83,21 @@ describe("validatePanelGroupLayout", () => { it("should adjust the layout to account for maximum percentage sizes", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [25, 75], - panelConstraints: [{}, { maxSizePercentage: 65 }], + panelConstraints: [{}, { maxSize: 65 }], }) ).toEqual([35, 65]); }); - it("should adjust the layout to account for maximum pixel sizes", () => { - expect( - validatePanelGroupLayout({ - groupSizePixels: 400, - layout: [20, 80], - panelConstraints: [ - {}, - { - maxSizePixels: 100, - }, - ], - }) - ).toEqual([75, 25]); - }); - it("should account for multiple panels with maximum size constraints", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [20, 60, 20], panelConstraints: [ { - maxSizePercentage: 15, + maxSize: 15, }, - { maxSizePercentage: 50 }, + { maxSize: 50 }, {}, ], }) @@ -149,9 +109,8 @@ describe("validatePanelGroupLayout", () => { it("should not collapse a panel that's at or above the minimum size", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [25, 75], - panelConstraints: [{ collapsible: true, minSizePercentage: 25 }, {}], + panelConstraints: [{ collapsible: true, minSize: 25 }, {}], }) ).toEqual([25, 75]); }); @@ -159,13 +118,12 @@ describe("validatePanelGroupLayout", () => { it("should collapse a panel once it drops below the halfway point between collapsed and minimum percentage sizes", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [15, 85], panelConstraints: [ { collapsible: true, - collapsedSizePercentage: 10, - minSizePercentage: 20, + collapsedSize: 10, + minSize: 20, }, {}, ], @@ -174,90 +132,17 @@ describe("validatePanelGroupLayout", () => { expect( validatePanelGroupLayout({ - groupSizePixels: NaN, layout: [14, 86], panelConstraints: [ { collapsible: true, - collapsedSizePercentage: 10, - minSizePercentage: 20, + collapsedSize: 10, + minSize: 20, }, {}, ], }) ).toEqual([10, 90]); }); - - it("should collapse a panel once it drops below the halfway point between collapsed and minimum pixel sizes", () => { - expect( - validatePanelGroupLayout({ - groupSizePixels: 400, - layout: [20, 80], - panelConstraints: [ - { - collapsible: true, - collapsedSizePixels: 0, - minSizePixels: 100, - }, - {}, - ], - }) - ).toEqual([25, 75]); - - expect( - validatePanelGroupLayout({ - groupSizePixels: 400, - layout: [10, 90], - panelConstraints: [ - { - collapsible: true, - collapsedSizePixels: 0, - minSizePixels: 100, - }, - {}, - ], - }) - ).toEqual([0, 100]); - }); - }); - - describe("combination of minimum and maximum size constraints", () => { - it("three panel min/max configuration", () => { - expect( - validatePanelGroupLayout({ - groupSizePixels: NaN, - layout: [25, 50, 25], - panelConstraints: [ - { minSizePercentage: 10, maxSizePercentage: 25 }, - { maxSizePercentage: 75 }, - { minSizePercentage: 10, maxSizePercentage: 50 }, - ], - }) - ).toEqual([25, 50, 25]); - - expect( - validatePanelGroupLayout({ - groupSizePixels: NaN, - layout: [5, 80, 15], - panelConstraints: [ - { minSizePercentage: 10, maxSizePercentage: 25 }, - { maxSizePercentage: 75 }, - { minSizePercentage: 10, maxSizePercentage: 50 }, - ], - }) - ).toEqual([10, 75, 15]); - - expect( - validatePanelGroupLayout({ - groupSizePixels: NaN, - layout: [30, 10, 60], - panelConstraints: [ - { minSizePercentage: 10, maxSizePercentage: 25 }, - { maxSizePercentage: 75 }, - { minSizePercentage: 10, maxSizePercentage: 50 }, - ], - }) - ).toEqual([25, 25, 50]); - }); }); }); diff --git a/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.ts b/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.ts index 50d4b3168..7fb669de0 100644 --- a/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.ts +++ b/packages/react-resizable-panels/src/utils/validatePanelGroupLayout.ts @@ -1,19 +1,22 @@ import { isDevelopment } from "#is-development"; import { PanelConstraints } from "../Panel"; +import { assert } from "./assert"; import { fuzzyNumbersEqual } from "./numbers/fuzzyNumbersEqual"; import { resizePanel } from "./resizePanel"; // All units must be in percentages; pixel values should be pre-converted export function validatePanelGroupLayout({ - groupSizePixels, layout: prevLayout, panelConstraints, }: { - groupSizePixels: number; layout: number[]; panelConstraints: PanelConstraints[]; }): number[] { const nextLayout = [...prevLayout]; + const nextLayoutTotalSize = nextLayout.reduce( + (accumulated, current) => accumulated + current, + 0 + ); // Validate layout expectations if (nextLayout.length !== panelConstraints.length) { @@ -22,31 +25,32 @@ export function validatePanelGroupLayout({ .map((size) => `${size}%`) .join(", ")}` ); - } else if ( - !fuzzyNumbersEqual( - nextLayout.reduce((accumulated, current) => accumulated + current, 0), - 100 - ) - ) { + } else if (!fuzzyNumbersEqual(nextLayoutTotalSize, 100)) { // This is not ideal so we should warn about it, but it may be recoverable in some cases // (especially if the amount is small) if (isDevelopment) { console.warn( `WARNING: Invalid layout total size: ${nextLayout .map((size) => `${size}%`) - .join(", ")}` + .join(", ")}. Layout normalization will be applied.` ); } + for (let index = 0; index < panelConstraints.length; index++) { + const unsafeSize = nextLayout[index]; + assert(unsafeSize != null); + const safeSize = (100 / nextLayoutTotalSize) * unsafeSize; + nextLayout[index] = safeSize; + } } let remainingSize = 0; // First pass: Validate the proposed layout given each panel's constraints for (let index = 0; index < panelConstraints.length; index++) { - const unsafeSize = nextLayout[index]!; + const unsafeSize = nextLayout[index]; + assert(unsafeSize != null); const safeSize = resizePanel({ - groupSizePixels, panelConstraints, panelIndex: index, size: unsafeSize, @@ -63,10 +67,10 @@ export function validatePanelGroupLayout({ // (It's not worth taking multiple additional passes to evenly distribute) if (!fuzzyNumbersEqual(remainingSize, 0)) { for (let index = 0; index < panelConstraints.length; index++) { - const prevSize = nextLayout[index]!; + const prevSize = nextLayout[index]; + assert(prevSize != null); const unsafeSize = prevSize + remainingSize; const safeSize = resizePanel({ - groupSizePixels, panelConstraints, panelIndex: index, size: unsafeSize, diff --git a/packages/react-resizable-panels/src/vendor/react.ts b/packages/react-resizable-panels/src/vendor/react.ts index a0f3d73cb..cffb0fb3e 100644 --- a/packages/react-resizable-panels/src/vendor/react.ts +++ b/packages/react-resizable-panels/src/vendor/react.ts @@ -12,6 +12,7 @@ import type { CSSProperties, ElementType, ForwardedRef, + HTMLAttributes, MouseEvent, PropsWithChildren, ReactNode, @@ -57,6 +58,7 @@ export type { CSSProperties, ElementType, ForwardedRef, + HTMLAttributes, MouseEvent, PropsWithChildren, ReactNode, diff --git a/tsconfig.json b/tsconfig.json index 8f3ccad11..195d7510a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "module": "es2020", "moduleResolution": "bundler", "noImplicitAny": true, + "noUncheckedIndexedAccess": true, "strict": true, "typeRoots": ["node_modules/@types"], "types": ["jest", "node"]