From 7c5c14b2c504af925978a738d308c062331b1be5 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jun 2025 17:07:40 +0200 Subject: [PATCH 1/6] feat(xyhandle): add dragThreshold #5315 --- packages/system/src/xyhandle/XYHandle.ts | 30 ++++++++++++++++++++---- packages/system/src/xyhandle/types.ts | 1 + 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index b8a5b4d2..dffdf8a1 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -45,6 +45,7 @@ function onPointerDown( getTransform, getFromHandle, autoPanSpeed, + dragThreshold = 1, }: OnPointerDownParams ) { // when xyflow is used inside a shadow root we can't use document @@ -56,6 +57,7 @@ function onPointerDown( const clickedHandle = doc?.elementFromPoint(x, y); const handleType = getHandleType(edgeUpdaterType, clickedHandle); const containerBounds = domNode?.getBoundingClientRect(); + let connectionStarted = false; if (!containerBounds || !handleType) { return; @@ -92,10 +94,9 @@ function onPointerDown( }; const fromNodeInternal = nodeLookup.get(nodeId)!; - const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true); - const newConnection: ConnectionInProgress = { + let previousConnection: ConnectionInProgress = { inProgress: true, isValid: null, @@ -110,12 +111,31 @@ function onPointerDown( toNode: null, }; - updateConnection(newConnection); - let previousConnection: ConnectionInProgress = newConnection; + function startConnection() { + updateConnection(previousConnection); + onConnectStart?.(event, { nodeId, handleId, handleType }); + } - onConnectStart?.(event, { nodeId, handleId, handleType }); + if (dragThreshold === 0) { + startConnection(); + } function onPointerMove(event: MouseEvent | TouchEvent) { + if (!connectionStarted) { + const { x: evtX, y: evtY } = getEventPosition(event); + const dx = evtX - x; + const dy = evtY - y; + const nextConnectionStarted = dx * dx + dy * dy > dragThreshold * dragThreshold; + + if (!nextConnectionStarted) { + return; + } + + startConnection(); + + connectionStarted = nextConnectionStarted; + } + if (!getFromHandle() || !fromHandle) { onPointerUp(event); return; diff --git a/packages/system/src/xyhandle/types.ts b/packages/system/src/xyhandle/types.ts index d3625c9b..baa8cf0a 100644 --- a/packages/system/src/xyhandle/types.ts +++ b/packages/system/src/xyhandle/types.ts @@ -37,6 +37,7 @@ export type OnPointerDownParams = { getTransform: () => Transform; getFromHandle: () => Handle | null; autoPanSpeed?: number; + dragThreshold?: number; }; export type IsValidParams = { From a07dc81afac5468ad75ed7a5c994f16f98e4e096 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jun 2025 17:33:44 +0200 Subject: [PATCH 2/6] feat(props): add connectionDragThreshold --- .../react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx | 1 + packages/react/src/components/Handle/index.tsx | 1 + packages/react/src/components/StoreUpdater/index.tsx | 1 + packages/react/src/container/ReactFlow/index.tsx | 2 ++ packages/react/src/store/initialState.ts | 1 + packages/react/src/types/component-props.ts | 6 ++++++ packages/react/src/types/store.ts | 1 + .../EdgeReconnectAnchor/EdgeReconnectAnchor.svelte | 4 +++- packages/svelte/src/lib/components/Handle/Handle.svelte | 3 ++- packages/svelte/src/lib/container/SvelteFlow/types.ts | 6 ++++++ packages/svelte/src/lib/store/initial-store.svelte.ts | 1 + 11 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 4aed1c21..04bdaebb 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -92,6 +92,7 @@ export function EdgeUpdateAnchors({ updateConnection, getTransform: () => store.getState().transform, getFromHandle: () => store.getState().connection.fromHandle, + dragThreshold: store.getState().connectionDragThreshold, }); }; diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index b2c3373e..31615f0c 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -149,6 +149,7 @@ function HandleComponent( getTransform: () => store.getState().transform, getFromHandle: () => store.getState().connection.fromHandle, autoPanSpeed: currentStore.autoPanSpeed, + dragThreshold: currentStore.connectionDragThreshold, }); } diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 5a5878c3..1506bd48 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -65,6 +65,7 @@ const reactFlowFieldsToTrack = [ 'isValidConnection', 'selectNodesOnDrag', 'nodeDragThreshold', + 'connectionDragThreshold', 'onBeforeDelete', 'debug', 'autoPanSpeed', diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index f0e73f4f..33b5ee54 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -139,6 +139,7 @@ function ReactFlow( style, id, nodeDragThreshold, + connectionDragThreshold, viewport, onViewportChange, width, @@ -306,6 +307,7 @@ function ReactFlow( isValidConnection={isValidConnection} selectNodesOnDrag={selectNodesOnDrag} nodeDragThreshold={nodeDragThreshold} + connectionDragThreshold={connectionDragThreshold} onBeforeDelete={onBeforeDelete} paneClickDistance={paneClickDistance} debug={debug} diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 2532b5ad..bfc9baaa 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -107,6 +107,7 @@ const getInitialState = ({ noPanClassName: 'nopan', nodeOrigin: storeNodeOrigin, nodeDragThreshold: 1, + connectionDragThreshold: 1, snapGrid: [15, 15], snapToGrid: false, diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index fc8b94a6..50021220 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -657,6 +657,12 @@ export interface ReactFlowProps [store.viewport.x, store.viewport.y, store.viewport.zoom], - getFromHandle: () => store.connection.fromHandle + getFromHandle: () => store.connection.fromHandle, + dragThreshold: store.connectionDragThreshold }); }; diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 8f5675c8..d636f4cf 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -139,7 +139,8 @@ store.onconnectend?.(event, connectionState); }, getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom], - getFromHandle: () => store.connection.fromHandle + getFromHandle: () => store.connection.fromHandle, + dragThreshold: store.connectionDragThreshold }); } } diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index d575db1a..9d40b5b3 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -170,6 +170,12 @@ export type SvelteFlowProps< * @default 0 */ nodeClickDistance?: number; + /** + * The threshold in pixels that the mouse must move before a connection line starts to drag. + * This is useful to prevent accidental connections when clicking on a handle. + * @default 1 + */ + connectionDragThreshold?: number; /** Minimum zoom level * @default 0.5 */ diff --git a/packages/svelte/src/lib/store/initial-store.svelte.ts b/packages/svelte/src/lib/store/initial-store.svelte.ts index ae09b576..a2d1ea68 100644 --- a/packages/svelte/src/lib/store/initial-store.svelte.ts +++ b/packages/svelte/src/lib/store/initial-store.svelte.ts @@ -286,6 +286,7 @@ export function getInitialStore Date: Mon, 16 Jun 2025 21:23:25 +0200 Subject: [PATCH 3/6] chore(examples): use connectionDragThreshold --- .../react/src/examples/CustomConnectionLine/index.tsx | 1 + .../routes/examples/custom-connection-line/+page.svelte | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/react/src/examples/CustomConnectionLine/index.tsx b/examples/react/src/examples/CustomConnectionLine/index.tsx index 2e5e2969..243a4f11 100644 --- a/examples/react/src/examples/CustomConnectionLine/index.tsx +++ b/examples/react/src/examples/CustomConnectionLine/index.tsx @@ -36,6 +36,7 @@ const ConnectionLineFlow = () => { onEdgesChange={onEdgesChange} connectionLineComponent={ConnectionLine} onConnect={onConnect} + connectionDragThreshold={25} > diff --git a/examples/svelte/src/routes/examples/custom-connection-line/+page.svelte b/examples/svelte/src/routes/examples/custom-connection-line/+page.svelte index c7c6f92c..1da89e75 100644 --- a/examples/svelte/src/routes/examples/custom-connection-line/+page.svelte +++ b/examples/svelte/src/routes/examples/custom-connection-line/+page.svelte @@ -24,7 +24,14 @@
- +
From 2441bf8d97a6b72494f216915d52d5acbeefefde Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jun 2025 23:20:59 +0200 Subject: [PATCH 4/6] chore(changeset): add --- .changeset/twelve-llamas-sort.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/twelve-llamas-sort.md diff --git a/.changeset/twelve-llamas-sort.md b/.changeset/twelve-llamas-sort.md new file mode 100644 index 00000000..f9cc76c9 --- /dev/null +++ b/.changeset/twelve-llamas-sort.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': minor +'@xyflow/system': minor +--- + +Add connectionDragThreshold prop From f98c1c8a59ab86f4ff15c91bac6e14389dd18439 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 17 Jun 2025 06:28:25 +0200 Subject: [PATCH 5/6] chore(svelte): cleanup --- .../components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte | 1 - packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte index 6f500f15..de8abd03 100644 --- a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte +++ b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte @@ -5,7 +5,6 @@ import { getContext } from 'svelte'; import { EdgeLabel } from '../EdgeLabel'; import type { EdgeReconnectAnchorProps } from './types'; - import drag from '$lib/actions/drag'; let { type, diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index 206fdc8c..847dc4e4 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -64,6 +64,7 @@ fitViewOptions, nodeOrigin, nodeDragThreshold, + connectionDragThreshold, minZoom, maxZoom, initialViewport, From 46b2626273d6d8af4dfe6cd1705b0091280b836a Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 17 Jun 2025 12:10:20 +0200 Subject: [PATCH 6/6] fix(reconnect): use connectionDragThreshold --- .../EdgeWrapper/EdgeUpdateAnchors.tsx | 19 ++++++++++++++----- .../EdgeReconnectAnchor.svelte | 14 +++++++++----- .../components/EdgeReconnectAnchor/types.ts | 1 + 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 04bdaebb..c9f3b7f0 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -1,5 +1,12 @@ // Reconnectable edges have a anchors around their handles to reconnect the edge. -import { XYHandle, type Connection, EdgePosition, FinalConnectionState, HandleType } from '@xyflow/system'; +import { + XYHandle, + type Connection, + EdgePosition, + FinalConnectionState, + HandleType, + OnConnectStart, +} from '@xyflow/system'; import { EdgeAnchor } from '../Edges/EdgeAnchor'; import type { EdgeWrapperProps, Edge } from '../../types/edges'; @@ -60,15 +67,17 @@ export function EdgeUpdateAnchors({ } = store.getState(); const isTarget = oppositeHandle.type === 'target'; - setReconnecting(true); - onReconnectStart?.(event, edge, oppositeHandle.type); - const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => { setReconnecting(false); onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState); }; const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection); + const _onConnectStart: OnConnectStart = (_event, params) => { + setReconnecting(true); + onReconnectStart?.(event, edge, oppositeHandle.type); + onConnectStart?.(_event, params); + }; XYHandle.onPointerDown(event.nativeEvent, { autoPanOnConnect, @@ -86,7 +95,7 @@ export function EdgeUpdateAnchors({ panBy, isValidConnection, onConnect: onConnectEdge, - onConnectStart, + onConnectStart: _onConnectStart, onConnectEnd, onReconnectEnd: _onReconnectEnd, updateConnection, diff --git a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte index de8abd03..c4b95fa5 100644 --- a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte +++ b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte @@ -1,7 +1,7 @@ diff --git a/packages/svelte/src/lib/components/EdgeReconnectAnchor/types.ts b/packages/svelte/src/lib/components/EdgeReconnectAnchor/types.ts index 74ed6d6d..38b71232 100644 --- a/packages/svelte/src/lib/components/EdgeReconnectAnchor/types.ts +++ b/packages/svelte/src/lib/components/EdgeReconnectAnchor/types.ts @@ -10,4 +10,5 @@ export type EdgeReconnectAnchorProps = { position?: XYPosition; size?: number; children?: Snippet; + dragThreshold?: number; } & HTMLAttributes;