Skip to content

Commit

Permalink
Add onDragging prop to PanelResizeHandle (#113)
Browse files Browse the repository at this point in the history
Resolves issue #94
  • Loading branch information
bvaughn authored Mar 18, 2023
2 parents 4d438d0 + a3aea0c commit a3537a7
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
export type PanelCollapseLogEntryType = "onCollapse";
export type PanelGroupLayoutLogEntryType = "onLayout";
export type PanelResizeHandleDraggingLogEntryType = "onDragging";
export type PanelResizeLogEntryType = "onResize";

export type PanelCollapseLogEntry = {
collapsed: boolean;
panelId: string;
type: PanelCollapseLogEntryType;
};
export type PanelResizeHandleDraggingLogEntry = {
isDragging: boolean;
resizeHandleId: string;
type: PanelResizeHandleDraggingLogEntryType;
};
export type PanelGroupLayoutLogEntry = {
groupId: string;
type: PanelGroupLayoutLogEntryType;
sizes: number[];
type: PanelGroupLayoutLogEntryType;
};
export type PanelResizeLogEntry = {
panelId: string;
type: PanelResizeLogEntryType;
size: number;
type: PanelResizeLogEntryType;
};

export type LogEntryType =
| PanelCollapseLogEntryType
| PanelResizeHandleDraggingLogEntryType
| PanelGroupLayoutLogEntryType
| PanelResizeLogEntryType;

export type LogEntry =
| PanelCollapseLogEntry
| PanelResizeHandleDraggingLogEntry
| PanelGroupLayoutLogEntry
| PanelResizeLogEntry;
23 changes: 22 additions & 1 deletion packages/react-resizable-panels-website/src/utils/UrlData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PanelOnResize,
PanelProps,
PanelResizeHandle,
PanelResizeHandleOnDragging,
PanelResizeHandleProps,
} from "react-resizable-panels";
import { ImperativeDebugLogHandle } from "../routes/examples/DebugLog";
Expand Down Expand Up @@ -241,7 +242,11 @@ export function urlPanelGroupToPanelGroup(
if (isUrlPanel(child)) {
return urlPanelToPanel(child, debugLogRef, idToPanelMapRef, index);
} else if (isUrlPanelResizeHandle(child)) {
return urlPanelResizeHandleToPanelResizeHandle(child, index);
return urlPanelResizeHandleToPanelResizeHandle(
child,
debugLogRef,
index
);
} else {
throw Error("Invalid child");
}
Expand All @@ -251,13 +256,29 @@ export function urlPanelGroupToPanelGroup(

function urlPanelResizeHandleToPanelResizeHandle(
urlPanelResizeHandle: UrlPanelResizeHandle,
debugLogRef: RefObject<ImperativeDebugLogHandle>,
key?: any
): ReactElement {
let onDragging: PanelResizeHandleOnDragging | undefined = undefined;
if (urlPanelResizeHandle.id) {
onDragging = (isDragging: boolean) => {
const debugLog = debugLogRef.current;
if (debugLog) {
debugLog.log({
isDragging,
resizeHandleId: urlPanelResizeHandle.id,
type: "onDragging",
});
}
};
}

return createElement(PanelResizeHandle, {
className: "PanelResizeHandle",
disabled: urlPanelResizeHandle.disabled,
id: urlPanelResizeHandle.id,
key,
onDragging,
style: urlPanelResizeHandle.style,
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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, defaultSize: 20, order: 1 }),
createElement(PanelResizeHandle, { id: "left-handle" }),
createElement(Panel, { defaultSize: 60, order: 2 }),
createElement(PanelResizeHandle, { id: "right-handle" }),
createElement(Panel, { collapsible: true, defaultSize: 20, order: 3 })
);

await goToUrl(page, panelGroup);
}

async function verifyEntries(
page: Page,
expected: Array<[handleId: string, isDragging: boolean]>
) {
const logEntries = await getLogEntries<PanelResizeHandleDraggingLogEntry>(
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],
]);
});
});
3 changes: 3 additions & 0 deletions packages/react-resizable-panels/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.0.37
* [#94](https://github.com/bvaughn/react-resizable-panels/issues/94): Add `onDragging` prop to `PanelResizeHandle` to be notified of when dragging starts/stops.

## 0.0.36
* [#96](https://github.com/bvaughn/react-resizable-panels/issues/96): No longer disable `pointer-events` during resize by default. This behavior can be re-enabled using the newly added `PanelGroup` prop `disablePointerEventsDuringResize`.

Expand Down
39 changes: 20 additions & 19 deletions packages/react-resizable-panels/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
## Props

### `PanelGroup`
| prop | type | description
| :--------------------------------- | :-------------------------- | :---
| `autoSaveId` | `?string` | Unique id used to auto-save group arrangement via `localStorage`
| `children` | `ReactNode` | Arbitrary React element(s)
| `className` | `?string` | Class name to attach to root element
| prop | type | description
| :--------------------------------- | :--------------------------- | :---
| `autoSaveId` | `?string` | Unique id used to auto-save group arrangement via `localStorage`
| `children` | `ReactNode` | Arbitrary React element(s)
| `className` | `?string` | Class name to attach to root element
| `direction` | `"horizontal" \| "vertical"` | Group orientation
| `disablePointerEventsDuringResize` | `?boolean = false` | Disable pointer events inside `Panel`s during resize <sup>2</sup>
| `id` | `?string` | Group id; falls back to `useId` when not provided
| `onLayout` | `?(sizes: number[]) => void` | Called when group layout changes
| `storage` | `?PanelGroupStorage` | Custom storage API; defaults to `localStorage` <sup>1</sup>
| `style` | `?CSSProperties` | CSS style to attach to root element
| `tagName` | `?string = "div"` | HTML element tag name for root element
| `disablePointerEventsDuringResize` | `?boolean = false` | Disable pointer events inside `Panel`s during resize <sup>2</sup>
| `id` | `?string` | Group id; falls back to `useId` when not provided
| `onLayout` | `?(sizes: number[]) => void` | Called when group layout changes
| `storage` | `?PanelGroupStorage` | Custom storage API; defaults to `localStorage` <sup>1</sup>
| `style` | `?CSSProperties` | CSS style to attach to root element
| `tagName` | `?string = "div"` | HTML element tag name for root element

<sup>1</sup>: Storage API must define the following _synchronous_ methods:
* `getItem: (name:string) => string`
Expand Down Expand Up @@ -69,14 +69,15 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
| `resize(percentage: 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
| `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
| `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
---

#### If you like this project, [buy me a coffee](http://givebrian.coffee/).
41 changes: 36 additions & 5 deletions packages/react-resizable-panels/src/PanelResizeHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ import useUniqueId from "./hooks/useUniqueId";

import { useWindowSplitterResizeHandlerBehavior } from "./hooks/useWindowSplitterBehavior";
import { PanelGroupContext } from "./PanelContexts";
import type { ResizeHandler, ResizeEvent } from "./types";
import type {
ResizeHandler,
ResizeEvent,
PanelResizeHandleOnDragging,
} from "./types";
import { getCursorStyle } from "./utils/cursor";

export type PanelResizeHandleProps = {
children?: ReactNode;
className?: string;
disabled?: boolean;
id?: string | null;
onDragging?: PanelResizeHandleOnDragging;
style?: CSSProperties;
tagName?: ElementType;
};
Expand All @@ -32,11 +37,20 @@ export function PanelResizeHandle({
className: classNameFromProps = "",
disabled = false,
id: idFromProps = null,
onDragging = null,
style: styleFromProps = {},
tagName: Type = "div",
}: PanelResizeHandleProps) {
const divElementRef = useRef<HTMLDivElement>(null);

// Use a ref to guard against users passing inline props
const callbacksRef = useRef<{
onDragging: PanelResizeHandleOnDragging | null;
}>({ onDragging });
useEffect(() => {
callbacksRef.current.onDragging = onDragging;
});

const panelGroupContext = useContext(PanelGroupContext);
if (panelGroupContext === null) {
throw Error(
Expand Down Expand Up @@ -69,6 +83,11 @@ export function PanelResizeHandle({
div.blur();

stopDragging();

const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(false);
}
}, [stopDragging]);

useEffect(() => {
Expand Down Expand Up @@ -130,13 +149,25 @@ export function PanelResizeHandle({
"data-panel-resize-handle-id": resizeHandleId,
onBlur: () => setIsFocused(false),
onFocus: () => setIsFocused(true),
onMouseDown: (event: MouseEvent) =>
startDragging(resizeHandleId, event.nativeEvent),
onMouseDown: (event: MouseEvent) => {
startDragging(resizeHandleId, event.nativeEvent);

const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(true);
}
},
onMouseUp: stopDraggingAndBlur,
onTouchCancel: stopDraggingAndBlur,
onTouchEnd: stopDraggingAndBlur,
onTouchStart: (event: TouchEvent) =>
startDragging(resizeHandleId, event.nativeEvent),
onTouchStart: (event: TouchEvent) => {
startDragging(resizeHandleId, event.nativeEvent);

const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(true);
}
},
ref: divElementRef,
role: "separator",
style: {
Expand Down
1 change: 1 addition & 0 deletions packages/react-resizable-panels/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type PanelGroupStorage = {
export type PanelGroupOnLayout = (sizes: number[]) => void;
export type PanelOnCollapse = (collapsed: boolean) => void;
export type PanelOnResize = (size: number) => void;
export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;

export type PanelData = {
callbacksRef: RefObject<{
Expand Down

1 comment on commit a3537a7

@vercel
Copy link

@vercel vercel bot commented on a3537a7 Mar 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.