improvement(workflow): use DOM hit-testing for edge drop-on-block detection (#3851)

This commit is contained in:
Waleed
2026-03-30 16:20:30 -07:00
committed by GitHub
parent 5c334874eb
commit 4ae5b1b620
@@ -263,7 +263,7 @@ const WorkflowContent = React.memo(
const params = useParams()
const router = useRouter()
const reactFlowInstance = useReactFlow()
const { screenToFlowPosition, getNodes, setNodes, getIntersectingNodes } = reactFlowInstance
const { screenToFlowPosition, getNodes, setNodes } = reactFlowInstance
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, {
embedded,
})
@@ -2849,38 +2849,29 @@ const WorkflowContent = React.memo(
)
/**
* Finds the best node at a given flow position for drop-on-block connection.
* Skips subflow containers as they have their own connection logic.
* Finds the node under the cursor using DOM hit-testing for pixel-perfect
* detection that matches exactly what the user sees on screen.
* Uses the same approach as ReactFlow's internal handle detection.
*/
const findNodeAtPosition = useCallback(
(position: { x: number; y: number }) => {
const cursorRect = {
x: position.x - 1,
y: position.y - 1,
width: 2,
height: 2,
const findNodeAtScreenPosition = useCallback(
(clientX: number, clientY: number) => {
const elements = document.elementsFromPoint(clientX, clientY)
const nodes = getNodes()
for (const el of elements) {
const nodeEl = el.closest('.react-flow__node') as HTMLElement | null
if (!nodeEl) continue
const nodeId = nodeEl.getAttribute('data-id')
if (!nodeId) continue
const node = nodes.find((n) => n.id === nodeId)
if (node && node.type !== 'subflowNode') return node
}
const intersecting = getIntersectingNodes(cursorRect, true).filter(
(node) => node.type !== 'subflowNode'
)
if (intersecting.length === 0) return undefined
if (intersecting.length === 1) return intersecting[0]
return intersecting.reduce((closest, node) => {
const getDistance = (n: Node) => {
const absPos = getNodeAbsolutePosition(n.id)
const dims = getBlockDimensions(n.id)
const centerX = absPos.x + dims.width / 2
const centerY = absPos.y + dims.height / 2
return Math.hypot(position.x - centerX, position.y - centerY)
}
return getDistance(node) < getDistance(closest) ? node : closest
})
return undefined
},
[getIntersectingNodes, getNodeAbsolutePosition, getBlockDimensions]
[getNodes]
)
/**
@@ -3005,15 +2996,9 @@ const WorkflowContent = React.memo(
return
}
// Get cursor position in flow coordinates
// Find node under cursor using DOM hit-testing
const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event
const flowPosition = screenToFlowPosition({
x: clientPos.clientX,
y: clientPos.clientY,
})
// Find node under cursor
const targetNode = findNodeAtPosition(flowPosition)
const targetNode = findNodeAtScreenPosition(clientPos.clientX, clientPos.clientY)
// Create connection if valid target found (handle-to-body case)
if (targetNode && targetNode.id !== source.nodeId) {
@@ -3027,7 +3012,7 @@ const WorkflowContent = React.memo(
connectionSourceRef.current = null
},
[screenToFlowPosition, findNodeAtPosition, onConnect]
[findNodeAtScreenPosition, onConnect]
)
/** Handles node drag to detect container intersections and update highlighting. */