diff --git a/packages/react-resizable-panels/src/PanelGroup.tsx b/packages/react-resizable-panels/src/PanelGroup.tsx index 038f5b4e6..fc88bfe18 100644 --- a/packages/react-resizable-panels/src/PanelGroup.tsx +++ b/packages/react-resizable-panels/src/PanelGroup.tsx @@ -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({ - screenX: 0, - screenY: 0, - }); + const prevOffsetRef = useRef(0); useLayoutEffect(() => { committedValuesRef.current.direction = direction; @@ -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( @@ -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, }), diff --git a/packages/react-resizable-panels/src/PanelResizeHandle.tsx b/packages/react-resizable-panels/src/PanelResizeHandle.tsx index f3b3428b7..14d35e3d9 100644 --- a/packages/react-resizable-panels/src/PanelResizeHandle.tsx +++ b/packages/react-resizable-panels/src/PanelResizeHandle.tsx @@ -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"; @@ -16,6 +23,8 @@ export default function PanelResizeHandle({ disabled?: boolean; id?: string | null; }) { + const divElementRef = useRef(null); + const panelContext = useContext(PanelContext); const panelGroupContext = useContext(PanelGroupContext); if (panelContext === null || panelGroupContext === null) { @@ -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); @@ -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, @@ -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", diff --git a/packages/react-resizable-panels/src/utils/coordinates.ts b/packages/react-resizable-panels/src/utils/coordinates.ts index 96186b02e..e1dce1918 100644 --- a/packages/react-resizable-panels/src/utils/coordinates.ts +++ b/packages/react-resizable-panels/src/utils/coordinates.ts @@ -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}"`); }