From 23d63d52b72bfd1e88251fee73622535c62ff48b Mon Sep 17 00:00:00 2001
From: Brian Vaughn
Date: Wed, 9 Aug 2023 13:11:10 -1000
Subject: [PATCH] Replaced PanelGroup validateLayout with Panel units="static"
---
.../src/components/Icon.tsx | 7 +-
.../src/routes/EndToEndTesting/index.tsx | 126 ++--
.../routes/examples/ExternalPersistence.tsx | 3 +
.../src/routes/examples/PixelBasedLayouts.tsx | 166 ++---
.../src/routes/examples/shared.module.css | 13 +-
.../src/utils/UrlData.ts | 39 +-
.../tests/CursorStyle.spec.ts | 5 +-
.../tests/DevelopmentWarnings.spec.ts | 144 -----
.../DevelopmentWarningsAndErrors.spec.ts | 283 ++++++++
.../tests/Panel-StaticUnits.spec.ts | 280 ++++++++
.../usePanelGroupLayoutValidator.spec.ts | 177 -----
.../tests/utils/panels.ts | 25 +
.../tests/utils/url.ts | 33 +-
packages/react-resizable-panels/CHANGELOG.md | 3 +
packages/react-resizable-panels/src/Panel.ts | 81 ++-
.../react-resizable-panels/src/PanelGroup.ts | 608 +++++++++---------
.../src/hooks/usePanelGroupLayoutValidator.ts | 118 ----
.../src/hooks/useWindowSplitterBehavior.ts | 24 +-
packages/react-resizable-panels/src/index.ts | 18 +-
packages/react-resizable-panels/src/types.ts | 11 +-
.../react-resizable-panels/src/utils/group.ts | 93 ++-
21 files changed, 1205 insertions(+), 1052 deletions(-)
delete mode 100644 packages/react-resizable-panels-website/tests/DevelopmentWarnings.spec.ts
create mode 100644 packages/react-resizable-panels-website/tests/DevelopmentWarningsAndErrors.spec.ts
create mode 100644 packages/react-resizable-panels-website/tests/Panel-StaticUnits.spec.ts
delete mode 100644 packages/react-resizable-panels-website/tests/usePanelGroupLayoutValidator.spec.ts
delete mode 100644 packages/react-resizable-panels/src/hooks/usePanelGroupLayoutValidator.ts
diff --git a/packages/react-resizable-panels-website/src/components/Icon.tsx b/packages/react-resizable-panels-website/src/components/Icon.tsx
index 1275d070a..8dea6313f 100644
--- a/packages/react-resizable-panels-website/src/components/Icon.tsx
+++ b/packages/react-resizable-panels-website/src/components/Icon.tsx
@@ -13,7 +13,8 @@ export type IconType =
| "resize-horizontal"
| "resize-vertical"
| "search"
- | "typescript";
+ | "typescript"
+ | "warning";
export default function Icon({
className = "",
@@ -75,6 +76,10 @@ export default function Icon({
path =
"M3,3H21V21H3V3M13.71,17.86C14.21,18.84 15.22,19.59 16.8,19.59C18.4,19.59 19.6,18.76 19.6,17.23C19.6,15.82 18.79,15.19 17.35,14.57L16.93,14.39C16.2,14.08 15.89,13.87 15.89,13.37C15.89,12.96 16.2,12.64 16.7,12.64C17.18,12.64 17.5,12.85 17.79,13.37L19.1,12.5C18.55,11.54 17.77,11.17 16.7,11.17C15.19,11.17 14.22,12.13 14.22,13.4C14.22,14.78 15.03,15.43 16.25,15.95L16.67,16.13C17.45,16.47 17.91,16.68 17.91,17.26C17.91,17.74 17.46,18.09 16.76,18.09C15.93,18.09 15.45,17.66 15.09,17.06L13.71,17.86M13,11.25H8V12.75H9.5V20H11.25V12.75H13V11.25Z";
break;
+ case "warning":
+ path =
+ "M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M13,13V7H11V13H13M13,17V15H11V17H13Z";
+ break;
}
return (
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 1ee087d2e..bb35b4569 100644
--- a/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx
+++ b/packages/react-resizable-panels-website/src/routes/EndToEndTesting/index.tsx
@@ -1,35 +1,47 @@
-import { ChangeEvent, useRef, useState } from "react";
+import {
+ ChangeEvent,
+ Component,
+ ErrorInfo,
+ PropsWithChildren,
+ useRef,
+ useState,
+} from "react";
import {
ImperativePanelGroupHandle,
ImperativePanelHandle,
+ getAvailableGroupSizePixels,
} from "react-resizable-panels";
-import { urlToUrlData, PanelGroupForUrlData } from "../../utils/UrlData";
+import { urlPanelGroupToPanelGroup, urlToUrlData } from "../../utils/UrlData";
import DebugLog, { ImperativeDebugLogHandle } from "../examples/DebugLog";
-import "./styles.css";
-import styles from "./styles.module.css";
+import { useLayoutEffect } from "react";
import {
assertImperativePanelGroupHandle,
assertImperativePanelHandle,
} from "../../../tests/utils/assert";
-import { useLayoutEffect } from "react";
-import { Metadata } from "../../../tests/utils/url";
+import "./styles.css";
+import styles from "./styles.module.css";
// Special route that can be configured via URL parameters.
-export default function EndToEndTesting() {
- const [metadata, setMetadata] = useState(() => {
- const url = new URL(
- typeof window !== undefined ? window.location.href : ""
- );
- const metadata = url.searchParams.get("metadata");
+class ErrorBoundary extends Component {
+ state = {
+ didError: false,
+ };
- return metadata ? JSON.parse(metadata) : null;
- });
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ console.error(error);
+ }
+
+ render() {
+ return this.state.didError ? null : this.props.children;
+ }
+}
- const [urlPanelGroup, setUrlPanelGroup] = useState(() => {
+function EndToEndTesting() {
+ const [urlData, setUrlData] = useState(() => {
const url = new URL(
typeof window !== undefined ? window.location.href : ""
);
@@ -43,52 +55,31 @@ export default function EndToEndTesting() {
typeof window !== undefined ? window.location.href : ""
);
- setUrlPanelGroup(urlToUrlData(url));
-
- const metadata = url.searchParams.get("metadata");
- setMetadata(metadata ? JSON.parse(metadata) : null);
+ setUrlData(urlToUrlData(url));
});
}, []);
useLayoutEffect(() => {
+ const calculatePanelSize = (panelElement: HTMLElement) => {
+ if (panelElement.childElementCount > 0) {
+ return; // Don't override nested groups
+ }
+
+ const panelSize = parseFloat(panelElement.style.flexGrow);
+
+ const panelGroupElement = panelElement.parentElement!;
+ const groupId = panelGroupElement.getAttribute("data-panel-group-id")!;
+ const panelGroupPixels = getAvailableGroupSizePixels(groupId);
+
+ panelElement.textContent = `${panelSize.toFixed(1)}%\n${(
+ (panelSize / 100) *
+ panelGroupPixels
+ ).toFixed(1)}px`;
+ };
+
const observer = new MutationObserver((mutationRecords) => {
mutationRecords.forEach((mutationRecord) => {
- const panelElement = mutationRecord.target as HTMLElement;
- if (panelElement.childElementCount > 0) {
- return;
- }
-
- const panelSize = parseFloat(panelElement.style.flexGrow);
-
- const panelGroupElement = panelElement.parentElement!;
- const groupId = panelGroupElement.getAttribute("data-panel-group-id");
- const direction = panelGroupElement.getAttribute(
- "data-panel-group-direction"
- );
- const resizeHandles = Array.from(
- panelGroupElement.querySelectorAll(
- `[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`
- )
- ) as HTMLElement[];
-
- let panelGroupPixels =
- direction === "horizontal"
- ? panelGroupElement.offsetWidth
- : panelGroupElement.offsetHeight;
- if (direction === "horizontal") {
- panelGroupPixels -= resizeHandles.reduce((accumulated, handle) => {
- return accumulated + handle.offsetWidth;
- }, 0);
- } else {
- panelGroupPixels -= resizeHandles.reduce((accumulated, handle) => {
- return accumulated + handle.offsetHeight;
- }, 0);
- }
-
- panelElement.textContent = `${panelSize.toFixed(1)}%\n${(
- (panelSize / 100) *
- panelGroupPixels
- ).toFixed(1)}px`;
+ calculatePanelSize(mutationRecord.target as HTMLElement);
});
});
@@ -97,6 +88,8 @@ export default function EndToEndTesting() {
observer.observe(element, {
attributes: true,
});
+
+ calculatePanelSize(element as HTMLElement);
});
return () => {
@@ -114,6 +107,10 @@ export default function EndToEndTesting() {
Map
>(new Map());
+ const children = urlData
+ ? urlPanelGroupToPanelGroup(urlData, debugLogRef, idToRefMapRef)
+ : null;
+
const onLayoutInputChange = (event: ChangeEvent) => {
const value = event.currentTarget.value;
setLayoutString(value);
@@ -208,17 +205,16 @@ export default function EndToEndTesting() {
-
- {urlPanelGroup && (
-
- )}
-
+ {children}
);
}
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
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 c5b56521f..4c00c138d 100644
--- a/packages/react-resizable-panels-website/src/routes/examples/ExternalPersistence.tsx
+++ b/packages/react-resizable-panels-website/src/routes/examples/ExternalPersistence.tsx
@@ -6,6 +6,7 @@ import ResizeHandle from "../../components/ResizeHandle";
import Example from "./Example";
import styles from "./shared.module.css";
+import Icon from "../../components/Icon";
export default function ExternalPersistence() {
return (
@@ -22,11 +23,13 @@ export default function ExternalPersistence() {
layout is saved as part of the URL hash.
+
Note the storage
API is synchronous. If an
async source is used (e.g. a database) then values should be
pre-fetched during the initial render (e.g. using Suspense).
+
Note calls to storage.setItem
are debounced by{" "}
100ms. Depending on your implementation, you may
wish to use a larger interval than that.
diff --git a/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx b/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx
index b681f03a6..44971a0e6 100644
--- a/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx
+++ b/packages/react-resizable-panels-website/src/routes/examples/PixelBasedLayouts.tsx
@@ -1,8 +1,4 @@
-import {
- Panel,
- PanelGroup,
- usePanelGroupLayoutValidator,
-} from "react-resizable-panels";
+import { Panel, PanelGroup } from "react-resizable-panels";
import ResizeHandle from "../../components/ResizeHandle";
@@ -14,28 +10,9 @@ import sharedStyles from "./shared.module.css";
import { PropsWithChildren } from "react";
import Code from "../../components/Code";
-import { dir } from "console";
+import Icon from "../../components/Icon";
export default function PixelBasedLayouts() {
- const validateLayoutLeft = usePanelGroupLayoutValidator({
- maxPixels: 200,
- minPixels: 100,
- position: "left",
- });
-
- const validateLayoutRight = usePanelGroupLayoutValidator({
- collapseBelowPixels: 100,
- maxPixels: 300,
- minPixels: 200,
- position: "right",
- });
-
- const validateLayoutTop = usePanelGroupLayoutValidator({
- maxPixels: 125,
- minPixels: 75,
- position: "top",
- });
-
return (
@@ -45,23 +22,28 @@ export default function PixelBasedLayouts() {
→Pixel based layouts
- Resizable panels typically use percentage-based layout constraints.
- PanelGroup
also supports custom validation functions for
- pixel-base constraints.
+ Resizable panels typically use percentage-based layout constraints, but
+ pixel units are also supported via the units
prop. The
+ example below shows a horizontal panel group where the first panel is
+ limited to a range of 100-200 pixels.
-
- The easiest way to do this is with the{" "}
- usePanelGroupLayoutValidator
hook, as shown in the example
- below.
+
+
+ Pixel units should be used sparingly because they require more complex
+ layout logic.
-
+
100px - 200px
@@ -88,7 +70,7 @@ export default function PixelBasedLayouts() {
Panels with pixel constraints can also be configured to collapse as
- shown below
+ shown below.
@@ -96,7 +78,6 @@ export default function PixelBasedLayouts() {
left
@@ -106,7 +87,14 @@ export default function PixelBasedLayouts() {
middle
-
+
200px - 300px
@@ -123,52 +111,6 @@ export default function PixelBasedLayouts() {
language="jsx"
showLineNumbers
/>
-
-
Vertical groups can also be managed with this hook.
-
-
-
-
-
-
-
-
-
- middle
-
-
-
- bottom
-
-
-
-
-
-
-
- The validateLayout
prop can also be used directly to
- implement an entirely custom layout.
-
-
-
);
}
@@ -197,55 +139,21 @@ function Size({
}
const CODE_HOOK = `
-const validateLayout = usePanelGroupLayoutValidator({
- maxPixels: 200,
- minPixels: 100,
- position: "left",
-});
-
-
- {/* Panels ... */}
+
+
+
+
+
+
`;
const CODE_HOOK_COLLAPSIBLE = `
-const validateLayout = usePanelGroupLayoutValidator({
- collapseBelowPixels: 100,
- maxPixels: 300,
- minPixels: 200,
- position: "right",
-});
-
-
- {/* Panels ... */}
+
+
+
+
+
+
`;
-
-const CODE_HOOK_VERTICAL = `
-const validateLayout = usePanelGroupLayoutValidator({
- maxPixels: 125,
- minPixels: 75,
- position: "top",
-});
-
-
- {/* Panels ... */}
-
-`;
-
-const CODE_CUSTOM = `
-function validateLayout({
- availableHeight,
- availableWidth,
- nextSizes,
- prevSizes,
-}: {
- availableHeight: number;
- availableWidth: number;
- nextSizes: number[];
- prevSizes: number[];
-}): number[] {
- // Compute and return an array of sizes
- // Note the values in the sizes array should total 100
-}
-`;
diff --git a/packages/react-resizable-panels-website/src/routes/examples/shared.module.css b/packages/react-resizable-panels-website/src/routes/examples/shared.module.css
index 1baf009c7..b94c09bf8 100644
--- a/packages/react-resizable-panels-website/src/routes/examples/shared.module.css
+++ b/packages/react-resizable-panels-website/src/routes/examples/shared.module.css
@@ -84,11 +84,20 @@
}
.WarningBlock {
- display: inline-block;
+ display: inline-flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 1ch;
background: var(--color-warning-background);
- padding: 0.25em 1ch;
+ padding: 0.5em;
border-radius: 0.5rem;
}
+.WarningIcon {
+ flex: 0 0 2rem;
+ width: 2rem;
+ height: 2rem;
+}
.InlineCode {
margin-right: 1.5ch;
diff --git a/packages/react-resizable-panels-website/src/utils/UrlData.ts b/packages/react-resizable-panels-website/src/utils/UrlData.ts
index 3beb51b79..4a9875ce4 100644
--- a/packages/react-resizable-panels-website/src/utils/UrlData.ts
+++ b/packages/react-resizable-panels-website/src/utils/UrlData.ts
@@ -19,9 +19,8 @@ import {
PanelResizeHandle,
PanelResizeHandleOnDragging,
PanelResizeHandleProps,
- usePanelGroupLayoutValidator,
+ PanelUnits,
} from "react-resizable-panels";
-import { Metadata } from "../../tests/utils/url";
import { ImperativeDebugLogHandle } from "../routes/examples/DebugLog";
type UrlPanel = {
@@ -30,11 +29,12 @@ type UrlPanel = {
collapsible?: boolean;
defaultSize?: number | null;
id?: string | null;
- maxSize?: number;
+ maxSize?: number | null;
minSize?: number;
order?: number | null;
style?: CSSProperties;
type: "UrlPanel";
+ units: PanelUnits;
};
type UrlPanelGroup = {
@@ -112,6 +112,7 @@ function UrlPanelToData(urlPanel: ReactElement): UrlPanel {
order: urlPanel.props.order,
style: urlPanel.props.style,
type: "UrlPanel",
+ units: urlPanel.props.units ?? "relative",
};
}
@@ -209,16 +210,16 @@ function urlPanelToPanel(
order: urlPanel.order,
ref: refSetter,
style: urlPanel.style,
+ units: urlPanel.units,
},
urlPanel.children.map((child, index) => {
if (isUrlPanelGroup(child)) {
- return createElement(PanelGroupForUrlData, {
+ return urlPanelGroupToPanelGroup(
+ child,
debugLogRef,
idToRefMapRef,
- key: index,
- metadata: null,
- urlPanelGroup: child,
- });
+ index
+ );
} else {
return createElement(Fragment, { key: index }, child);
}
@@ -226,19 +227,14 @@ function urlPanelToPanel(
);
}
-export function PanelGroupForUrlData({
- debugLogRef,
- idToRefMapRef,
- metadata,
- urlPanelGroup,
-}: {
- debugLogRef: RefObject;
+export function urlPanelGroupToPanelGroup(
+ urlPanelGroup: UrlPanelGroup,
+ debugLogRef: RefObject,
idToRefMapRef: RefObject<
Map
- >;
- metadata: Metadata | null;
- urlPanelGroup: UrlPanelGroup;
-}): ReactElement {
+ >,
+ key?: any
+): ReactElement {
let onLayout: PanelGroupOnLayout | undefined = undefined;
let refSetter;
@@ -261,9 +257,6 @@ export function PanelGroupForUrlData({
};
}
- const config = metadata ? metadata.usePanelGroupLayoutValidator : undefined;
- const validateLayout = usePanelGroupLayoutValidator((config ?? {}) as any);
-
return createElement(
PanelGroup,
{
@@ -271,10 +264,10 @@ export function PanelGroupForUrlData({
className: "PanelGroup",
direction: urlPanelGroup.direction,
id: urlPanelGroup.id,
+ key: key,
onLayout,
ref: refSetter,
style: urlPanelGroup.style,
- validateLayout: config ? validateLayout : undefined,
},
urlPanelGroup.children.map((child, index) => {
if (isUrlPanel(child)) {
diff --git a/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts b/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts
index 780b7fa83..8984f889e 100644
--- a/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts
+++ b/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts
@@ -38,7 +38,10 @@ test.describe("cursor style", () => {
createElement(
PanelGroup,
{ direction },
- createElement(Panel, { defaultSize: 50, id: "first-panel" }),
+ createElement(Panel, {
+ defaultSize: 50,
+ id: "first-panel",
+ }),
createElement(PanelResizeHandle),
createElement(Panel, { defaultSize: 50, id: "last-panel" })
)
diff --git a/packages/react-resizable-panels-website/tests/DevelopmentWarnings.spec.ts b/packages/react-resizable-panels-website/tests/DevelopmentWarnings.spec.ts
deleted file mode 100644
index 27eb106ee..000000000
--- a/packages/react-resizable-panels-website/tests/DevelopmentWarnings.spec.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { expect, test } from "@playwright/test";
-import { createElement } from "react";
-import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
-
-import { goToUrl, updateUrl } from "./utils/url";
-
-function createElements({
- numPanels,
- omitIdProp = false,
- omitOrderProp = false,
-}: {
- numPanels: 1 | 2;
- omitIdProp?: boolean;
- omitOrderProp?: boolean;
-}) {
- const panels = [
- createElement(Panel, {
- collapsible: true,
- defaultSize: numPanels === 2 ? 50 : 100,
- id: omitIdProp ? undefined : "left",
- order: omitOrderProp ? undefined : 1,
- }),
- ];
-
- if (numPanels === 2) {
- panels.push(
- createElement(PanelResizeHandle, { id: "right-handle" }),
- createElement(Panel, {
- collapsible: true,
- defaultSize: 50,
- id: omitIdProp ? undefined : "right",
- order: omitOrderProp ? undefined : 2,
- })
- );
- }
-
- return createElement(
- PanelGroup,
- { direction: "horizontal", id: "group" },
- ...panels
- );
-}
-
-test.describe("Development warnings", () => {
- test.describe("conditional panels", () => {
- test("should warning about missing id props", async ({ page }) => {
- await goToUrl(
- page,
- createElements({
- omitIdProp: true,
- numPanels: 1,
- })
- );
-
- const warnings: string[] = [];
- page.on("console", (message) => {
- if (message.type() === "warning") {
- warnings.push(message.text());
- }
- });
-
- await updateUrl(
- page,
- createElements({
- omitIdProp: true,
- numPanels: 2,
- })
- );
- expect(warnings).toHaveLength(1);
- expect(warnings[0]).toContain("id and order props recommended");
-
- await updateUrl(
- page,
- createElements({
- omitIdProp: true,
- numPanels: 1,
- })
- );
- expect(warnings).toHaveLength(1);
- });
-
- test("should warning about missing order props", async ({ page }) => {
- await goToUrl(
- page,
- createElements({
- omitOrderProp: true,
- numPanels: 1,
- })
- );
-
- const warnings: string[] = [];
- page.on("console", (message) => {
- if (message.type() === "warning") {
- warnings.push(message.text());
- }
- });
-
- await updateUrl(
- page,
- createElements({
- omitOrderProp: true,
- numPanels: 2,
- })
- );
- expect(warnings).toHaveLength(1);
- expect(warnings[0]).toContain("id and order props recommended");
-
- await updateUrl(
- page,
- createElements({
- omitOrderProp: true,
- numPanels: 1,
- })
- );
- expect(warnings).toHaveLength(1);
- });
-
- test("should not warn if id an order props are specified", async ({
- page,
- }) => {
- await goToUrl(
- page,
- createElements({
- numPanels: 1,
- })
- );
-
- const warnings: string[] = [];
- page.on("console", (message) => {
- if (message.type() === "warning") {
- warnings.push(message.text());
- }
- });
-
- await updateUrl(
- page,
- createElements({
- numPanels: 2,
- })
- );
- expect(warnings).toHaveLength(0);
- });
- });
-});
diff --git a/packages/react-resizable-panels-website/tests/DevelopmentWarningsAndErrors.spec.ts b/packages/react-resizable-panels-website/tests/DevelopmentWarningsAndErrors.spec.ts
new file mode 100644
index 000000000..1d6866809
--- /dev/null
+++ b/packages/react-resizable-panels-website/tests/DevelopmentWarningsAndErrors.spec.ts
@@ -0,0 +1,283 @@
+import { Page, expect, test } from "@playwright/test";
+import { createElement } from "react";
+import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
+
+import { goToUrl, updateUrl } from "./utils/url";
+
+function createElements({
+ numPanels,
+ omitIdProp = false,
+ omitOrderProp = false,
+}: {
+ numPanels: 1 | 2;
+ omitIdProp?: boolean;
+ omitOrderProp?: boolean;
+}) {
+ const panels = [
+ createElement(Panel, {
+ collapsible: true,
+ defaultSize: numPanels === 2 ? 50 : 100,
+ id: omitIdProp ? undefined : "left",
+ order: omitOrderProp ? undefined : 1,
+ }),
+ ];
+
+ if (numPanels === 2) {
+ panels.push(
+ createElement(PanelResizeHandle, { id: "right-handle" }),
+ createElement(Panel, {
+ collapsible: true,
+ defaultSize: 50,
+ id: omitIdProp ? undefined : "right",
+ order: omitOrderProp ? undefined : 2,
+ })
+ );
+ }
+
+ return createElement(
+ PanelGroup,
+ { direction: "horizontal", id: "group" },
+ ...panels
+ );
+}
+
+async function flushMessages(page: Page) {
+ await goToUrl(page, createElement(PanelGroup));
+}
+
+test.describe("Development warnings and errors", () => {
+ const errors: string[] = [];
+ const warnings: string[] = [];
+
+ test.beforeEach(({ page }) => {
+ errors.splice(0);
+ warnings.splice(0);
+
+ page.on("console", (message) => {
+ switch (message.type()) {
+ case "error":
+ errors.push(message.text());
+ break;
+ case "warning":
+ warnings.push(message.text());
+ break;
+ }
+ });
+ });
+
+ test.describe("conditional panels", () => {
+ test("should warning about missing id props", async ({ page }) => {
+ await goToUrl(
+ page,
+ createElements({
+ omitIdProp: true,
+ numPanels: 1,
+ })
+ );
+
+ await updateUrl(
+ page,
+ createElements({
+ omitIdProp: true,
+ numPanels: 2,
+ })
+ );
+ expect(warnings).toHaveLength(1);
+ expect(warnings[0]).toContain("id and order props recommended");
+
+ await updateUrl(
+ page,
+ createElements({
+ omitIdProp: true,
+ numPanels: 1,
+ })
+ );
+ expect(warnings).toHaveLength(1);
+ });
+
+ test("should warning about missing order props", async ({ page }) => {
+ await goToUrl(
+ page,
+ createElements({
+ omitOrderProp: true,
+ numPanels: 1,
+ })
+ );
+
+ await updateUrl(
+ page,
+ createElements({
+ omitOrderProp: true,
+ numPanels: 2,
+ })
+ );
+ expect(warnings).toHaveLength(1);
+ expect(warnings[0]).toContain("id and order props recommended");
+
+ await updateUrl(
+ page,
+ createElements({
+ omitOrderProp: true,
+ numPanels: 1,
+ })
+ );
+ expect(warnings).toHaveLength(1);
+ });
+
+ test("should not warn if id an order props are specified", async ({
+ page,
+ }) => {
+ await goToUrl(
+ page,
+ createElements({
+ numPanels: 1,
+ })
+ );
+
+ await updateUrl(
+ page,
+ createElements({
+ numPanels: 2,
+ })
+ );
+ expect(warnings).toHaveLength(0);
+ });
+
+ test("should throw if defaultSize is less than 0", async ({ page }) => {
+ await goToUrl(
+ page,
+ createElement(
+ PanelGroup,
+ { direction: "horizontal" },
+ createElement(Panel, { defaultSize: -1 })
+ )
+ );
+
+ await flushMessages(page);
+
+ expect(errors).not.toHaveLength(0);
+ expect(errors).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining("Invalid Panel defaultSize provided, -1"),
+ ])
+ );
+ });
+
+ test("should throw if defaultSize is greater than 100 and units are relative", async ({
+ page,
+ }) => {
+ await goToUrl(
+ page,
+ createElement(
+ PanelGroup,
+ { direction: "horizontal" },
+ createElement(Panel, { defaultSize: 400 })
+ )
+ );
+
+ await flushMessages(page);
+
+ expect(errors).not.toHaveLength(0);
+ expect(errors).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining("Invalid Panel defaultSize provided, 400"),
+ ])
+ );
+ });
+
+ test("should not throw if defaultSize is greater than 100 and units are static", async ({
+ page,
+ }) => {
+ await goToUrl(
+ page,
+ createElement(
+ PanelGroup,
+ { direction: "horizontal" },
+ createElement(Panel, { defaultSize: 400, units: "static" })
+ )
+ );
+
+ await flushMessages(page);
+
+ expect(errors).toHaveLength(0);
+ });
+
+ test("should warn if defaultSize is less than minSize", async ({
+ page,
+ }) => {
+ await goToUrl(
+ page,
+ createElement(
+ PanelGroup,
+ { direction: "horizontal" },
+ createElement(Panel, { defaultSize: 25, minSize: 50 }),
+ createElement(PanelResizeHandle),
+ createElement(Panel)
+ )
+ );
+
+ await flushMessages(page);
+
+ expect(errors).not.toHaveLength(0);
+ expect(errors).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining(
+ "Panel minSize (50) cannot be greater than defaultSize (25)"
+ ),
+ ])
+ );
+ });
+
+ test("should warn if defaultSize is greater than maxSize", async ({
+ page,
+ }) => {
+ await goToUrl(
+ page,
+ createElement(
+ PanelGroup,
+ { direction: "horizontal" },
+ createElement(Panel, { defaultSize: 75, maxSize: 50 }),
+ createElement(PanelResizeHandle),
+ createElement(Panel)
+ )
+ );
+
+ await flushMessages(page);
+
+ expect(errors).not.toHaveLength(0);
+ expect(errors).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining(
+ "Panel maxSize (50) cannot be less than defaultSize (75)"
+ ),
+ ])
+ );
+ });
+
+ test("should warn if total defaultSizes do not add up to 100", async ({
+ page,
+ }) => {
+ await goToUrl(
+ page,
+ createElement(
+ PanelGroup,
+ { direction: "horizontal" },
+ createElement(Panel, { defaultSize: 25 }),
+ createElement(PanelResizeHandle),
+ createElement(Panel, { defaultSize: 25 })
+ )
+ );
+
+ await flushMessages(page);
+
+ expect(errors).not.toHaveLength(0);
+ expect(errors).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining(
+ "Invalid panel group configuration; default panel sizes should total 100 but was 50"
+ ),
+ ])
+ );
+ });
+ });
+});
diff --git a/packages/react-resizable-panels-website/tests/Panel-StaticUnits.spec.ts b/packages/react-resizable-panels-website/tests/Panel-StaticUnits.spec.ts
new file mode 100644
index 000000000..f27c3fb35
--- /dev/null
+++ b/packages/react-resizable-panels-website/tests/Panel-StaticUnits.spec.ts
@@ -0,0 +1,280 @@
+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 } from "./utils/url";
+
+async function goToUrlHelper(
+ page: Page,
+ props: {
+ leftPanelProps?: PanelProps;
+ leftResizeHandleProps?: PanelResizeHandleProps;
+ middlePanelProps?: PanelProps;
+ panelGroupProps?: PanelGroupProps;
+ rightPanelProps?: PanelProps;
+ rightResizeHandleProps?: PanelResizeHandleProps;
+ } = {}
+) {
+ await goToUrl(
+ page,
+ createElement(
+ PanelGroup,
+ { direction: "horizontal", id: "group", ...props.panelGroupProps },
+ createElement(Panel, {
+ id: "left-panel",
+ ...props.leftPanelProps,
+ }),
+ createElement(PanelResizeHandle, {
+ id: "left-resize-handle",
+ ...props.leftResizeHandleProps,
+ }),
+ createElement(Panel, {
+ id: "middle-panel",
+ ...props.middlePanelProps,
+ }),
+ createElement(PanelResizeHandle, {
+ id: "right-resize-handle",
+ ...props.rightResizeHandleProps,
+ }),
+ createElement(Panel, {
+ id: "right-panel",
+ ...props.rightPanelProps,
+ })
+ )
+ );
+}
+
+test.describe("Static Panel units", () => {
+ test.describe("initial layout", () => {
+ test("should observe max size constraint for default layout", async ({
+ page,
+ }) => {
+ // Static left panel
+ await goToUrlHelper(page, {
+ leftPanelProps: { maxSize: 100, minSize: 50, units: "static" },
+ });
+ const leftPanel = page.locator('[data-panel-id="left-panel"]');
+ await verifyPanelSizePixels(leftPanel, 100);
+
+ // Static middle panel
+ await goToUrlHelper(page, {
+ middlePanelProps: { maxSize: 100, minSize: 50, units: "static" },
+ });
+ const middlePanel = page.locator('[data-panel-id="middle-panel"]');
+ await verifyPanelSizePixels(middlePanel, 100);
+
+ // Static right panel
+ await goToUrlHelper(page, {
+ rightPanelProps: { maxSize: 100, minSize: 50, units: "static" },
+ });
+ 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: { maxSize: 300, minSize: 200, units: "static" },
+ });
+
+ 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: { maxSize: 100, minSize: 50, units: "static" },
+ });
+
+ 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: { maxSize: 100, minSize: 50, units: "static" },
+ });
+
+ 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: { maxSize: 100, minSize: 50, units: "static" },
+ });
+
+ const leftPanel = page.locator("[data-panel]").first();
+
+ await imperativeResizePanel(page, "left-panel", 80);
+ await verifyPanelSizePixels(leftPanel, 100);
+
+ await imperativeResizePanel(page, "left-panel", 4);
+ await verifyPanelSizePixels(leftPanel, 50);
+ });
+
+ test("should honor min/max constraint when indirectly resizing via imperative Panel API", async ({
+ page,
+ }) => {
+ await goToUrlHelper(page, {
+ rightPanelProps: { maxSize: 100, minSize: 50, units: "static" },
+ });
+
+ const rightPanel = page.locator("[data-panel]").last();
+
+ await imperativeResizePanel(page, "middle-panel", 1);
+ await verifyPanelSizePixels(rightPanel, 100);
+
+ await imperativeResizePanel(page, "middle-panel", 98);
+ await verifyPanelSizePixels(rightPanel, 50);
+ });
+
+ test("should support collapsable panels", async ({ page }) => {
+ await goToUrlHelper(page, {
+ leftPanelProps: {
+ collapsible: true,
+ minSize: 100,
+ maxSize: 200,
+ units: "static",
+ },
+ });
+
+ const leftPanel = page.locator("[data-panel]").first();
+
+ await imperativeResizePanel(page, "left-panel", 25);
+ await verifyPanelSizePixels(leftPanel, 100);
+
+ await imperativeResizePanel(page, "left-panel", 10);
+ await verifyPanelSizePixels(leftPanel, 0);
+
+ await imperativeResizePanel(page, "left-panel", 15);
+ await verifyPanelSizePixels(leftPanel, 100);
+ });
+ });
+
+ test("should observe min size constraint if the overall group size shrinks", async ({
+ page,
+ }) => {
+ await goToUrlHelper(page, {
+ leftPanelProps: {
+ defaultSize: 50,
+ maxSize: 100,
+ minSize: 50,
+ units: "static",
+ },
+ });
+ const leftPanel = page.locator('[data-panel-id="left-panel"]');
+ await verifyPanelSizePixels(leftPanel, 50);
+
+ await page.setViewportSize({ width: 300, height: 300 });
+ await verifyPanelSizePixels(leftPanel, 50);
+ });
+
+ test("should observe max size constraint if the overall group size expands", async ({
+ page,
+ }) => {
+ await goToUrlHelper(page, {
+ leftPanelProps: {
+ defaultSize: 100,
+ maxSize: 100,
+ minSize: 50,
+ units: "static",
+ },
+ });
+
+ const leftPanel = page.locator('[data-panel-id="left-panel"]');
+
+ await verifyPanelSizePixels(leftPanel, 100);
+
+ await page.setViewportSize({ width: 500, height: 300 });
+ await verifyPanelSizePixels(leftPanel, 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",
+ minSize: 50,
+ maxSize: 75,
+ units: "static",
+ }),
+ createElement(PanelResizeHandle, {
+ id: "first-resize-handle",
+ }),
+ createElement(Panel, {
+ id: "second-panel",
+ }),
+ createElement(PanelResizeHandle, {
+ id: "second-resize-handle",
+ }),
+ createElement(Panel, {
+ id: "third-panel",
+ }),
+ createElement(PanelResizeHandle, {
+ id: "third-resize-handle",
+ }),
+ createElement(Panel, {
+ id: "fourth-panel",
+ minSize: 50,
+ maxSize: 75,
+ units: "static",
+ })
+ )
+ );
+
+ 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);
+ });
+});
diff --git a/packages/react-resizable-panels-website/tests/usePanelGroupLayoutValidator.spec.ts b/packages/react-resizable-panels-website/tests/usePanelGroupLayoutValidator.spec.ts
deleted file mode 100644
index 425b2fbfe..000000000
--- a/packages/react-resizable-panels-website/tests/usePanelGroupLayoutValidator.spec.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import { Page, test } from "@playwright/test";
-import { createElement } from "react";
-import {
- Panel,
- PanelGroup,
- PanelGroupProps,
- PanelProps,
- PanelResizeHandle,
- usePanelGroupLayoutValidator,
-} from "react-resizable-panels";
-
-import {
- dragResizeTo,
- imperativeResizePanel,
- imperativeResizePanelGroup,
- verifyPanelSizePixels,
-} from "./utils/panels";
-import { goToUrl } from "./utils/url";
-
-type HookConfig = Parameters[0];
-
-async function goToUrlHelper(
- page: Page,
- panelProps: {
- leftPanelProps?: PanelProps;
- middlePanelProps?: PanelProps;
- panelGroupProps?: PanelGroupProps;
- rightPanelProps?: PanelProps;
- } = {},
- hookConfig?: Partial
-) {
- await goToUrl(
- page,
- createElement(
- PanelGroup,
- { direction: "horizontal", id: "group", ...panelProps.panelGroupProps },
- createElement(Panel, {
- id: "left-panel",
- minSize: 5,
- ...panelProps.leftPanelProps,
- }),
- createElement(PanelResizeHandle),
- createElement(Panel, {
- id: "middle-panel",
- minSize: 5,
- ...panelProps.middlePanelProps,
- }),
- createElement(PanelResizeHandle),
- createElement(Panel, {
- id: "right-panel",
- minSize: 5,
- ...panelProps.rightPanelProps,
- })
- ),
- {
- usePanelGroupLayoutValidator: {
- minPixels: 50,
- maxPixels: 100,
- position: "left",
- ...hookConfig,
- },
- }
- );
-}
-
-test.describe("usePanelGroupLayoutValidator", () => {
- test.describe("initial layout", () => {
- test("should observe max size constraint for default layout", async ({
- page,
- }) => {
- await goToUrlHelper(page, {
- middlePanelProps: { defaultSize: 20 },
- rightPanelProps: { defaultSize: 20 },
- });
-
- const leftPanel = page.locator("[data-panel]").first();
- await verifyPanelSizePixels(leftPanel, 100);
- });
-
- test("should observe min size constraint for default layout", async ({
- page,
- }) => {
- await goToUrlHelper(page, {
- middlePanelProps: { defaultSize: 45 },
- rightPanelProps: { defaultSize: 45 },
- });
-
- const leftPanel = page.locator("[data-panel]").first();
- await verifyPanelSizePixels(leftPanel, 50);
- });
-
- test("should honor min/max constraint when resizing via keyboard", async ({
- page,
- }) => {
- await goToUrlHelper(page);
-
- 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);
-
- const leftPanel = page.locator("[data-panel]").first();
-
- await dragResizeTo(page, "left-panel", { size: 50 });
- await verifyPanelSizePixels(leftPanel, 100);
-
- await dragResizeTo(page, "left-panel", { size: 0 });
- await verifyPanelSizePixels(leftPanel, 50);
- });
-
- test("should honor min/max constraint when resizing via imperative Panel API", async ({
- page,
- }) => {
- await goToUrlHelper(page);
-
- const leftPanel = page.locator("[data-panel]").first();
-
- await imperativeResizePanel(page, "left-panel", 80);
- await verifyPanelSizePixels(leftPanel, 100);
-
- await imperativeResizePanel(page, "left-panel", 4);
- await verifyPanelSizePixels(leftPanel, 50);
- });
-
- test("should honor min/max constraint when resizing via imperative PanelGroup API", async ({
- page,
- }) => {
- await goToUrlHelper(page);
-
- const leftPanel = page.locator("[data-panel]").first();
-
- await imperativeResizePanelGroup(page, "group", [80, 10, 10]);
- await verifyPanelSizePixels(leftPanel, 100);
-
- await imperativeResizePanelGroup(page, "group", [5, 55, 40]);
- await verifyPanelSizePixels(leftPanel, 50);
- });
-
- test("should support collapsable panels", async ({ page }) => {
- await goToUrlHelper(
- page,
- {},
- {
- collapseBelowPixels: 50,
- minPixels: 100,
- maxPixels: 200,
- }
- );
-
- const leftPanel = page.locator("[data-panel]").first();
-
- await imperativeResizePanel(page, "left-panel", 25);
- await verifyPanelSizePixels(leftPanel, 100);
-
- await imperativeResizePanel(page, "left-panel", 10);
- await verifyPanelSizePixels(leftPanel, 0);
-
- await imperativeResizePanel(page, "left-panel", 15);
- await verifyPanelSizePixels(leftPanel, 100);
- });
- });
-});
diff --git a/packages/react-resizable-panels-website/tests/utils/panels.ts b/packages/react-resizable-panels-website/tests/utils/panels.ts
index f39aea778..29b8c5481 100644
--- a/packages/react-resizable-panels-website/tests/utils/panels.ts
+++ b/packages/react-resizable-panels-website/tests/utils/panels.ts
@@ -9,6 +9,31 @@ type Operation = {
size: number;
};
+export async function dragResizeBy(
+ page: Page,
+ panelResizeHandleId: string,
+ delta: number
+) {
+ const dragHandle = page.locator(
+ `[data-panel-resize-handle-id="${panelResizeHandleId}"]`
+ );
+ const direction = await dragHandle.getAttribute(
+ "data-panel-group-direction"
+ )!;
+
+ let dragHandleRect = (await dragHandle.boundingBox())!;
+ let pageX = dragHandleRect.x + dragHandleRect.width / 2;
+ let pageY = dragHandleRect.y + dragHandleRect.height / 2;
+
+ await page.mouse.move(pageX, pageY);
+ await page.mouse.down();
+ await page.mouse.move(
+ direction === "horizontal" ? pageX + delta : pageX,
+ direction === "vertical" ? pageY + delta : pageY
+ );
+ await page.mouse.up();
+}
+
export async function dragResizeTo(
page: Page,
panelId: string,
diff --git a/packages/react-resizable-panels-website/tests/utils/url.ts b/packages/react-resizable-panels-website/tests/utils/url.ts
index cfc184eae..88f6bb1b0 100644
--- a/packages/react-resizable-panels-website/tests/utils/url.ts
+++ b/packages/react-resizable-panels-website/tests/utils/url.ts
@@ -1,53 +1,42 @@
import { Page } from "@playwright/test";
import { ReactElement } from "react";
-import {
- PanelGroupProps,
- usePanelGroupLayoutValidator,
-} from "react-resizable-panels";
+import { PanelGroupProps } from "react-resizable-panels";
import { UrlPanelGroupToEncodedString } from "../../src/utils/UrlData";
-export type Metadata = {
- usePanelGroupLayoutValidator?: Parameters<
- typeof usePanelGroupLayoutValidator
- >[0];
-};
-
export async function goToUrl(
page: Page,
- element: ReactElement,
- metadata?: Metadata
+ element: ReactElement
) {
const encodedString = UrlPanelGroupToEncodedString(element);
const url = new URL("http://localhost:1234/__e2e");
url.searchParams.set("urlPanelGroup", encodedString);
- url.searchParams.set("metadata", metadata ? JSON.stringify(metadata) : "");
+
+ // Uncomment when testing for easier debugging
+ // console.log(url.toString());
await page.goto(url.toString());
}
export async function updateUrl(
page: Page,
- element: ReactElement,
- metadata?: Metadata
+ element: ReactElement
) {
- const urlPanelGroupString = UrlPanelGroupToEncodedString(element);
- const metadataString = metadata ? JSON.stringify(metadata) : "";
+ const encodedString = UrlPanelGroupToEncodedString(element);
await page.evaluate(
- ([metadataString, urlPanelGroupString]) => {
+ ([encodedString]) => {
const url = new URL(window.location.href);
- url.searchParams.set("urlPanelGroup", urlPanelGroupString);
- url.searchParams.set("metadata", metadataString);
+ url.searchParams.set("urlPanelGroup", encodedString);
window.history.pushState(
- { urlPanelGroup: urlPanelGroupString },
+ { urlPanelGroup: encodedString },
"",
url.toString()
);
window.dispatchEvent(new Event("popstate"));
},
- [metadataString, urlPanelGroupString]
+ [encodedString]
);
}
diff --git a/packages/react-resizable-panels/CHANGELOG.md b/packages/react-resizable-panels/CHANGELOG.md
index 1e3a81152..5dba64fdc 100644
--- a/packages/react-resizable-panels/CHANGELOG.md
+++ b/packages/react-resizable-panels/CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog
+## 0.0.55
+* New `units` prop added to `Panel` to support pixel-based panel size constraints.
+
## 0.0.54
* [172](https://github.com/bvaughn/react-resizable-panels/issues/172): Development warning added to `PanelGroup` for conditionally-rendered `Panel`(s) that don't have `id` and `order` props
* [156](https://github.com/bvaughn/react-resizable-panels/pull/156): Package exports now used to select between node (server-rendering) and browser (client-rendering) bundles
diff --git a/packages/react-resizable-panels/src/Panel.ts b/packages/react-resizable-panels/src/Panel.ts
index 026b88e99..6798206f7 100644
--- a/packages/react-resizable-panels/src/Panel.ts
+++ b/packages/react-resizable-panels/src/Panel.ts
@@ -19,7 +19,9 @@ import {
PanelData,
PanelOnCollapse,
PanelOnResize,
+ PanelUnits,
} from "./types";
+import { isDevelopment } from "#is-development";
export type PanelProps = {
children?: ReactNode;
@@ -28,13 +30,14 @@ export type PanelProps = {
collapsible?: boolean;
defaultSize?: number | null;
id?: string | null;
- maxSize?: number;
+ maxSize?: number | null;
minSize?: number;
onCollapse?: PanelOnCollapse | null;
onResize?: PanelOnResize | null;
order?: number | null;
style?: CSSProperties;
tagName?: ElementType;
+ units?: PanelUnits;
};
export type ImperativePanelHandle = {
@@ -53,13 +56,14 @@ function PanelWithForwardedRef({
defaultSize = null,
forwardedRef,
id: idFromProps = null,
- maxSize = 100,
+ maxSize = null,
minSize = 10,
onCollapse = null,
onResize = null,
order = null,
style: styleFromProps = {},
tagName: Type = "div",
+ units = "relative",
}: PanelProps & {
forwardedRef: ForwardedRef;
}) {
@@ -92,23 +96,65 @@ function PanelWithForwardedRef({
});
// Basic props validation
- if (minSize < 0 || minSize > 100) {
- throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
- } else if (maxSize < 0 || maxSize > 100) {
- throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
- } else {
- if (defaultSize !== null) {
- if (defaultSize < 0 || defaultSize > 100) {
- throw Error(
- `Panel defaultSize must be between 0 and 100, but was ${defaultSize}`
- );
- } else if (minSize > defaultSize && !collapsible) {
+ if (minSize < 0) {
+ if (isDevelopment) {
+ console.error(`Invalid Panel minSize provided, ${minSize}`);
+ }
+
+ minSize = 0;
+ } else if (units === "relative" && minSize > 100) {
+ if (isDevelopment) {
+ console.error(`Invalid Panel minSize provided, ${minSize}`);
+ }
+
+ minSize = 0;
+ }
+
+ if (maxSize != null) {
+ if (maxSize < 0) {
+ if (isDevelopment) {
+ console.error(`Invalid Panel maxSize provided, ${maxSize}`);
+ }
+
+ maxSize = null;
+ } else if (units === "relative" && maxSize > 100) {
+ if (isDevelopment) {
+ console.error(`Invalid Panel maxSize provided, ${maxSize}`);
+ }
+
+ maxSize = null;
+ }
+ }
+
+ if (defaultSize !== null) {
+ if (defaultSize < 0) {
+ if (isDevelopment) {
+ console.error(`Invalid Panel defaultSize provided, ${defaultSize}`);
+ }
+
+ defaultSize = null;
+ } else if (defaultSize > 100 && units === "relative") {
+ if (isDevelopment) {
+ console.error(`Invalid Panel defaultSize provided, ${defaultSize}`);
+ }
+
+ defaultSize = null;
+ } else if (defaultSize < minSize && !collapsible) {
+ if (isDevelopment) {
console.error(
- `Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`
+ `Panel minSize (${minSize}) cannot be greater than defaultSize (${defaultSize})`
);
+ }
- defaultSize = minSize;
+ defaultSize = minSize;
+ } else if (maxSize != null && defaultSize > maxSize) {
+ if (isDevelopment) {
+ console.error(
+ `Panel maxSize (${maxSize}) cannot be less than defaultSize (${defaultSize})`
+ );
}
+
+ defaultSize = maxSize;
}
}
@@ -126,9 +172,10 @@ function PanelWithForwardedRef({
defaultSize: number | null;
id: string;
idWasAutoGenerated: boolean;
- maxSize: number;
+ maxSize: number | null;
minSize: number;
order: number | null;
+ units: PanelUnits;
}>({
callbacksRef,
collapsedSize,
@@ -139,6 +186,7 @@ function PanelWithForwardedRef({
maxSize,
minSize,
order,
+ units,
});
useIsomorphicLayoutEffect(() => {
committedValuesRef.current.size = parseSizeFromStyle(style);
@@ -152,6 +200,7 @@ function PanelWithForwardedRef({
panelDataRef.current.maxSize = maxSize;
panelDataRef.current.minSize = minSize;
panelDataRef.current.order = order;
+ panelDataRef.current.units = units;
});
useIsomorphicLayoutEffect(() => {
diff --git a/packages/react-resizable-panels/src/PanelGroup.ts b/packages/react-resizable-panels/src/PanelGroup.ts
index e46dc0ef0..95fd87c34 100644
--- a/packages/react-resizable-panels/src/PanelGroup.ts
+++ b/packages/react-resizable-panels/src/PanelGroup.ts
@@ -24,7 +24,6 @@ import {
PanelData,
PanelGroupOnLayout,
PanelGroupStorage,
- PanelGroupValidateLayout,
ResizeEvent,
} from "./types";
import { areEqual } from "./utils/arrays";
@@ -40,12 +39,12 @@ import debounce from "./utils/debounce";
import {
adjustByDelta,
callPanelCallbacks,
+ getAvailableGroupSizePixels,
getBeforeAndAfterIds,
getFlexGrow,
getPanelGroup,
getResizeHandle,
getResizeHandlePanelIds,
- getResizeHandlesForGroup,
panelsMapToSortedArray,
} from "./utils/group";
import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization";
@@ -97,6 +96,8 @@ const defaultStorage: PanelGroupStorage = {
export type CommittedValues = {
direction: Direction;
+ id: string;
+ panelIdsWithStaticUnits: Set;
panels: Map;
sizes: number[];
};
@@ -131,7 +132,6 @@ export type PanelGroupProps = {
storage?: PanelGroupStorage;
style?: CSSProperties;
tagName?: ElementType;
- validateLayout?: PanelGroupValidateLayout;
};
export type ImperativePanelGroupHandle = {
@@ -151,7 +151,6 @@ function PanelGroupWithForwardedRef({
storage = defaultStorage,
style: styleFromProps = {},
tagName: Type = "div",
- validateLayout,
}: PanelGroupProps & {
forwardedRef: ForwardedRef;
}) {
@@ -180,89 +179,15 @@ function PanelGroupWithForwardedRef({
// Use a ref to guard against users passing inline props
const callbacksRef = useRef<{
onLayout: PanelGroupOnLayout | undefined;
- validateLayout: PanelGroupValidateLayout | undefined;
- }>({ onLayout, validateLayout });
+ }>({ onLayout });
useEffect(() => {
callbacksRef.current.onLayout = onLayout;
- callbacksRef.current.validateLayout = validateLayout;
});
const panelIdToLastNotifiedSizeMapRef = useRef>({});
// 0-1 values representing the relative size of each panel.
- const [sizes, setSizesUnsafe] = useState([]);
-
- const validateLayoutHelper = useCallback(
- (nextSizes: number[]) => {
- const { direction, sizes: prevSizes } = committedValuesRef.current;
- const { validateLayout } = callbacksRef.current;
- if (validateLayout) {
- const groupElement = getPanelGroup(groupId)!;
- const resizeHandles = getResizeHandlesForGroup(groupId);
-
- let availableHeight = groupElement.offsetHeight;
- let availableWidth = groupElement.offsetWidth;
-
- if (direction === "horizontal") {
- availableWidth -= resizeHandles.reduce((accumulated, handle) => {
- return accumulated + handle.offsetWidth;
- }, 0);
- } else {
- availableHeight -= resizeHandles.reduce((accumulated, handle) => {
- return accumulated + handle.offsetHeight;
- }, 0);
- }
-
- let nextSizesBefore;
- if (isDevelopment) {
- nextSizesBefore = [...nextSizes];
- }
-
- nextSizes = validateLayout({
- availableHeight,
- availableWidth,
- nextSizes,
- prevSizes,
- });
-
- if (isDevelopment) {
- const { didLogInvalidLayoutWarning } = devWarningsRef.current;
- if (!didLogInvalidLayoutWarning) {
- const total = nextSizes.reduce(
- (accumulated, current) => accumulated + current,
- 0
- );
- if (total < 99 || total > 101) {
- devWarningsRef.current.didLogInvalidLayoutWarning = true;
-
- console.warn(
- "Invalid layout.\nGiven:",
- nextSizesBefore,
- "\nReturned:",
- nextSizes
- );
- }
- }
- }
- }
-
- return nextSizes;
- },
- [groupId]
- );
-
- const setSizes = useCallback(
- (nextSizes: number[]) => {
- const { sizes: prevSizes } = committedValuesRef.current;
-
- nextSizes = validateLayoutHelper(nextSizes);
-
- if (!areEqual(prevSizes, nextSizes)) {
- setSizesUnsafe(nextSizes);
- }
- },
- [validateLayoutHelper]
- );
+ const [sizes, setSizes] = useState([]);
// Used to support imperative collapse/expand API.
const panelSizeBeforeCollapse = useRef