diff --git a/README.md b/README.md
index 5adb5b1be..37d5ba0bd 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,26 @@ Supported input methods include mouse, touch, and keyboard (via [Window Splitter
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.
+
+```tsx
+
+ {renderSideBar && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+```
+
### Can a attach a ref to the DOM elements?
No. I think exposing two refs (one for the component's imperative API and one for a DOM element) would be awkward. This library does export several utility methods for accessing the underlying DOM elements though. For example:
@@ -74,31 +94,11 @@ This likely means that you haven't applied any CSS to style the resize handles.
```
-### 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.
-
-```tsx
-
- {renderSideBar && (
- <>
-
-
- >
- )}
-
-
-
-
-```
-
### How can I use persistent layouts with SSR?
By default, this library uses `localStorage` to persist layouts. With server rendering, this can cause a flicker when the default layout (rendered on the server) is replaced with the persisted layout (in `localStorage`). The way to avoid this flicker is to also persist the layout with a cookie like so:
-##### Server component
+#### Server component
```tsx
import ResizablePanels from "@/app/ResizablePanels";
@@ -116,7 +116,7 @@ export function ServerComponent() {
}
```
-##### Client component
+#### Client component
```tsx
"use client";
diff --git a/packages/react-resizable-panels/CHANGELOG.md b/packages/react-resizable-panels/CHANGELOG.md
index c73dd60a8..fdcee9e2c 100644
--- a/packages/react-resizable-panels/CHANGELOG.md
+++ b/packages/react-resizable-panels/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 2.0.0
+
+- Support resizing multiple (intersecting) panels at once (#274)
+This behavior can be customized using a new `hitAreaMargins` prop; defaults to a 15 pixel margin for _coarse_ inputs and a 5 pixel margin for _fine_ inputs.
+
## 1.0.10
- Fixed edge case constraints check bug that could cause a collapsed panel to re-expand unnecessarily (#273)
diff --git a/packages/react-resizable-panels/README.md b/packages/react-resizable-panels/README.md
index 176f5c946..528fef17c 100644
--- a/packages/react-resizable-panels/README.md
+++ b/packages/react-resizable-panels/README.md
@@ -20,7 +20,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
;
```
-### If you like this project, 🎉 [become a sponsor](https://github.com/sponsors/bvaughn/) or ☕ [buy me a coffee](http://givebrian.coffee/)
+## If you like this project, 🎉 [become a sponsor](https://github.com/sponsors/bvaughn/) or ☕ [buy me a coffee](http://givebrian.coffee/)
## Props
@@ -44,11 +44,11 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
- `setItem: (name: string, value: string) => void`
`PanelGroup` components also expose an imperative API for manual resizing:
-| method | description
-| :-------------------------------- | :---
-| `getId(): string` | Gets the panel group's ID.
-| `getLayout(): number[]` | Gets the panel group's current _layout_ (`[1 - 100, ...]`).
-| `setLayout(layout: number[])` | Resize panel group to the specified _layout_ (`[1 - 100, ...]`).
+| method | description |
+| :---------------------------- | :--------------------------------------------------------------- |
+| `getId(): string` | Gets the panel group's ID. |
+| `getLayout(): number[]` | Gets the panel group's current _layout_ (`[1 - 100, ...]`). |
+| `setLayout(layout: number[])` | Resize panel group to the specified _layout_ (`[1 - 100, ...]`). |
### `Panel`
@@ -63,7 +63,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
| `maxSize` | `?number = 100` | Maximum allowable size of panel (numeric value between 1-100); defaults to `100` |
| `minSize` | `?number = 10` | Minimum allowable size of panel (numeric value between 1-100); defaults to `10` |
| `onCollapse` | `?() => void` | Called when panel is collapsed |
-| `onExpand` | `?() => void` | Called when panel is expanded |
+| `onExpand` | `?() => void` | Called when panel is expanded |
| `onResize` | `?(size: number) => void` | Called when panel is resized; `size` parameter is a numeric value between 1-100. 1 |
| `order` | `?number` | Order of panel within group; required for groups with conditionally rendered panels |
| `style` | `?CSSProperties` | CSS style to attach to root element |
@@ -72,28 +72,29 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
1: If any `Panel` has an `onResize` callback, the `order` prop should be provided for all `Panel`s.
`Panel` components also expose an imperative API for manual resizing:
-| method | description
-| :--------------------------- | :---
-| `collapse()` | If panel is `collapsible`, collapse it fully.
-| `expand()` | If panel is currently _collapsed_, expand it to its most recent size.
-| `getId(): string` | Gets the ID of the panel.
-| `getSize(): number` | Gets the current size of the panel as a percentage (`1 - 100`).
-| `isCollapsed(): boolean` | Returns `true` if the panel is currently _collapsed_ (`size === 0`).
-| `isExpanded(): boolean` | Returns `true` if the panel is currently _not collapsed_ (`!isCollapsed()`).
-| `getSize(): number` | Returns the most recently commited size of the panel as a percentage (`1 - 100`).
-| `resize(size: number)` | Resize panel to the specified _percentage_ (`1 - 100`).
+| method | description |
+| :----------------------- | :--------------------------------------------------------------------------------- |
+| `collapse()`v | If panel is `collapsible`, collapse it fully. |
+| `expand()` | If panel is currently _collapsed_, expand it to its most recent size. |
+| `getId(): string` | Gets the ID of the panel. |
+| `getSize(): number` | Gets the current size of the panel as a percentage (`1 - 100`). |
+| `isCollapsed(): boolean` | Returns `true` if the panel is currently _collapsed_ (`size === 0`). |
+| `isExpanded(): boolean` | Returns `true` if the panel is currently _not collapsed_ (`!isCollapsed()`). |
+| `getSize(): number` | Returns the most recently committed size of the panel as a percentage (`1 - 100`). |
+| `resize(size: number)` | Resize panel to the specified _percentage_ (`1 - 100`). |
### `PanelResizeHandle`
-| prop | type | description |
-| :----------- | :------------------------------- | :------------------------------------------------------------------------------ |
-| `children` | `?ReactNode` | Custom drag UI; can be any arbitrary React element(s) |
-| `className` | `?string` | Class name to attach to root element |
-| `disabled` | `?boolean` | Disable drag handle |
-| `id` | `?string` | Resize handle id (unique within group); falls back to `useId` when not provided |
-| `onDragging` | `?(isDragging: boolean) => void` | Called when group layout changes |
-| `style` | `?CSSProperties` | CSS style to attach to root element |
-| `tagName` | `?string = "div"` | HTML element tag name for root element |
+| prop | type | description |
+| :--------------- | :-------------------------------------------- | :------------------------------------------------------------------------------ |
+| `children` | `?ReactNode` | Custom drag UI; can be any arbitrary React element(s) |
+| `className` | `?string` | Class name to attach to root element |
+| `hitAreaMargins` | `?{ coarse: number = 15; fine: number = 5; }` | Allow this much margin when determining resizable handle hit detection |
+| `disabled` | `?boolean` | Disable drag handle |
+| `id` | `?string` | Resize handle id (unique within group); falls back to `useId` when not provided |
+| `onDragging` | `?(isDragging: boolean) => void` | Called when group layout changes |
+| `style` | `?CSSProperties` | CSS style to attach to root element |
+| `tagName` | `?string = "div"` | HTML element tag name for root element |
---
@@ -123,62 +124,6 @@ The `Panel` API doesn't _require_ `id` and `order` props because they aren't nec
```
-### How can I use persistent layouts with SSR?
-
-By default, this library uses `localStorage` to persist layouts. With server rendering, this can cause a flicker when the default layout (rendered on the server) is replaced with the persisted layout (in `localStorage`). The way to avoid this flicker is to also persist the layout with a cookie like so:
-
-##### Server component
-
-```tsx
-import ResizablePanels from "@/app/ResizablePanels";
-import { cookies } from "next/headers";
-
-export function ServerComponent() {
- const layout = cookies().get("react-resizable-panels:layout");
-
- let defaultLayout;
- if (layout) {
- defaultLayout = JSON.parse(layout.value);
- }
-
- return ;
-}
-```
-
-##### Client component
-
-```tsx
-"use client";
-
-import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
-
-export function ClientComponent({
- defaultLayout = [33, 67],
-}: {
- defaultLayout: number[] | undefined;
-}) {
- const onLayout = (sizes: number[]) => {
- document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`;
- };
-
- return (
-
- {/* ... */}
-
- {/* ... */}
-
- );
-}
-```
-
----
-
-## 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.
-
### Can a attach a ref to the DOM elements?
No. I think exposing two refs (one for the component's imperative API and one for a DOM element) would be awkward. This library does export several utility methods for accessing the underlying DOM elements though. For example:
@@ -230,31 +175,11 @@ This likely means that you haven't applied any CSS to style the resize handles.
```
-### 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.
-
-```tsx
-
- {renderSideBar && (
- <>
-
-
- >
- )}
-
-
-
-
-```
-
### How can I use persistent layouts with SSR?
By default, this library uses `localStorage` to persist layouts. With server rendering, this can cause a flicker when the default layout (rendered on the server) is replaced with the persisted layout (in `localStorage`). The way to avoid this flicker is to also persist the layout with a cookie like so:
-##### Server component
+#### Server component
```tsx
import ResizablePanels from "@/app/ResizablePanels";
@@ -272,7 +197,7 @@ export function ServerComponent() {
}
```
-##### Client component
+#### Client component
```tsx
"use client";
diff --git a/packages/react-resizable-panels/src/PanelResizeHandle.ts b/packages/react-resizable-panels/src/PanelResizeHandle.ts
index c57937c59..9f0229cde 100644
--- a/packages/react-resizable-panels/src/PanelResizeHandle.ts
+++ b/packages/react-resizable-panels/src/PanelResizeHandle.ts
@@ -18,6 +18,7 @@ import {
ResizeHandler,
} from "./PanelGroupContext";
import {
+ PointerHitAreaMargins,
registerResizeHandle,
ResizeHandlerAction,
ResizeHandlerState,
@@ -33,7 +34,7 @@ export type PanelResizeHandleProps = Omit<
PropsWithChildren<{
className?: string;
disabled?: boolean;
- gutter?: number;
+ hitAreaMargins?: PointerHitAreaMargins;
id?: string | null;
onDragging?: PanelResizeHandleOnDragging;
style?: CSSProperties;
@@ -45,7 +46,7 @@ export function PanelResizeHandle({
children = null,
className: classNameFromProps = "",
disabled = false,
- gutter = 5,
+ hitAreaMargins,
id: idFromProps,
onDragging,
style: styleFromProps = {},
@@ -136,13 +137,18 @@ export function PanelResizeHandle({
resizeHandleId,
element,
direction,
- gutter,
+ {
+ // Coarse inputs (e.g. finger/touch)
+ coarse: hitAreaMargins?.coarse ?? 15,
+ // Fine inputs (e.g. mouse)
+ fine: hitAreaMargins?.fine ?? 5,
+ },
setResizeHandlerState
);
}, [
direction,
disabled,
- gutter,
+ hitAreaMargins,
registerResizeHandleWithParentGroup,
resizeHandleId,
resizeHandler,
diff --git a/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts b/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts
index 3be54951b..efe64c4ec 100644
--- a/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts
+++ b/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts
@@ -1,6 +1,7 @@
import { Direction, ResizeEvent } from "./types";
import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor";
import { getResizeEventCoordinates } from "./utils/events/getResizeEventCoordinates";
+import { getInputType } from "./utils/getInputType";
export type ResizeHandlerAction = "down" | "move" | "up";
export type ResizeHandlerState = "drag" | "hover" | "inactive";
@@ -10,10 +11,15 @@ export type SetResizeHandlerState = (
event: ResizeEvent
) => void;
+export type PointerHitAreaMargins = {
+ coarse: number;
+ fine: number;
+};
+
export type ResizeHandlerData = {
direction: Direction;
element: HTMLElement;
- gutter: number;
+ hitAreaMargins: PointerHitAreaMargins;
setResizeHandlerState: SetResizeHandlerState;
};
@@ -22,6 +28,8 @@ export const EXCEEDED_HORIZONTAL_MAX = 0b0010;
export const EXCEEDED_VERTICAL_MIN = 0b0100;
export const EXCEEDED_VERTICAL_MAX = 0b1000;
+const isCoarsePointer = getInputType() === "coarse";
+
let intersectingHandles: ResizeHandlerData[] = [];
let isPointerDown = false;
let ownerDocumentCounts: Map = new Map();
@@ -33,7 +41,7 @@ export function registerResizeHandle(
resizeHandleId: string,
element: HTMLElement,
direction: Direction,
- gutter: number,
+ hitAreaMargins: PointerHitAreaMargins,
setResizeHandlerState: SetResizeHandlerState
) {
const { ownerDocument } = element;
@@ -41,7 +49,7 @@ export function registerResizeHandle(
const data: ResizeHandlerData = {
direction,
element,
- gutter,
+ hitAreaMargins,
setResizeHandlerState,
};
@@ -122,31 +130,24 @@ function handlePointerUp(event: ResizeEvent) {
updateListeners();
}
-function intersects({
- data,
- x,
- y,
-}: {
- data: ResizeHandlerData;
- x: number;
- y: number;
-}) {
- const { element, gutter } = data;
- const { bottom, left, right, top } = element.getBoundingClientRect();
-
- return (
- x >= left - gutter &&
- x <= right + gutter &&
- y >= top - gutter &&
- y <= bottom + gutter
- );
-}
-
function recalculateIntersectingHandles({ x, y }: { x: number; y: number }) {
intersectingHandles.splice(0);
registeredResizeHandlers.forEach((data) => {
- if (intersects({ data, x, y })) {
+ const { element, hitAreaMargins } = data;
+ const { bottom, left, right, top } = element.getBoundingClientRect();
+
+ const margin = isCoarsePointer
+ ? hitAreaMargins.coarse
+ : hitAreaMargins.fine;
+
+ const intersects =
+ x >= left - margin &&
+ x <= right + margin &&
+ y >= top - margin &&
+ y <= bottom + margin;
+
+ if (intersects) {
intersectingHandles.push(data);
}
});
diff --git a/packages/react-resizable-panels/src/utils/getInputType.ts b/packages/react-resizable-panels/src/utils/getInputType.ts
new file mode 100644
index 000000000..4e539fb19
--- /dev/null
+++ b/packages/react-resizable-panels/src/utils/getInputType.ts
@@ -0,0 +1,5 @@
+export function getInputType(): "coarse" | "fine" | undefined {
+ if (typeof matchMedia === "function") {
+ return matchMedia("(pointer:coarse)").matches ? "coarse" : "fine";
+ }
+}