diff --git a/packages/core/src/components/Edges/wrapEdge.tsx b/packages/core/src/components/Edges/wrapEdge.tsx index 4ac3b139..36c49615 100644 --- a/packages/core/src/components/Edges/wrapEdge.tsx +++ b/packages/core/src/components/Edges/wrapEdge.tsx @@ -4,7 +4,7 @@ import cc from 'classcat'; import { useStoreApi } from '../../hooks/useStore'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; -import { handleMouseDown } from '../Handle/handler'; +import { handlePointerDown } from '../Handle/handler'; import { EdgeAnchor } from './EdgeAnchor'; import { getMarkerId } from '../../utils/graph'; import { getMouseHandler } from './utils'; @@ -99,14 +99,14 @@ export default (EdgeComponent: ComponentType) => { setUpdating(true); onEdgeUpdateStart?.(event, edge, handleType); - const _onEdgeUpdateEnd = (evt: MouseEvent) => { + const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => { setUpdating(false); onEdgeUpdateEnd?.(evt, edge, handleType); }; const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); - handleMouseDown({ + handlePointerDown({ event, handleId, nodeId, diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts index 40be2090..766f48ba 100644 --- a/packages/core/src/components/Handle/handler.ts +++ b/packages/core/src/components/Handle/handler.ts @@ -1,24 +1,17 @@ -import type { MouseEvent as ReactMouseEvent } from 'react'; +import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react'; import { StoreApi } from 'zustand'; -import { getHostForElement, calcAutoPan } from '../../utils'; +import { getHostForElement, calcAutoPan, getEventPosition, isMouseEvent } from '../../utils'; import type { OnConnect, HandleType, ReactFlowState } from '../../types'; import { pointToRendererPoint, rendererPointToPoint } from '../../utils/graph'; -import { - ConnectionHandle, - getClosestHandle, - getConnectionPosition, - getHandleLookup, - isValidHandle, - ValidConnectionFunc, -} from './utils'; +import { ConnectionHandle, getClosestHandle, getHandleLookup, isValidHandle, ValidConnectionFunc } from './utils'; function resetRecentHandle(handleDomNode: Element): void { handleDomNode?.classList.remove('react-flow__handle-valid'); handleDomNode?.classList.remove('react-flow__handle-connecting'); } -export function handleMouseDown({ +export function handlePointerDown({ event, handleId, nodeId, @@ -30,7 +23,7 @@ export function handleMouseDown({ elementEdgeUpdaterType, onEdgeUpdateEnd, }: { - event: ReactMouseEvent; + event: ReactMouseEvent | ReactTouchEvent; handleId: string | null; nodeId: string; onConnect: OnConnect; @@ -39,7 +32,7 @@ export function handleMouseDown({ setState: StoreApi['setState']; isValidConnection: ValidConnectionFunc; elementEdgeUpdaterType?: HandleType; - onEdgeUpdateEnd?: (evt: MouseEvent) => void; + onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void; }): void { // when react-flow is used inside a shadow root we can't use document const doc = getHostForElement(event.target as HTMLElement); @@ -48,7 +41,8 @@ export function handleMouseDown({ let autoPanId = 0; let prevClosestHandle: ConnectionHandle | null; - const clickedElement = doc?.elementFromPoint(event.clientX, event.clientY); + const { clientX, clientY } = isMouseEvent(event) ? event : event.touches[0]!; + const clickedElement = doc?.elementFromPoint(clientX, clientY); const elementIsTarget = clickedElement?.classList.contains('target'); const elementIsSource = clickedElement?.classList.contains('source'); @@ -59,7 +53,7 @@ export function handleMouseDown({ const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementIsTarget ? 'target' : 'source'; const containerBounds = domNode.getBoundingClientRect(); let prevActiveHandle: Element; - let connectionPosition = getConnectionPosition(event, containerBounds); + let connectionPosition = getEventPosition(event, containerBounds); let autoPanStarted = false; const handleLookup = getHandleLookup({ @@ -89,10 +83,10 @@ export function handleMouseDown({ onConnectStart?.(event, { nodeId, handleId, handleType }); - function onMouseMove(event: MouseEvent) { + function onPointerMove(event: MouseEvent | TouchEvent) { const { transform } = getState(); + connectionPosition = getEventPosition(event, containerBounds); - connectionPosition = getConnectionPosition(event, containerBounds); prevClosestHandle = getClosestHandle( pointToRendererPoint(connectionPosition, transform, false, [1, 1]), connectionRadius, @@ -138,7 +132,7 @@ export function handleMouseDown({ } } - function onMouseUp(event: MouseEvent) { + function onPointerUp(event: MouseEvent | TouchEvent) { cancelAnimationFrame(autoPanId); autoPanStarted = false; @@ -172,10 +166,16 @@ export function handleMouseDown({ connectionHandleType: null, }); - doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject); - doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject); + doc.removeEventListener('mousemove', onPointerMove as EventListener); + doc.removeEventListener('mouseup', onPointerUp as EventListener); + + doc.removeEventListener('touchmove', onPointerMove as EventListener); + doc.removeEventListener('touchend', onPointerUp as EventListener); } - doc.addEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject); - doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject); + doc.addEventListener('mousemove', onPointerMove as EventListener); + doc.addEventListener('mouseup', onPointerUp as EventListener); + + doc.addEventListener('touchmove', onPointerMove as EventListener); + doc.addEventListener('touchend', onPointerUp as EventListener); } diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx index 19a78314..89212300 100644 --- a/packages/core/src/components/Handle/index.tsx +++ b/packages/core/src/components/Handle/index.tsx @@ -1,11 +1,11 @@ -import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent } from 'react'; +import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; -import { handleMouseDown } from './handler'; -import { devWarn, getHostForElement } from '../../utils'; +import { handlePointerDown } from './handler'; +import { devWarn, getHostForElement, isMouseEvent } from '../../utils'; import { addEdge } from '../../utils/graph'; import { Position } from '../../types'; import type { HandleProps, Connection, ReactFlowState } from '../../types'; @@ -33,6 +33,7 @@ const Handle = forwardRef( children, className, onMouseDown, + onTouchStart, ...rest }, ref @@ -66,9 +67,11 @@ const Handle = forwardRef( onConnect?.(edgeParams); }; - const onMouseDownHandler = (event: ReactMouseEvent) => { - if (event.button === 0) { - handleMouseDown({ + const onPointerDown = (event: ReactMouseEvent | ReactTouchEvent) => { + const isMouse = isMouseEvent(event); + + if ((isMouse && event.button === 0) || 'touches' in event) { + handlePointerDown({ event, handleId, nodeId, @@ -79,7 +82,12 @@ const Handle = forwardRef( isValidConnection, }); } - onMouseDown?.(event); + + if (isMouse) { + onMouseDown?.(event); + } else { + onTouchStart?.(event); + } }; const onClick = (event: ReactMouseEvent) => { @@ -136,7 +144,8 @@ const Handle = forwardRef( connectionStartHandle?.type === type, }, ])} - onMouseDown={onMouseDownHandler} + onMouseDown={onPointerDown} + onTouchStart={onPointerDown} onClick={connectOnClick ? onClick : undefined} ref={ref} {...rest} diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts index 69d6f68c..d6cf9e8b 100644 --- a/packages/core/src/components/Handle/utils.ts +++ b/packages/core/src/components/Handle/utils.ts @@ -1,5 +1,3 @@ -import { MouseEvent as ReactMouseEvent } from 'react'; - import { ConnectionMode } from '../../types'; import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types'; import { internalsSymbol } from '../../utils'; @@ -134,10 +132,3 @@ export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHand return res; }, []); } - -export function getConnectionPosition(event: MouseEvent | ReactMouseEvent, bounds: DOMRect): XYPosition { - return { - x: event.clientX - bounds.left, - y: event.clientY - bounds.top, - }; -} diff --git a/packages/core/src/container/Pane/index.tsx b/packages/core/src/container/Pane/index.tsx index e92f1d79..bb9e3615 100644 --- a/packages/core/src/container/Pane/index.tsx +++ b/packages/core/src/container/Pane/index.tsx @@ -11,8 +11,9 @@ import { containerStyle } from '../../styles'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { getSelectionChanges } from '../../utils/changes'; import { getConnectedEdges, getNodesInside } from '../../utils/graph'; +import { getEventPosition } from '../../utils'; import { SelectionMode } from '../../types'; -import type { ReactFlowProps, XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types'; +import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types'; type PaneProps = { isSelecting: boolean; @@ -33,13 +34,6 @@ type PaneProps = { > >; -function getMousePosition(event: ReactMouseEvent, containerBounds: DOMRect): XYPosition { - return { - x: event.clientX - containerBounds.left, - y: event.clientY - containerBounds.top, - }; -} - const wrapHandler = ( handler: React.MouseEventHandler | undefined, containerRef: React.MutableRefObject @@ -118,7 +112,7 @@ const Pane = memo( return; } - const { x, y } = getMousePosition(event, containerBounds.current); + const { x, y } = getEventPosition(event, containerBounds.current); resetSelectedElements(); @@ -145,7 +139,7 @@ const Pane = memo( store.setState({ userSelectionActive: true, nodesSelectionActive: false }); - const mousePos = getMousePosition(event, containerBounds.current); + const mousePos = getEventPosition(event, containerBounds.current); const startX = userSelectionRect.startX ?? 0; const startY = userSelectionRect.startY ?? 0; diff --git a/packages/core/src/hooks/useDrag/index.ts b/packages/core/src/hooks/useDrag/index.ts index c9d0ab3d..868b6387 100644 --- a/packages/core/src/hooks/useDrag/index.ts +++ b/packages/core/src/hooks/useDrag/index.ts @@ -7,7 +7,7 @@ import { useStoreApi } from '../../hooks/useStore'; import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils'; import { handleNodeClick } from '../../components/Nodes/utils'; import useGetPointerPosition from '../useGetPointerPosition'; -import { calcAutoPan } from '../../utils'; +import { calcAutoPan, getEventPosition } from '../../utils'; import type { NodeDragItem, Node, SelectionDragHandler, UseDragEvent, XYPosition } from '../../types'; export type UseDragData = { dx: number; dy: number }; @@ -168,10 +168,7 @@ function useDrag({ } containerBounds.current = domNode?.getBoundingClientRect() || null; - mousePosition.current = { - x: event.sourceEvent.clientX - (containerBounds.current?.left ?? 0), - y: event.sourceEvent.clientY - (containerBounds.current?.top ?? 0), - }; + mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!); }) .on('drag', (event: UseDragEvent) => { const pointerPos = getPointerPosition(event); @@ -188,10 +185,7 @@ function useDrag({ dragItems.current ) { dragEvent.current = event.sourceEvent as MouseEvent; - mousePosition.current = { - x: event.sourceEvent.clientX - (containerBounds.current?.left ?? 0), - y: event.sourceEvent.clientY - (containerBounds.current?.top ?? 0), - }; + mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!); updateNodes(pointerPos); } diff --git a/packages/core/src/types/component-props.ts b/packages/core/src/types/component-props.ts index 90b7124f..bafb8d5e 100644 --- a/packages/core/src/types/component-props.ts +++ b/packages/core/src/types/component-props.ts @@ -61,7 +61,7 @@ export type ReactFlowProps = HTMLAttributes & { onEdgeMouseLeave?: EdgeMouseHandler; onEdgeDoubleClick?: EdgeMouseHandler; onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; - onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void; + onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void; onNodesChange?: OnNodesChange; onEdgesChange?: OnEdgesChange; onNodesDelete?: OnNodesDelete; diff --git a/packages/core/src/types/edges.ts b/packages/core/src/types/edges.ts index 92e10637..c2202788 100644 --- a/packages/core/src/types/edges.ts +++ b/packages/core/src/types/edges.ts @@ -85,7 +85,7 @@ export type WrapEdgeProps = Omit, 'sourceHandle' | 'targetHandl onMouseLeave?: EdgeMouseHandler; edgeUpdaterRadius?: number; onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; - onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void; + onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void; rfId?: string; isFocusable: boolean; pathOptions?: BezierPathOptions | SmoothStepPathOptions; diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index 6289c9c1..ba0de157 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -1,5 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { MouseEvent as ReactMouseEvent, ComponentType, MemoExoticComponent } from 'react'; +import type { + MouseEvent as ReactMouseEvent, + TouchEvent as ReactTouchEvent, + ComponentType, + MemoExoticComponent, +} from 'react'; import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3'; import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; @@ -77,8 +82,8 @@ export type OnConnectStartParams = { handleType: HandleType | null; }; -export type OnConnectStart = (event: ReactMouseEvent, params: OnConnectStartParams) => void; -export type OnConnectEnd = (event: MouseEvent) => void; +export type OnConnectStart = (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => void; +export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void; export type Viewport = { x: number; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 30aa3789..3051f25a 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,4 +1,8 @@ -import type { KeyboardEvent as ReactKeyboardEvent } from 'react'; +import type { + KeyboardEvent as ReactKeyboardEvent, + MouseEvent as ReactMouseEvent, + TouchEvent as ReactTouchEvent, +} from 'react'; import type { Dimensions, Node, XYPosition, CoordinateExtent, Box, Rect } from '../types'; @@ -111,3 +115,21 @@ export function isInputDOMNode(event: KeyboardEvent | ReactKeyboardEvent): boole !!target?.closest('.nokey') ); } + +export const isMouseEvent = ( + event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent +): event is MouseEvent | ReactMouseEvent => 'button' in event; + +export const getEventPosition = ( + event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent, + bounds: DOMRect +) => { + const isMouse = isMouseEvent(event); + const evtX = isMouse ? event.clientX : event.touches?.[0].clientX; + const evtY = isMouse ? event.clientY : event.touches?.[0].clientY; + + return { + x: evtX - bounds.left, + y: evtY - bounds.top, + }; +};