From c428b906b759dd7e08cc919942e0f8bc9c9c4a3d Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 9 May 2023 17:08:14 +0200 Subject: [PATCH 1/2] refactor(handles): reduce re-renderings, handles on top of each other #3010 --- .../src/examples/EasyConnect/CustomNode.tsx | 18 +++++++-------- .../src/examples/EasyConnect/style.css | 14 +----------- .../core/src/components/Handle/handler.ts | 20 ++++++++--------- packages/core/src/components/Handle/index.tsx | 2 +- packages/core/src/components/Handle/utils.ts | 22 +++++++++++++++---- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx b/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx index 989df5fa..bd107c93 100644 --- a/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx +++ b/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx @@ -2,8 +2,9 @@ import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId; -export default function CustomNode({ id, isConnectable }: NodeProps) { +export default function CustomNode({ id }: NodeProps) { const connectionNodeId = useStore(connectionNodeIdSelector); + const isConnecting = !!connectionNodeId; const isTarget = connectionNodeId && connectionNodeId !== id; const targetHandleStyle = { zIndex: isTarget ? 3 : 1 }; @@ -18,19 +19,16 @@ export default function CustomNode({ id, isConnectable }: NodeProps) { backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6', }} > + {!isConnecting && ( + + )} + - {label} diff --git a/examples/vite-app/src/examples/EasyConnect/style.css b/examples/vite-app/src/examples/EasyConnect/style.css index c2ebc0a2..8e06c4ea 100644 --- a/examples/vite-app/src/examples/EasyConnect/style.css +++ b/examples/vite-app/src/examples/EasyConnect/style.css @@ -28,19 +28,7 @@ border: 2px solid #222138; } -div.sourceHandle { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - border-radius: 0; - transform: none; - border: none; - opacity: 0; -} - -div.targetHandle { +div.customHandle { width: 100%; height: 100%; background: blue; diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts index 9825295b..0b598dec 100644 --- a/packages/core/src/components/Handle/handler.ts +++ b/packages/core/src/components/Handle/handler.ts @@ -51,7 +51,7 @@ export function handlePointerDown({ cancelConnection, } = getState(); let autoPanId = 0; - let prevClosestHandle: ConnectionHandle | null; + let closestHandle: ConnectionHandle | null; const { x, y } = getEventPosition(event); const clickedHandle = doc?.elementFromPoint(x, y); @@ -106,9 +106,9 @@ export function handlePointerDown({ function onPointerMove(event: MouseEvent | TouchEvent) { const { transform } = getState(); - connectionPosition = getEventPosition(event, containerBounds); - prevClosestHandle = getClosestHandle( + connectionPosition = getEventPosition(event, containerBounds); + closestHandle = getClosestHandle( pointToRendererPoint(connectionPosition, transform, false, [1, 1]), connectionRadius, handleLookup @@ -121,7 +121,7 @@ export function handlePointerDown({ const result = isValidHandle( event, - prevClosestHandle, + closestHandle, connectionMode, nodeId, handleId, @@ -136,20 +136,20 @@ export function handlePointerDown({ setState({ connectionPosition: - prevClosestHandle && isValid + closestHandle && isValid ? rendererPointToPoint( { - x: prevClosestHandle.x, - y: prevClosestHandle.y, + x: closestHandle.x, + y: closestHandle.y, }, transform ) : connectionPosition, - connectionStatus: getConnectionStatus(!!prevClosestHandle, isValid), + connectionStatus: getConnectionStatus(!!closestHandle, isValid), connectionEndHandle: result.endHandle, }); - if (!prevClosestHandle && !isValid && !handleDomNode) { + if (!closestHandle && !isValid && !handleDomNode) { return resetRecentHandle(prevActiveHandle); } @@ -164,7 +164,7 @@ export function handlePointerDown({ } function onPointerUp(event: MouseEvent | TouchEvent) { - if ((prevClosestHandle || handleDomNode) && connection && isValid) { + if ((closestHandle || handleDomNode) && connection && isValid) { onConnect?.(connection); } diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx index dcd95d7c..ca724285 100644 --- a/packages/core/src/components/Handle/index.tsx +++ b/packages/core/src/components/Handle/index.tsx @@ -62,7 +62,7 @@ const Handle = forwardRef( const store = useStoreApi(); const nodeId = useNodeId(); const { connectOnClick, noPanClassName } = useStore(selector, shallow); - const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type)); + const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type), shallow); if (!nodeId) { store.getState().onError?.('010', errorMessages['error010']()); diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts index b8e8cf6a..353b249a 100644 --- a/packages/core/src/components/Handle/utils.ts +++ b/packages/core/src/components/Handle/utils.ts @@ -41,18 +41,30 @@ export function getClosestHandle( connectionRadius: number, handles: ConnectionHandle[] ): ConnectionHandle | null { - let closestHandle: ConnectionHandle | null = null; + let closestHandles: ConnectionHandle[] = []; let minDistance = Infinity; handles.forEach((handle) => { const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2)); - if (distance <= connectionRadius && distance < minDistance) { + if (distance <= connectionRadius) { + if (distance < minDistance) { + closestHandles = [handle]; + } else if (distance === minDistance) { + // when multiple handles are on the same distance we collect all of them + closestHandles.push(handle); + } minDistance = distance; - closestHandle = handle; } }); - return closestHandle; + if (!closestHandles.length) { + return null; + } + + return closestHandles.length === 1 + ? closestHandles[0] + : // if multiple handles are layouted on top of each other we take the one with type = target because it's more likely that the user wants to connect to this one + closestHandles.find((handle) => handle.type === 'target') || closestHandles[0]; } type Result = { @@ -81,6 +93,8 @@ export function isValidHandle( ); const { x, y } = getEventPosition(event); const handleBelow = doc.elementFromPoint(x, y); + // we always want to prioritize the handle below the mouse cursor over the closest distance handle, + // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor const handleToCheck = handleBelow?.classList.contains('react-flow__handle') ? handleBelow : handleDomNode; const result: Result = { From cf7a7d3dad1e73215a72a5dc72e21fd50208cdbb Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 9 May 2023 17:09:28 +0200 Subject: [PATCH 2/2] chore(changeset): add --- .changeset/dirty-tools-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-tools-switch.md diff --git a/.changeset/dirty-tools-switch.md b/.changeset/dirty-tools-switch.md new file mode 100644 index 00000000..d07b3e1d --- /dev/null +++ b/.changeset/dirty-tools-switch.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +handles: handles on top of each other, reduce re-renderings