From 428f2e8fb95eaf30be06687a66c9c41062523eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karlo=20Bistri=C4=8Dki?= Date: Thu, 31 Jul 2025 15:55:11 +0200 Subject: [PATCH 1/6] Get clicked handle element from event.currentTarget --- packages/react/src/components/Handle/index.tsx | 3 ++- packages/system/src/xyhandle/XYHandle.ts | 4 ++-- packages/system/src/xyhandle/types.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 31615f0c..601c0a31 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -120,6 +120,7 @@ function HandleComponent( return; } + const handleElement = event.currentTarget const isMouseTriggered = isMouseEvent(event.nativeEvent); if ( @@ -128,7 +129,7 @@ function HandleComponent( ) { const currentStore = store.getState(); - XYHandle.onPointerDown(event.nativeEvent, { + XYHandle.onPointerDown(event.nativeEvent, handleElement, { autoPanOnConnect: currentStore.autoPanOnConnect, connectionMode: currentStore.connectionMode, connectionRadius: currentStore.connectionRadius, diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index f6326ede..dc470daf 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -22,6 +22,7 @@ const alwaysValid = () => true; function onPointerDown( event: MouseEvent | TouchEvent, + handleElement: Element, { connectionMode, connectionRadius, @@ -54,8 +55,7 @@ function onPointerDown( let closestHandle: Handle | null; const { x, y } = getEventPosition(event); - const clickedHandle = doc?.elementFromPoint(x, y); - const handleType = getHandleType(edgeUpdaterType, clickedHandle); + const handleType = getHandleType(edgeUpdaterType, handleElement); const containerBounds = domNode?.getBoundingClientRect(); let connectionStarted = false; diff --git a/packages/system/src/xyhandle/types.ts b/packages/system/src/xyhandle/types.ts index baa8cf0a..a71eb962 100644 --- a/packages/system/src/xyhandle/types.ts +++ b/packages/system/src/xyhandle/types.ts @@ -54,7 +54,7 @@ export type IsValidParams = { }; export type XYHandleInstance = { - onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void; + onPointerDown: (event: MouseEvent | TouchEvent, handleElement: Element, params: OnPointerDownParams) => void; isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result; }; From 940b42de895c80966012a8d8da3ce2cc67bf64e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karlo=20Bistri=C4=8Dki?= Date: Thu, 31 Jul 2025 16:18:45 +0200 Subject: [PATCH 2/6] add DetachedHandle example --- examples/react/src/App/routes.ts | 6 +++ .../src/examples/DetachedHandle/index.tsx | 53 +++++++++++++++++++ .../src/examples/DetachedHandle/style.css | 10 ++++ 3 files changed, 69 insertions(+) create mode 100644 examples/react/src/examples/DetachedHandle/index.tsx create mode 100644 examples/react/src/examples/DetachedHandle/style.css diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 4c003ea5..52f80979 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -58,6 +58,7 @@ import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop'; import DevTools from '../examples/DevTools'; import Redux from '../examples/Redux'; import MovingHandles from '../examples/MovingHandles'; +import DetachedHandle from '../examples/DetachedHandle'; export interface IRoute { name: string; @@ -146,6 +147,11 @@ const routes: IRoute[] = [ path: 'default-nodes', component: DefaultNodes, }, + { + name: 'DetachedHandle', + path: 'detached-handle', + component: DetachedHandle, + }, { name: 'DevTools', path: 'devtools', diff --git a/examples/react/src/examples/DetachedHandle/index.tsx b/examples/react/src/examples/DetachedHandle/index.tsx new file mode 100644 index 00000000..9ca20ddc --- /dev/null +++ b/examples/react/src/examples/DetachedHandle/index.tsx @@ -0,0 +1,53 @@ +import { ReactFlow, Node, ReactFlowProvider, Background, BackgroundVariant, NodeProps, Handle, Position } from '@xyflow/react'; + +import './style.css'; + +const initialNodes: Node[] = [ + { + id: '1', + data: { label: 'Node 1' }, + position: { x: 250, y: 5 }, + }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 50, y: 100 }, + }, + { + id: '3', + data: { label: 'Node 3' }, + position: { x: 450, y: 100 }, + }, +]; + +const CustomNode = (_: NodeProps) => { + return ( + <> + +
Custom node
+ + + + + ); +}; + +const nodeTypes = { + default: CustomNode, +}; + +const DetachedHandle = () => { + return ( + + + + ); +}; + +export default function App() { + return ( + + + + ); +} diff --git a/examples/react/src/examples/DetachedHandle/style.css b/examples/react/src/examples/DetachedHandle/style.css new file mode 100644 index 00000000..941c9807 --- /dev/null +++ b/examples/react/src/examples/DetachedHandle/style.css @@ -0,0 +1,10 @@ +.detached-handle { + position: absolute; + top: 50%; + left: 1rem; + transform: translateY(-50%); + width: 2rem; + height: 2rem; + border: none; + border-radius: 50%; +} \ No newline at end of file From bbdc67c858f3d77b9893e064873648ab4ef247ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karlo=20Bistri=C4=8Dki?= Date: Thu, 31 Jul 2025 16:23:44 +0200 Subject: [PATCH 3/6] fix EdgeUpdateAnchors --- packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index c9f3b7f0..73a900f9 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -79,7 +79,7 @@ export function EdgeUpdateAnchors({ onConnectStart?.(_event, params); }; - XYHandle.onPointerDown(event.nativeEvent, { + XYHandle.onPointerDown(event.nativeEvent, event.currentTarget, { autoPanOnConnect, connectionMode, connectionRadius, From f18e98569b1cc38b6ec2b7d7a3d1fd8b56a5d42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karlo=20Bistri=C4=8Dki?= Date: Thu, 31 Jul 2025 16:43:32 +0200 Subject: [PATCH 4/6] Add changeset --- .changeset/funny-doors-travel.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/funny-doors-travel.md diff --git a/.changeset/funny-doors-travel.md b/.changeset/funny-doors-travel.md new file mode 100644 index 00000000..d4af2f2d --- /dev/null +++ b/.changeset/funny-doors-travel.md @@ -0,0 +1,6 @@ +--- +'@xyflow/react': patch +'@xyflow/system': patch +--- + +Fix clicking on detached handle elements not initiating drawing of connections From 295884fea9fa4cb2ea6e03d8e6d2b7d1206c15ae Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 12 Aug 2025 13:16:56 +0200 Subject: [PATCH 5/6] chore(handle): cleanup --- .changeset/funny-doors-travel.md | 1 + .../react/src/examples/DetachedHandle/index.tsx | 13 +++++++++++-- .../components/EdgeWrapper/EdgeUpdateAnchors.tsx | 11 ++++++----- packages/react/src/components/Handle/index.tsx | 5 ++--- .../EdgeReconnectAnchor/EdgeReconnectAnchor.svelte | 3 ++- .../svelte/src/lib/components/Handle/Handle.svelte | 5 +++-- packages/system/src/xyhandle/XYHandle.ts | 12 ++++++------ packages/system/src/xyhandle/types.ts | 3 ++- 8 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.changeset/funny-doors-travel.md b/.changeset/funny-doors-travel.md index d4af2f2d..f7ad710c 100644 --- a/.changeset/funny-doors-travel.md +++ b/.changeset/funny-doors-travel.md @@ -1,6 +1,7 @@ --- '@xyflow/react': patch '@xyflow/system': patch +'@xyflow/svelte': patch --- Fix clicking on detached handle elements not initiating drawing of connections diff --git a/examples/react/src/examples/DetachedHandle/index.tsx b/examples/react/src/examples/DetachedHandle/index.tsx index 9ca20ddc..d3ca4515 100644 --- a/examples/react/src/examples/DetachedHandle/index.tsx +++ b/examples/react/src/examples/DetachedHandle/index.tsx @@ -1,4 +1,13 @@ -import { ReactFlow, Node, ReactFlowProvider, Background, BackgroundVariant, NodeProps, Handle, Position } from '@xyflow/react'; +import { + ReactFlow, + Node, + ReactFlowProvider, + Background, + BackgroundVariant, + NodeProps, + Handle, + Position, +} from '@xyflow/react'; import './style.css'; @@ -38,7 +47,7 @@ const nodeTypes = { const DetachedHandle = () => { return ( - + ); diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 73a900f9..36a983aa 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -1,11 +1,11 @@ // Reconnectable edges have a anchors around their handles to reconnect the edge. import { XYHandle, + type EdgePosition, + type FinalConnectionState, + type HandleType, + type OnConnectStart, type Connection, - EdgePosition, - FinalConnectionState, - HandleType, - OnConnectStart, } from '@xyflow/system'; import { EdgeAnchor } from '../Edges/EdgeAnchor'; @@ -79,7 +79,7 @@ export function EdgeUpdateAnchors({ onConnectStart?.(_event, params); }; - XYHandle.onPointerDown(event.nativeEvent, event.currentTarget, { + XYHandle.onPointerDown(event.nativeEvent, { autoPanOnConnect, connectionMode, connectionRadius, @@ -102,6 +102,7 @@ export function EdgeUpdateAnchors({ getTransform: () => store.getState().transform, getFromHandle: () => store.getState().connection.fromHandle, dragThreshold: store.getState().connectionDragThreshold, + handleDomNode: event.currentTarget, }); }; diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 601c0a31..dec2d242 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -114,13 +114,11 @@ function HandleComponent( onConnectAction?.(edgeParams); onConnect?.(edgeParams); }; - const onPointerDown = (event: ReactMouseEvent | ReactTouchEvent) => { if (!nodeId) { return; } - const handleElement = event.currentTarget const isMouseTriggered = isMouseEvent(event.nativeEvent); if ( @@ -129,7 +127,8 @@ function HandleComponent( ) { const currentStore = store.getState(); - XYHandle.onPointerDown(event.nativeEvent, handleElement, { + XYHandle.onPointerDown(event.nativeEvent, { + handleDomNode: event.currentTarget, autoPanOnConnect: currentStore.autoPanOnConnect, connectionMode: currentStore.connectionMode, connectionRadius: currentStore.connectionRadius, diff --git a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte index c4b95fa5..d18bc2e8 100644 --- a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte +++ b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte @@ -102,7 +102,8 @@ updateConnection, getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom], getFromHandle: () => store.connection.fromHandle, - dragThreshold: dragThreshold ?? store.connectionDragThreshold + dragThreshold: dragThreshold ?? store.connectionDragThreshold, + handleDomNode: event.currentTarget as HTMLElement }); }; diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index d636f4cf..48946d41 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -111,7 +111,7 @@ function onpointerdown(event: MouseEvent | TouchEvent) { const isMouseTriggered = isMouseEvent(event); - if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) { + if (event.currentTarget && ((isMouseTriggered && event.button === 0) || !isMouseTriggered)) { XYHandle.onPointerDown(event, { handleId, nodeId, @@ -140,7 +140,8 @@ }, getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom], getFromHandle: () => store.connection.fromHandle, - dragThreshold: store.connectionDragThreshold + dragThreshold: store.connectionDragThreshold, + handleDomNode: event.currentTarget as HTMLElement }); } } diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index dc470daf..aa71d49d 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -22,7 +22,6 @@ const alwaysValid = () => true; function onPointerDown( event: MouseEvent | TouchEvent, - handleElement: Element, { connectionMode, connectionRadius, @@ -47,6 +46,7 @@ function onPointerDown( getFromHandle, autoPanSpeed, dragThreshold = 1, + handleDomNode, }: OnPointerDownParams ) { // when xyflow is used inside a shadow root we can't use document @@ -55,7 +55,7 @@ function onPointerDown( let closestHandle: Handle | null; const { x, y } = getEventPosition(event); - const handleType = getHandleType(edgeUpdaterType, handleElement); + const handleType = getHandleType(edgeUpdaterType, handleDomNode); const containerBounds = domNode?.getBoundingClientRect(); let connectionStarted = false; @@ -72,7 +72,7 @@ function onPointerDown( let autoPanStarted = false; let connection: Connection | null = null; let isValid: boolean | null = false; - let handleDomNode: Element | null = null; + let resultHandleDomNode: Element | null = null; // when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas function autoPan(): void { @@ -167,7 +167,7 @@ function onPointerDown( nodeLookup, }); - handleDomNode = result.handleDomNode; + resultHandleDomNode = result.handleDomNode; connection = result.connection; isValid = isConnectionValid(!!closestHandle, result.isValid); @@ -208,7 +208,7 @@ function onPointerDown( function onPointerUp(event: MouseEvent | TouchEvent) { if (connectionStarted) { - if ((closestHandle || handleDomNode) && connection && isValid) { + if ((closestHandle || resultHandleDomNode) && connection && isValid) { onConnect?.(connection); } @@ -235,7 +235,7 @@ function onPointerDown( autoPanStarted = false; isValid = false; connection = null; - handleDomNode = null; + resultHandleDomNode = null; doc.removeEventListener('mousemove', onPointerMove as EventListener); doc.removeEventListener('mouseup', onPointerUp as EventListener); diff --git a/packages/system/src/xyhandle/types.ts b/packages/system/src/xyhandle/types.ts index a71eb962..11d553ff 100644 --- a/packages/system/src/xyhandle/types.ts +++ b/packages/system/src/xyhandle/types.ts @@ -38,6 +38,7 @@ export type OnPointerDownParams = { getFromHandle: () => Handle | null; autoPanSpeed?: number; dragThreshold?: number; + handleDomNode: Element; }; export type IsValidParams = { @@ -54,7 +55,7 @@ export type IsValidParams = { }; export type XYHandleInstance = { - onPointerDown: (event: MouseEvent | TouchEvent, handleElement: Element, params: OnPointerDownParams) => void; + onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void; isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result; }; From 9c0d88ca3447e5787547d4fac7f98106c7beae91 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 12 Aug 2025 13:36:33 +0200 Subject: [PATCH 6/6] chore(svelte): add detached handle example --- .../src/components/Header/Header.svelte | 5 ++- .../examples/detached-handle/+page.svelte | 41 +++++++++++++++++++ .../detached-handle/CustomNode.svelte | 36 ++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 examples/svelte/src/routes/examples/detached-handle/+page.svelte create mode 100644 examples/svelte/src/routes/examples/detached-handle/CustomNode.svelte diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index f6260917..1db10784 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -1,6 +1,6 @@ + + + + + diff --git a/examples/svelte/src/routes/examples/detached-handle/CustomNode.svelte b/examples/svelte/src/routes/examples/detached-handle/CustomNode.svelte new file mode 100644 index 00000000..02335f7a --- /dev/null +++ b/examples/svelte/src/routes/examples/detached-handle/CustomNode.svelte @@ -0,0 +1,36 @@ + + +
+
+ {data.label} +
+ + + + + +
+ +