Skip to content

Commit

Permalink
Refactored offset/movement state to simplify it a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Dec 25, 2022
1 parent 18297d6 commit b718a59
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 71 deletions.
21 changes: 5 additions & 16 deletions packages/react-resizable-panels/src/PanelGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@ export default function PanelGroup({

// Tracks the most recent coordinates of a touch/mouse event.
// This is needed to calculate movement (because TouchEvent doesn't support movementX and movementY).
const prevCoordinatesRef = useRef<Coordinates>({
screenX: 0,
screenY: 0,
});
const prevOffsetRef = useRef<number>(0);

useLayoutEffect(() => {
committedValuesRef.current.direction = direction;
Expand Down Expand Up @@ -205,19 +202,14 @@ export default function PanelGroup({

const nextCoordinates = getUpdatedCoordinates(
event,
prevCoordinatesRef.current,
prevOffsetRef.current,
{ height, width },
direction
);
prevCoordinatesRef.current = {
screenX: nextCoordinates.screenX,
screenY: nextCoordinates.screenY,
};
prevOffsetRef.current = nextCoordinates.offset;

const isHorizontal = direction === "horizontal";
const movement = isHorizontal
? nextCoordinates.movementX
: nextCoordinates.movementY;
const movement = nextCoordinates.movement;
const delta = isHorizontal ? movement / width : movement / height;

const nextSizes = adjustByDelta(
Expand Down Expand Up @@ -260,10 +252,7 @@ export default function PanelGroup({
startDragging: (id: string) => setActiveHandleId(id),
stopDragging: () => {
setActiveHandleId(null);
prevCoordinatesRef.current = {
screenX: 0,
screenY: 0,
};
prevOffsetRef.current = 0;
},
unregisterPanel,
}),
Expand Down
37 changes: 28 additions & 9 deletions packages/react-resizable-panels/src/PanelResizeHandle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ReactNode, useContext, useEffect, useState } from "react";
import {
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";

import useUniqueId from "./hooks/useUniqueId";
import { useWindowSplitterResizeHandlerBehavior } from "./hooks/useWindowSplitterBehavior";
Expand All @@ -16,6 +23,8 @@ export default function PanelResizeHandle({
disabled?: boolean;
id?: string | null;
}) {
const divElementRef = useRef<HTMLDivElement>(null);

const panelContext = useContext(PanelContext);
const panelGroupContext = useContext(PanelGroupContext);
if (panelContext === null || panelGroupContext === null) {
Expand All @@ -41,6 +50,15 @@ export default function PanelResizeHandle({
null
);

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();

stopDragging();
}, [stopDragging]);

useEffect(() => {
if (disabled) {
setResizeHandler(null);
Expand All @@ -62,20 +80,20 @@ export default function PanelResizeHandle({
resizeHandler(event);
};

document.body.addEventListener("mouseleave", stopDragging);
document.body.addEventListener("mouseleave", stopDraggingAndBlur);
document.body.addEventListener("mousemove", onMove);
document.body.addEventListener("touchmove", onMove);
document.body.addEventListener("mouseup", stopDragging);
document.body.addEventListener("mouseup", stopDraggingAndBlur);

return () => {
document.body.style.cursor = "";

document.body.removeEventListener("mouseleave", stopDragging);
document.body.removeEventListener("mouseleave", stopDraggingAndBlur);
document.body.removeEventListener("mousemove", onMove);
document.body.removeEventListener("touchmove", onMove);
document.body.removeEventListener("mouseup", stopDragging);
document.body.removeEventListener("mouseup", stopDraggingAndBlur);
};
}, [direction, disabled, isDragging, resizeHandler, stopDragging]);
}, [direction, disabled, isDragging, resizeHandler, stopDraggingAndBlur]);

useWindowSplitterResizeHandlerBehavior({
disabled,
Expand All @@ -90,10 +108,11 @@ export default function PanelResizeHandle({
data-panel-resize-handle-enabled={!disabled}
data-panel-resize-handle-id={id}
onMouseDown={() => startDragging(id)}
onMouseUp={stopDragging}
onTouchCancel={stopDragging}
onTouchEnd={stopDragging}
onMouseUp={stopDraggingAndBlur}
onTouchCancel={stopDraggingAndBlur}
onTouchEnd={stopDraggingAndBlur}
onTouchStart={() => startDragging(id)}
ref={divElementRef}
role="separator"
style={{
cursor: direction === "horizontal" ? "ew-resize" : "ns-resize",
Expand Down
79 changes: 33 additions & 46 deletions packages/react-resizable-panels/src/utils/coordinates.ts
Original file line number Diff line number Diff line change
@@ -1,105 +1,92 @@
import { Direction, ResizeEvent } from "../types";

export type Coordinates = {
screenX: number;
screenY: number;
movement: number;
offset: number;
};

export type Dimensions = {
export type Size = {
height: number;
width: number;
};

export type Movement = {
movementX: number;
movementY: number;
};

const element = document.createElement("div");
element.getBoundingClientRect();

// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX
export function getUpdatedCoordinates(
event: ResizeEvent,
prevCoordinates: Coordinates,
dimensions: Dimensions,
prevOffset: number,
{ height, width }: Size,
direction: Direction
): Coordinates & Movement {
const { screenX: prevScreenX, screenY: prevScreenY } = prevCoordinates;
const { height, width } = dimensions;
): Coordinates {
const isHorizontal = direction === "horizontal";
const size = isHorizontal ? width : height;

const getMovementBetween = (current: number, prev: number) =>
prev === 0 ? 0 : current - prev;

if (isKeyDown(event)) {
let movementX = 0;
let movementY = 0;
let movement = 0;

const size = direction === "horizontal" ? width : height;
const denominator = event.shiftKey ? 10 : 100;
const delta = size / denominator;

switch (event.key) {
case "ArrowDown":
movementY = delta;
movement = delta;
break;
case "ArrowLeft":
movementX = -delta;
movement = -delta;
break;
case "ArrowRight":
movementX = delta;
movement = delta;
break;
case "ArrowUp":
movementY = -delta;
movement = -delta;
break;
case "End":
if (direction === "horizontal") {
movementX = size;
if (isHorizontal) {
movement = size;
} else {
movementY = size;
movement = size;
}
break;
case "Home":
if (direction === "horizontal") {
movementX = -size;
if (isHorizontal) {
movement = -size;
} else {
movementY = -size;
movement = -size;
}
break;
}

// Estimate screen X/Y to be the center of the resize handle.
// Otherwise the first mouse/touch event after a keyboard event will appear to "jump"
let screenX = 0;
let screenY = 0;
let offset = 0;
if (document.activeElement) {
const rect = document.activeElement.getBoundingClientRect();
screenX = rect.left + rect.width / 2;
screenY = rect.top + rect.height / 2;
offset = isHorizontal
? rect.left + rect.width / 2
: rect.top + rect.height / 2;
}

return {
movementX,
movementY,
screenX,
screenY,
movement,
offset,
};
} else if (isTouchMoveEvent(event)) {
const firstTouch = event.touches[0];

return {
movementX: getMovementBetween(firstTouch.screenX, prevScreenX),
movementY: getMovementBetween(firstTouch.screenY, prevScreenY),
screenX: firstTouch.screenX,
screenY: firstTouch.screenY,
};
const offset = isHorizontal ? firstTouch.screenX : firstTouch.screenY;
const movement = getMovementBetween(offset, prevOffset);

return { movement, offset };
} else if (isMouseMoveEvent(event)) {
return {
movementX: getMovementBetween(event.screenX, prevScreenX),
movementY: getMovementBetween(event.screenY, prevScreenY),
screenX: event.screenX,
screenY: event.screenY,
};
const offset = isHorizontal ? event.screenX : event.screenY;
const movement = getMovementBetween(offset, prevOffset);

return { movement, offset };
} else {
throw Error(`Unsupported event type: "${(event as any).type}"`);
}
Expand Down

1 comment on commit b718a59

@vercel
Copy link

@vercel vercel bot commented on b718a59 Dec 25, 2022

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.