Skip to content

Commit

Permalink
Bugfix: onDragging prop only called for active handle(s)
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Feb 10, 2024
1 parent a63f61d commit fb68d84
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 70 deletions.
205 changes: 183 additions & 22 deletions packages/react-resizable-panels/src/PanelResizeHandle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { act } from "react-dom/test-utils";
import { Panel, PanelGroup, PanelResizeHandle } from ".";
import { assert } from "./utils/assert";
import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
import { dispatchPointerEvent } from "./utils/test-utils";
import {
dispatchPointerEvent,
mockBoundingClientRect,
} from "./utils/test-utils";
import type { PanelResizeHandleProps } from "react-resizable-panels";

describe("PanelResizeHandle", () => {
let expectedWarnings: string[] = [];
Expand Down Expand Up @@ -67,47 +71,204 @@ describe("PanelResizeHandle", () => {
expect(element.title).toBe("bar");
});

function setupMockedGroup({
leftProps = {},
rightProps = {},
}: {
leftProps?: Partial<PanelResizeHandleProps>;
rightProps?: Partial<PanelResizeHandleProps>;
} = {}) {
act(() => {
root.render(
<PanelGroup direction="horizontal" id="test-group">
<Panel />
<PanelResizeHandle id="handle-left" tabIndex={1} {...leftProps} />
<Panel />
<PanelResizeHandle id="handle-right" tabIndex={2} {...rightProps} />
<Panel />
</PanelGroup>
);
});

const leftElement = container.querySelector(
'[data-panel-resize-handle-id="handle-left"]'
) as HTMLElement;

const rightElement = container.querySelector(
'[data-panel-resize-handle-id="handle-right"]'
) as HTMLElement;

// JSDom doesn't properly handle bounding rects
mockBoundingClientRect(leftElement, {
x: 50,
y: 0,
height: 50,
width: 2,
});
mockBoundingClientRect(rightElement, {
x: 100,
y: 0,
height: 50,
width: 2,
});

return {
leftElement,
rightElement,
};
}

describe("callbacks", () => {
describe("onDragging", () => {
it("should fire when dragging starts/stops", async () => {
it("should fire when dragging starts/stops", () => {
const onDragging = jest.fn();

act(() => {
root.render(
<PanelGroup direction="horizontal">
<Panel />
<PanelResizeHandle
id="handle"
onDragging={onDragging}
tabIndex={123}
title="bar"
/>
<Panel />
</PanelGroup>
);
const { leftElement } = setupMockedGroup({
leftProps: { onDragging },
});

const handleElement = container.querySelector(
'[data-panel-resize-handle-id="handle"]'
) as HTMLElement;

act(() => {
dispatchPointerEvent("mouseover", handleElement);
dispatchPointerEvent("mousemove", leftElement);
});
expect(onDragging).not.toHaveBeenCalled();

act(() => {
dispatchPointerEvent("mousedown", handleElement);
dispatchPointerEvent("mousedown", leftElement);
});
expect(onDragging).toHaveBeenCalledTimes(1);
expect(onDragging).toHaveBeenCalledWith(true);

act(() => {
dispatchPointerEvent("mouseup", handleElement);
dispatchPointerEvent("mouseup", leftElement);
});
expect(onDragging).toHaveBeenCalledTimes(2);
expect(onDragging).toHaveBeenCalledWith(false);
});

it("should only fire for the handle that has been dragged", () => {
const onDraggingLeft = jest.fn();
const onDraggingRight = jest.fn();

const { leftElement } = setupMockedGroup({
leftProps: { onDragging: onDraggingLeft },
rightProps: { onDragging: onDraggingRight },
});

act(() => {
dispatchPointerEvent("mousemove", leftElement);
});
expect(onDraggingLeft).not.toHaveBeenCalled();
expect(onDraggingRight).not.toHaveBeenCalled();

act(() => {
dispatchPointerEvent("mousedown", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(1);
expect(onDraggingLeft).toHaveBeenCalledWith(true);
expect(onDraggingRight).not.toHaveBeenCalled();

act(() => {
dispatchPointerEvent("mouseup", leftElement);
});
expect(onDraggingLeft).toHaveBeenCalledTimes(2);
expect(onDraggingLeft).toHaveBeenCalledWith(false);
expect(onDraggingRight).not.toHaveBeenCalled();
});
});
});

describe("data attributes", () => {
function verifyAttribute(
element: HTMLElement,
attributeName: string,
expectedValue: string | null
) {
const actualValue = element.getAttribute(attributeName);
expect(actualValue).toBe(expectedValue);
}

it("should initialize with the correct props based attributes", () => {
const { leftElement, rightElement } = setupMockedGroup();

verifyAttribute(leftElement, "data-panel-group-id", "test-group");
verifyAttribute(leftElement, "data-resize-handle", "");
verifyAttribute(leftElement, "data-panel-group-direction", "horizontal");
verifyAttribute(leftElement, "data-panel-resize-handle-enabled", "true");
verifyAttribute(
leftElement,
"data-panel-resize-handle-id",
"handle-left"
);

verifyAttribute(rightElement, "data-panel-group-id", "test-group");
verifyAttribute(rightElement, "data-resize-handle", "");
verifyAttribute(rightElement, "data-panel-group-direction", "horizontal");
verifyAttribute(rightElement, "data-panel-resize-handle-enabled", "true");
verifyAttribute(
rightElement,
"data-panel-resize-handle-id",
"handle-right"
);
});

it("should update data-resize-handle-active and data-resize-handle-state when dragging starts/stops", () => {
const { leftElement, rightElement } = setupMockedGroup();
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "inactive");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mousemove", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "hover");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mousedown", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", "pointer");
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "drag");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mouseup", leftElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "hover");
verifyAttribute(rightElement, "data-resize-handle-state", "inactive");

act(() => {
dispatchPointerEvent("mousemove", rightElement);
});
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
verifyAttribute(leftElement, "data-resize-handle-state", "inactive");
verifyAttribute(rightElement, "data-resize-handle-state", "hover");
});

it("should update data-resize-handle-active when focused", () => {
const { leftElement, rightElement } = setupMockedGroup();
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);

act(() => {
leftElement.focus();
});
expect(document.activeElement).toBe(leftElement);
verifyAttribute(leftElement, "data-resize-handle-active", "keyboard");
verifyAttribute(rightElement, "data-resize-handle-active", null);

act(() => {
leftElement.blur();
});
expect(document.activeElement).not.toBe(leftElement);
verifyAttribute(leftElement, "data-resize-handle-active", null);
verifyAttribute(rightElement, "data-resize-handle-active", null);
});
});
});
55 changes: 30 additions & 25 deletions packages/react-resizable-panels/src/PanelResizeHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import {
PointerHitAreaMargins,
registerResizeHandle,
ResizeHandlerAction,
ResizeHandlerState,
} from "./PanelResizeHandleRegistry";
import { assert } from "./utils/assert";

export type PanelResizeHandleOnDragging = (isDragging: boolean) => void;
export type ResizeHandlerState = "drag" | "hover" | "inactive";

export type PanelResizeHandleProps = Omit<
HTMLAttributes<keyof HTMLElementTagNameMap>,
Expand Down Expand Up @@ -109,37 +109,42 @@ export function PanelResizeHandle({

const setResizeHandlerState = (
action: ResizeHandlerAction,
state: ResizeHandlerState,
isActive: boolean,
event: ResizeEvent
) => {
setState(state);

switch (action) {
case "down": {
startDragging(resizeHandleId, event);

const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(true);
if (isActive) {
switch (action) {
case "down": {
setState("drag");

startDragging(resizeHandleId, event);

const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(true);
}
break;
}
break;
}
case "up": {
stopDragging();
case "move": {
setState("hover");

const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(false);
resizeHandler(event);
break;
}
break;
}
}
case "up": {
setState("hover");

stopDragging();

switch (state) {
case "drag": {
resizeHandler(event);
break;
const { onDragging } = callbacksRef.current;
if (onDragging) {
onDragging(false);
}
break;
}
}
} else {
setState("inactive");
}
};

Expand Down
36 changes: 13 additions & 23 deletions packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import { getResizeEventCoordinates } from "./utils/events/getResizeEventCoordina
import { getInputType } from "./utils/getInputType";

export type ResizeHandlerAction = "down" | "move" | "up";
export type ResizeHandlerState = "drag" | "hover" | "inactive";
export type SetResizeHandlerState = (
action: ResizeHandlerAction,
state: ResizeHandlerState,
isActive: boolean,
event: ResizeEvent
) => void;

Expand Down Expand Up @@ -93,21 +92,18 @@ function handlePointerDown(event: ResizeEvent) {
function handlePointerMove(event: ResizeEvent) {
const { x, y } = getResizeEventCoordinates(event);

if (isPointerDown) {
intersectingHandles.forEach((data) => {
const { setResizeHandlerState } = data;

setResizeHandlerState("move", "drag", event);
});

// Update cursor based on return value(s) from active handles
updateCursor();
} else {
if (!isPointerDown) {
// Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed
// at that point, the handles may not move with the pointer (depending on constraints)
// but the same set of active handles should be locked until the pointer is released
recalculateIntersectingHandles({ x, y });
updateResizeHandlerStates("move", event);
updateCursor();
}

updateResizeHandlerStates("move", event);

// Update cursor based on return value(s) from active handles
updateCursor();

if (intersectingHandles.length > 0) {
event.preventDefault();
}
Expand Down Expand Up @@ -250,14 +246,8 @@ function updateResizeHandlerStates(
registeredResizeHandlers.forEach((data) => {
const { setResizeHandlerState } = data;

if (intersectingHandles.includes(data)) {
if (isPointerDown) {
setResizeHandlerState(action, "drag", event);
} else {
setResizeHandlerState(action, "hover", event);
}
} else {
setResizeHandlerState(action, "inactive", event);
}
const isActive = intersectingHandles.includes(data);

setResizeHandlerState(action, isActive, event);
});
}
Loading

0 comments on commit fb68d84

Please sign in to comment.