diff --git a/.changeset/funny-doors-travel.md b/.changeset/funny-doors-travel.md new file mode 100644 index 00000000..f7ad710c --- /dev/null +++ b/.changeset/funny-doors-travel.md @@ -0,0 +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/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..d3ca4515 --- /dev/null +++ b/examples/react/src/examples/DetachedHandle/index.tsx @@ -0,0 +1,62 @@ +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 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} +
+ + + + + +
+ + diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index c9f3b7f0..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'; @@ -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 31615f0c..dec2d242 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -114,7 +114,6 @@ function HandleComponent( onConnectAction?.(edgeParams); onConnect?.(edgeParams); }; - const onPointerDown = (event: ReactMouseEvent | ReactTouchEvent) => { if (!nodeId) { return; @@ -129,6 +128,7 @@ function HandleComponent( const currentStore = store.getState(); 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 f6326ede..aa71d49d 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -46,6 +46,7 @@ function onPointerDown( getFromHandle, autoPanSpeed, dragThreshold = 1, + handleDomNode, }: OnPointerDownParams ) { // when xyflow is used inside a shadow root we can't use document @@ -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, 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 baa8cf0a..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 = {