diff --git a/.changeset/thick-needles-march.md b/.changeset/thick-needles-march.md
new file mode 100644
index 00000000..26d0890e
--- /dev/null
+++ b/.changeset/thick-needles-march.md
@@ -0,0 +1,7 @@
+---
+'@reactflow/background': minor
+'@reactflow/core': minor
+'@reactflow/minimap': minor
+---
+
+Handles: add isConnectableStart and isConnectableEnd props
diff --git a/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx b/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx
index 4102b9a7..43cdb5bf 100644
--- a/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx
+++ b/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx
@@ -6,15 +6,21 @@ import styles from './validation.module.css';
const selector = (state: ReactFlowState) => ({
connectionPosition: state.connectionPosition,
connectionStatus: state.connectionStatus,
- connectionNodeId: state.connectionNodeId,
- connectionTargetNodeId: state.connectionTargetNodeId,
+ connectionStartNodeId: state.connectionStartHandle?.nodeId,
+ connectionStartHandleType: state.connectionStartHandle?.type,
+ connectionEndNodeId: state.connectionEndHandle?.nodeId,
+ connectionEndHandleType: state.connectionEndHandle?.type,
});
function ConnectionStatus() {
- const { connectionPosition, connectionStatus, connectionNodeId, connectionTargetNodeId } = useStore(
- selector,
- shallow
- );
+ const {
+ connectionPosition,
+ connectionStatus,
+ connectionStartNodeId,
+ connectionStartHandleType,
+ connectionEndNodeId,
+ connectionEndHandleType,
+ } = useStore(selector, shallow);
if (!connectionPosition) {
return null;
@@ -22,15 +28,17 @@ function ConnectionStatus() {
return (
- {connectionNodeId ? (
+ {connectionStartNodeId ? (
<>
connection info
position: {JSON.stringify(connectionPosition)}
-
status: {JSON.stringify(connectionStatus)}
-
source node id: {JSON.stringify(connectionNodeId)}
-
target node id: {JSON.stringify(connectionTargetNodeId)}
+
status: {connectionStatus}
+
from node id: {connectionStartNodeId}
+
from handle type: {connectionStartHandleType}
+
to node id: {connectionEndNodeId}
+
to handle type: {connectionEndHandleType}
>
) : (
'no connection data'
diff --git a/examples/vite-app/src/examples/Validation/index.tsx b/examples/vite-app/src/examples/Validation/index.tsx
index ed2b5419..f385083b 100644
--- a/examples/vite-app/src/examples/Validation/index.tsx
+++ b/examples/vite-app/src/examples/Validation/index.tsx
@@ -38,7 +38,7 @@ const CustomInput: FC
= () => (
const CustomNode: FC = ({ id }) => (
<>
-
+
{id}
>
diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts
index 3ad191b8..9825295b 100644
--- a/packages/core/src/components/Handle/handler.ts
+++ b/packages/core/src/components/Handle/handler.ts
@@ -89,12 +89,17 @@ export function handlePointerDown({
setState({
connectionPosition,
+ connectionStatus: null,
+ // connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionNodeId: nodeId,
connectionHandleId: handleId,
connectionHandleType: handleType,
- connectionStatus: null,
- connectionTargetNodeId: null,
- connectionTargetHandleId: null,
+ connectionStartHandle: {
+ nodeId,
+ handleId,
+ type: handleType,
+ },
+ connectionEndHandle: null,
});
onConnectStart?.(event, { nodeId, handleId, handleType });
@@ -141,8 +146,7 @@ export function handlePointerDown({
)
: connectionPosition,
connectionStatus: getConnectionStatus(!!prevClosestHandle, isValid),
- connectionTargetNodeId: connection.target,
- connectionTargetHandleId: connection.targetHandle,
+ connectionEndHandle: result.endHandle,
});
if (!prevClosestHandle && !isValid && !handleDomNode) {
diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx
index c66f5272..dcd95d7c 100644
--- a/packages/core/src/components/Handle/index.tsx
+++ b/packages/core/src/components/Handle/index.tsx
@@ -7,8 +7,7 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import { handlePointerDown } from './handler';
import { getHostForElement, isMouseEvent } from '../../utils';
import { addEdge } from '../../utils/graph';
-import { Position } from '../../types';
-import type { HandleProps, Connection, ReactFlowState } from '../../types';
+import { type HandleProps, type Connection, type ReactFlowState, HandleType, Position } from '../../types';
import { isValidHandle } from './utils';
import { errorMessages } from '../../contants';
@@ -22,6 +21,23 @@ const selector = (s: ReactFlowState) => ({
noPanClassName: s.noPanClassName,
});
+const connectingSelector =
+ (nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
+ const {
+ connectionStartHandle: startHandle,
+ connectionEndHandle: endHandle,
+ connectionClickStartHandle: clickHandle,
+ } = state;
+
+ return {
+ connecting:
+ (startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type) ||
+ (endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type),
+ clickConnecting:
+ clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type,
+ };
+ };
+
const Handle = forwardRef(
(
{
@@ -29,6 +45,8 @@ const Handle = forwardRef(
position = Position.Top,
isValidConnection,
isConnectable = true,
+ isConnectableStart = true,
+ isConnectableEnd = true,
id,
onConnect,
children,
@@ -39,20 +57,17 @@ const Handle = forwardRef(
},
ref
) => {
+ const handleId = id || null;
+ const isTarget = type === 'target';
const store = useStoreApi();
const nodeId = useNodeId();
+ const { connectOnClick, noPanClassName } = useStore(selector, shallow);
+ const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type));
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
-
- return null;
}
- const { connectionStartHandle, connectOnClick, noPanClassName } = useStore(selector, shallow);
-
- const handleId = id || null;
- const isTarget = type === 'target';
-
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
@@ -70,9 +85,13 @@ const Handle = forwardRef(
};
const onPointerDown = (event: ReactMouseEvent | ReactTouchEvent) => {
+ if (!nodeId) {
+ return;
+ }
+
const isMouseTriggered = isMouseEvent(event);
- if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
+ if (isConnectableStart && ((isMouseTriggered && event.button === 0) || !isMouseTriggered)) {
handlePointerDown({
event,
handleId,
@@ -96,12 +115,18 @@ const Handle = forwardRef(
const {
onClickConnectStart,
onClickConnectEnd,
+ connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
} = store.getState();
- if (!connectionStartHandle) {
+
+ if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
+ return;
+ }
+
+ if (!connectionClickStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
- store.setState({ connectionStartHandle: { nodeId, type, handleId } });
+ store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
@@ -115,9 +140,9 @@ const Handle = forwardRef(
type,
},
connectionMode,
- connectionStartHandle.nodeId,
- connectionStartHandle.handleId || null,
- connectionStartHandle.type,
+ connectionClickStartHandle.nodeId,
+ connectionClickStartHandle.handleId || null,
+ connectionClickStartHandle.type,
isValidConnectionHandler,
doc
);
@@ -128,7 +153,7 @@ const Handle = forwardRef(
onClickConnectEnd?.(event as unknown as MouseEvent);
- store.setState({ connectionStartHandle: null });
+ store.setState({ connectionClickStartHandle: null });
};
return (
@@ -147,10 +172,12 @@ const Handle = forwardRef(
source: !isTarget,
target: isTarget,
connectable: isConnectable,
- connecting:
- connectionStartHandle?.nodeId === nodeId &&
- connectionStartHandle?.handleId === handleId &&
- connectionStartHandle?.type === type,
+ connectablestart: isConnectableStart,
+ connectableend: isConnectableEnd,
+ connecting: clickConnecting,
+ // this class is used to style the handle when the user is connecting
+ connectionindicator:
+ isConnectable && ((isConnectableStart && !connecting) || (isConnectableEnd && connecting)),
},
])}
onMouseDown={onPointerDown}
diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts
index bfd181e1..b8e8cf6a 100644
--- a/packages/core/src/components/Handle/utils.ts
+++ b/packages/core/src/components/Handle/utils.ts
@@ -1,6 +1,6 @@
import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
-import { ConnectionMode, ConnectionStatus } from '../../types';
+import { ConnectingHandle, ConnectionMode, ConnectionStatus } from '../../types';
import { getEventPosition, internalsSymbol } from '../../utils';
import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types';
@@ -59,6 +59,7 @@ type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection;
+ endHandle: ConnectingHandle | null;
};
const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
@@ -70,7 +71,7 @@ export function isValidHandle(
connectionMode: ConnectionMode,
fromNodeId: string,
fromHandleId: string | null,
- fromType: string,
+ fromType: HandleType,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
@@ -86,12 +87,15 @@ export function isValidHandle(
handleDomNode: handleToCheck,
isValid: false,
connection: nullConnection,
+ endHandle: null,
};
if (handleToCheck) {
const handleType = getHandleType(undefined, handleToCheck);
const handleNodeId = handleToCheck.getAttribute('data-nodeid');
const handleId = handleToCheck.getAttribute('data-handleid');
+ const connectable = handleToCheck.classList.contains('connectable');
+ const connectableEnd = handleToCheck.classList.contains('connectableend');
const connection: Connection = {
source: isTarget ? handleNodeId : fromNodeId,
@@ -102,14 +106,21 @@ export function isValidHandle(
result.connection = connection;
+ const isConnectable = connectable && connectableEnd;
// in strict mode we don't allow target to target or source to source connections
const isValid =
- handleToCheck.classList.contains('connectable') &&
+ isConnectable &&
(connectionMode === ConnectionMode.Strict
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId);
if (isValid) {
+ result.endHandle = {
+ nodeId: handleNodeId as string,
+ handleId,
+ type: handleType as HandleType,
+ };
+
result.isValid = isValidConnection(connection);
}
}
diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts
index 58cc5080..f4365d45 100644
--- a/packages/core/src/store/index.ts
+++ b/packages/core/src/store/index.ts
@@ -275,8 +275,8 @@ const createRFStore = () =>
connectionHandleId: initialState.connectionHandleId,
connectionHandleType: initialState.connectionHandleType,
connectionStatus: initialState.connectionStatus,
- connectionTargetNodeId: initialState.connectionTargetNodeId,
- connectionTargetHandleId: initialState.connectionTargetHandleId,
+ connectionStartHandle: initialState.connectionStartHandle,
+ connectionEndHandle: initialState.connectionEndHandle,
}),
reset: () => set({ ...initialState }),
}));
diff --git a/packages/core/src/store/initialState.ts b/packages/core/src/store/initialState.ts
index 2722038d..8c4a5d76 100644
--- a/packages/core/src/store/initialState.ts
+++ b/packages/core/src/store/initialState.ts
@@ -31,8 +31,6 @@ const initialState: ReactFlowStore = {
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
- connectionTargetNodeId: null,
- connectionTargetHandleId: null,
connectionPosition: { x: 0, y: 0 },
connectionStatus: null,
connectionMode: ConnectionMode.Strict,
@@ -57,6 +55,8 @@ const initialState: ReactFlowStore = {
multiSelectionActive: false,
connectionStartHandle: null,
+ connectionEndHandle: null,
+ connectionClickStartHandle: null,
connectOnClick: true,
ariaLiveMessage: '',
diff --git a/packages/core/src/styles/init.css b/packages/core/src/styles/init.css
index 790dcaff..f676e445 100644
--- a/packages/core/src/styles/init.css
+++ b/packages/core/src/styles/init.css
@@ -144,7 +144,7 @@
min-width: 5px;
min-height: 5px;
- &.connectable {
+ &.connectionindicator {
pointer-events: all;
cursor: crosshair;
}
diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts
index fab391d0..aeb33632 100644
--- a/packages/core/src/types/general.ts
+++ b/packages/core/src/types/general.ts
@@ -21,7 +21,7 @@ import type {
NodeOrigin,
} from './nodes';
import type { Edge, EdgeProps, WrapEdgeProps } from './edges';
-import type { HandleType, StartHandle } from './handles';
+import type { HandleType, ConnectingHandle } from './handles';
import type { DefaultEdgeOptions } from '.';
import type { ReactFlowInstance } from './instance';
@@ -167,10 +167,9 @@ export type ReactFlowStore = {
userSelectionActive: boolean;
userSelectionRect: SelectionRect | null;
+ // @todo remove this in next major version in favor of connectionStartHandle
connectionNodeId: string | null;
connectionHandleId: string | null;
- connectionTargetNodeId: string | null;
- connectionTargetHandleId: string | null;
connectionHandleType: HandleType | null;
connectionPosition: XYPosition;
connectionStatus: ConnectionStatus | null;
@@ -188,7 +187,10 @@ export type ReactFlowStore = {
multiSelectionActive: boolean;
- connectionStartHandle: StartHandle | null;
+ connectionStartHandle: ConnectingHandle | null;
+ connectionEndHandle: ConnectingHandle | null;
+ // @todo this is only used for the click connection - we might remove this in the next major version
+ connectionClickStartHandle: ConnectingHandle | null;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
diff --git a/packages/core/src/types/handles.ts b/packages/core/src/types/handles.ts
index 0850526c..7e40d1e5 100644
--- a/packages/core/src/types/handles.ts
+++ b/packages/core/src/types/handles.ts
@@ -2,22 +2,25 @@ import type { XYPosition, Position, Dimensions, OnConnect, Connection } from '.'
export type HandleType = 'source' | 'target';
-export interface HandleElement extends XYPosition, Dimensions {
- id?: string | null;
- position: Position;
-}
+export type HandleElement = XYPosition &
+ Dimensions & {
+ id?: string | null;
+ position: Position;
+ };
-export interface StartHandle {
+export type ConnectingHandle = {
nodeId: string;
type: HandleType;
handleId?: string | null;
-}
+};
-export interface HandleProps {
+export type HandleProps = {
type: HandleType;
position: Position;
isConnectable?: boolean;
+ isConnectableStart?: boolean;
+ isConnectableEnd?: boolean;
onConnect?: OnConnect;
isValidConnection?: (connection: Connection) => boolean;
id?: string;
-}
+};