Merge pull request #2763 from wbkd/feat/touch-connect

Feat: touch connect
This commit is contained in:
Moritz Klack
2023-01-24 14:22:48 +01:00
committed by GitHub
13 changed files with 126 additions and 79 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@reactflow/core': patch
---
Connecting nodes: Enable connections on touch devices
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 3000 --open", "dev": "vite --port 3000 --open --host",
"serve": "vite serve --port 3000", "serve": "vite serve --port 3000",
"build": "vite build", "build": "vite build",
"test:dev": "cypress open", "test:dev": "cypress open",
@@ -4,7 +4,7 @@ import cc from 'classcat';
import { useStoreApi } from '../../hooks/useStore'; import { useStoreApi } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { handleMouseDown } from '../Handle/handler'; import { handlePointerDown } from '../Handle/handler';
import { EdgeAnchor } from './EdgeAnchor'; import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph'; import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils'; import { getMouseHandler } from './utils';
@@ -99,14 +99,14 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
setUpdating(true); setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType); onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent) => { const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false); setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType); onEdgeUpdateEnd?.(evt, edge, handleType);
}; };
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
handleMouseDown({ handlePointerDown({
event, event,
handleId, handleId,
nodeId, nodeId,
+41 -35
View File
@@ -1,24 +1,20 @@
import type { MouseEvent as ReactMouseEvent } from 'react'; import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import { StoreApi } from 'zustand'; import { StoreApi } from 'zustand';
import { getHostForElement, calcAutoPan } from '../../utils'; import { getHostForElement, calcAutoPan, getEventPosition } from '../../utils';
import type { OnConnect, HandleType, ReactFlowState } from '../../types'; import type { OnConnect, HandleType, ReactFlowState } from '../../types';
import { pointToRendererPoint, rendererPointToPoint } from '../../utils/graph'; import { pointToRendererPoint, rendererPointToPoint } from '../../utils/graph';
import { import {
ConnectionHandle, ConnectionHandle,
getClosestHandle, getClosestHandle,
getConnectionPosition,
getHandleLookup, getHandleLookup,
getHandleType,
isValidHandle, isValidHandle,
resetRecentHandle,
ValidConnectionFunc, ValidConnectionFunc,
} from './utils'; } from './utils';
function resetRecentHandle(handleDomNode: Element): void { export function handlePointerDown({
handleDomNode?.classList.remove('react-flow__handle-valid');
handleDomNode?.classList.remove('react-flow__handle-connecting');
}
export function handleMouseDown({
event, event,
handleId, handleId,
nodeId, nodeId,
@@ -27,10 +23,10 @@ export function handleMouseDown({
getState, getState,
setState, setState,
isValidConnection, isValidConnection,
elementEdgeUpdaterType, edgeUpdaterType,
onEdgeUpdateEnd, onEdgeUpdateEnd,
}: { }: {
event: ReactMouseEvent; event: ReactMouseEvent | ReactTouchEvent;
handleId: string | null; handleId: string | null;
nodeId: string; nodeId: string;
onConnect: OnConnect; onConnect: OnConnect;
@@ -38,28 +34,36 @@ export function handleMouseDown({
getState: StoreApi<ReactFlowState>['getState']; getState: StoreApi<ReactFlowState>['getState'];
setState: StoreApi<ReactFlowState>['setState']; setState: StoreApi<ReactFlowState>['setState'];
isValidConnection: ValidConnectionFunc; isValidConnection: ValidConnectionFunc;
elementEdgeUpdaterType?: HandleType; edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent) => void; onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
}): void { }): void {
// when react-flow is used inside a shadow root we can't use document // when react-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
const { connectionMode, domNode, autoPanOnConnect, connectionRadius, onConnectStart, onConnectEnd, panBy, getNodes } = const {
getState(); connectionMode,
domNode,
autoPanOnConnect,
connectionRadius,
onConnectStart,
onConnectEnd,
panBy,
getNodes,
cancelConnection,
} = getState();
let autoPanId = 0; let autoPanId = 0;
let prevClosestHandle: ConnectionHandle | null; let prevClosestHandle: ConnectionHandle | null;
const clickedElement = doc?.elementFromPoint(event.clientX, event.clientY); const { x, y } = getEventPosition(event);
const elementIsTarget = clickedElement?.classList.contains('target'); const clickedHandle = doc?.elementFromPoint(x, y);
const elementIsSource = clickedElement?.classList.contains('source'); const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
if (!domNode || (!elementIsTarget && !elementIsSource && !elementEdgeUpdaterType)) { if (!containerBounds || !handleType) {
return; return;
} }
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementIsTarget ? 'target' : 'source';
const containerBounds = domNode.getBoundingClientRect();
let prevActiveHandle: Element; let prevActiveHandle: Element;
let connectionPosition = getConnectionPosition(event, containerBounds); let connectionPosition = getEventPosition(event, containerBounds);
let autoPanStarted = false; let autoPanStarted = false;
const handleLookup = getHandleLookup({ const handleLookup = getHandleLookup({
@@ -89,10 +93,10 @@ export function handleMouseDown({
onConnectStart?.(event, { nodeId, handleId, handleType }); onConnectStart?.(event, { nodeId, handleId, handleType });
function onMouseMove(event: MouseEvent) { function onPointerMove(event: MouseEvent | TouchEvent) {
const { transform } = getState(); const { transform } = getState();
connectionPosition = getEventPosition(event, containerBounds);
connectionPosition = getConnectionPosition(event, containerBounds);
prevClosestHandle = getClosestHandle( prevClosestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]), pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius, connectionRadius,
@@ -138,7 +142,7 @@ export function handleMouseDown({
} }
} }
function onMouseUp(event: MouseEvent) { function onPointerUp(event: MouseEvent | TouchEvent) {
cancelAnimationFrame(autoPanId); cancelAnimationFrame(autoPanId);
autoPanStarted = false; autoPanStarted = false;
@@ -160,22 +164,24 @@ export function handleMouseDown({
onConnectEnd?.(event); onConnectEnd?.(event);
if (elementEdgeUpdaterType) { if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event); onEdgeUpdateEnd?.(event);
} }
resetRecentHandle(prevActiveHandle); resetRecentHandle(prevActiveHandle);
setState({ cancelConnection();
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
});
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject); doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject); 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('mousemove', onPointerMove as EventListener);
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject); doc.addEventListener('mouseup', onPointerUp as EventListener);
doc.addEventListener('touchmove', onPointerMove as EventListener);
doc.addEventListener('touchend', onPointerUp as EventListener);
} }
+17 -8
View File
@@ -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 cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import { handleMouseDown } from './handler'; import { handlePointerDown } from './handler';
import { devWarn, getHostForElement } from '../../utils'; import { devWarn, getHostForElement, isMouseEvent } from '../../utils';
import { addEdge } from '../../utils/graph'; import { addEdge } from '../../utils/graph';
import { Position } from '../../types'; import { Position } from '../../types';
import type { HandleProps, Connection, ReactFlowState } from '../../types'; import type { HandleProps, Connection, ReactFlowState } from '../../types';
@@ -33,6 +33,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
children, children,
className, className,
onMouseDown, onMouseDown,
onTouchStart,
...rest ...rest
}, },
ref ref
@@ -66,9 +67,11 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
onConnect?.(edgeParams); onConnect?.(edgeParams);
}; };
const onMouseDownHandler = (event: ReactMouseEvent<HTMLDivElement>) => { const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (event.button === 0) { const isMouseTriggered = isMouseEvent(event);
handleMouseDown({
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
handlePointerDown({
event, event,
handleId, handleId,
nodeId, nodeId,
@@ -79,7 +82,12 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
isValidConnection, isValidConnection,
}); });
} }
onMouseDown?.(event);
if (isMouseTriggered) {
onMouseDown?.(event);
} else {
onTouchStart?.(event);
}
}; };
const onClick = (event: ReactMouseEvent) => { const onClick = (event: ReactMouseEvent) => {
@@ -136,7 +144,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
connectionStartHandle?.type === type, connectionStartHandle?.type === type,
}, },
])} ])}
onMouseDown={onMouseDownHandler} onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined} onClick={connectOnClick ? onClick : undefined}
ref={ref} ref={ref}
{...rest} {...rest}
+18 -7
View File
@@ -1,5 +1,3 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import { ConnectionMode } from '../../types'; import { ConnectionMode } from '../../types';
import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types'; import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types';
import { internalsSymbol } from '../../utils'; import { internalsSymbol } from '../../utils';
@@ -135,9 +133,22 @@ export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHand
}, []); }, []);
} }
export function getConnectionPosition(event: MouseEvent | ReactMouseEvent, bounds: DOMRect): XYPosition { export function getHandleType(
return { edgeUpdaterType: HandleType | undefined,
x: event.clientX - bounds.left, handleDomNode: Element | null
y: event.clientY - bounds.top, ): HandleType | null {
}; if (edgeUpdaterType) {
return edgeUpdaterType;
} else if (handleDomNode?.classList.contains('target')) {
return 'target';
} else if (handleDomNode?.classList.contains('source')) {
return 'source';
}
return null;
}
export function resetRecentHandle(handleDomNode: Element): void {
handleDomNode?.classList.remove('react-flow__handle-valid');
handleDomNode?.classList.remove('react-flow__handle-connecting');
} }
+4 -10
View File
@@ -11,8 +11,9 @@ import { containerStyle } from '../../styles';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges } from '../../utils/changes'; import { getSelectionChanges } from '../../utils/changes';
import { getConnectedEdges, getNodesInside } from '../../utils/graph'; import { getConnectedEdges, getNodesInside } from '../../utils/graph';
import { getEventPosition } from '../../utils';
import { SelectionMode } from '../../types'; import { SelectionMode } from '../../types';
import type { ReactFlowProps, XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types'; import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
type PaneProps = { type PaneProps = {
isSelecting: boolean; 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 = ( const wrapHandler = (
handler: React.MouseEventHandler | undefined, handler: React.MouseEventHandler | undefined,
containerRef: React.MutableRefObject<HTMLDivElement | null> containerRef: React.MutableRefObject<HTMLDivElement | null>
@@ -118,7 +112,7 @@ const Pane = memo(
return; return;
} }
const { x, y } = getMousePosition(event, containerBounds.current); const { x, y } = getEventPosition(event, containerBounds.current);
resetSelectedElements(); resetSelectedElements();
@@ -145,7 +139,7 @@ const Pane = memo(
store.setState({ userSelectionActive: true, nodesSelectionActive: false }); store.setState({ userSelectionActive: true, nodesSelectionActive: false });
const mousePos = getMousePosition(event, containerBounds.current); const mousePos = getEventPosition(event, containerBounds.current);
const startX = userSelectionRect.startX ?? 0; const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0; const startY = userSelectionRect.startY ?? 0;
+3 -9
View File
@@ -7,7 +7,7 @@ import { useStoreApi } from '../../hooks/useStore';
import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils'; import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils'; import { handleNodeClick } from '../../components/Nodes/utils';
import useGetPointerPosition from '../useGetPointerPosition'; import useGetPointerPosition from '../useGetPointerPosition';
import { calcAutoPan } from '../../utils'; import { calcAutoPan, getEventPosition } from '../../utils';
import type { NodeDragItem, Node, SelectionDragHandler, UseDragEvent, XYPosition } from '../../types'; import type { NodeDragItem, Node, SelectionDragHandler, UseDragEvent, XYPosition } from '../../types';
export type UseDragData = { dx: number; dy: number }; export type UseDragData = { dx: number; dy: number };
@@ -168,10 +168,7 @@ function useDrag({
} }
containerBounds.current = domNode?.getBoundingClientRect() || null; containerBounds.current = domNode?.getBoundingClientRect() || null;
mousePosition.current = { mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!);
x: event.sourceEvent.clientX - (containerBounds.current?.left ?? 0),
y: event.sourceEvent.clientY - (containerBounds.current?.top ?? 0),
};
}) })
.on('drag', (event: UseDragEvent) => { .on('drag', (event: UseDragEvent) => {
const pointerPos = getPointerPosition(event); const pointerPos = getPointerPosition(event);
@@ -188,10 +185,7 @@ function useDrag({
dragItems.current dragItems.current
) { ) {
dragEvent.current = event.sourceEvent as MouseEvent; dragEvent.current = event.sourceEvent as MouseEvent;
mousePosition.current = { mousePosition.current = getEventPosition(event.sourceEvent, containerBounds.current!);
x: event.sourceEvent.clientX - (containerBounds.current?.left ?? 0),
y: event.sourceEvent.clientY - (containerBounds.current?.top ?? 0),
};
updateNodes(pointerPos); updateNodes(pointerPos);
} }
+1
View File
@@ -273,6 +273,7 @@ const createRFStore = () =>
set({ set({
connectionNodeId: initialState.connectionNodeId, connectionNodeId: initialState.connectionNodeId,
connectionHandleId: initialState.connectionHandleId, connectionHandleId: initialState.connectionHandleId,
connectionHandleType: initialState.connectionHandleType,
}), }),
reset: () => set({ ...initialState }), reset: () => set({ ...initialState }),
})); }));
+1 -1
View File
@@ -61,7 +61,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
onEdgeMouseLeave?: EdgeMouseHandler; onEdgeMouseLeave?: EdgeMouseHandler;
onEdgeDoubleClick?: EdgeMouseHandler; onEdgeDoubleClick?: EdgeMouseHandler;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; 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; onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange; onEdgesChange?: OnEdgesChange;
onNodesDelete?: OnNodesDelete; onNodesDelete?: OnNodesDelete;
+1 -1
View File
@@ -85,7 +85,7 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
onMouseLeave?: EdgeMouseHandler; onMouseLeave?: EdgeMouseHandler;
edgeUpdaterRadius?: number; edgeUpdaterRadius?: number;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; 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; rfId?: string;
isFocusable: boolean; isFocusable: boolean;
pathOptions?: BezierPathOptions | SmoothStepPathOptions; pathOptions?: BezierPathOptions | SmoothStepPathOptions;
+8 -3
View File
@@ -1,5 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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 { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
@@ -77,8 +82,8 @@ export type OnConnectStartParams = {
handleType: HandleType | null; handleType: HandleType | null;
}; };
export type OnConnectStart = (event: ReactMouseEvent, params: OnConnectStartParams) => void; export type OnConnectStart = (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => void;
export type OnConnectEnd = (event: MouseEvent) => void; export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type Viewport = { export type Viewport = {
x: number; x: number;
+23 -1
View File
@@ -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'; import type { Dimensions, Node, XYPosition, CoordinateExtent, Box, Rect } from '../types';
@@ -111,3 +115,21 @@ export function isInputDOMNode(event: KeyboardEvent | ReactKeyboardEvent): boole
!!target?.closest('.nokey') !!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 isMouseTriggered = isMouseEvent(event);
const evtX = isMouseTriggered ? event.clientX : event.touches?.[0].clientX;
const evtY = isMouseTriggered ? event.clientY : event.touches?.[0].clientY;
return {
x: evtX - (bounds?.left ?? 0),
y: evtY - (bounds?.top ?? 0),
};
};