diff --git a/examples/vite-app/src/examples/Validation/validation.module.css b/examples/vite-app/src/examples/Validation/validation.module.css index 37333aae..63d58eee 100644 --- a/examples/vite-app/src/examples/Validation/validation.module.css +++ b/examples/vite-app/src/examples/Validation/validation.module.css @@ -31,3 +31,11 @@ .validationflow :global .valid { background: #55dd99; } + +.validationflow :global .valid .react-flow__connection-path { + stroke: #55dd99; +} + +.validationflow :global .invalid .react-flow__connection-path { + stroke: #ff6060; +} diff --git a/packages/core/src/components/ConnectionLine/index.tsx b/packages/core/src/components/ConnectionLine/index.tsx index fae0c3b1..45ec7937 100644 --- a/packages/core/src/components/ConnectionLine/index.tsx +++ b/packages/core/src/components/ConnectionLine/index.tsx @@ -1,12 +1,19 @@ import { CSSProperties, useCallback } from 'react'; import { shallow } from 'zustand/shallow'; +import cc from 'classcat'; import { useStore } from '../../hooks/useStore'; import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { internalsSymbol } from '../../utils'; -import type { ConnectionLineComponent, HandleType, ReactFlowState, ReactFlowStore } from '../../types'; +import type { + ConnectionLineComponent, + ConnectionStatus, + HandleType, + ReactFlowState, + ReactFlowStore, +} from '../../types'; import { Position, ConnectionLineType, ConnectionMode } from '../../types'; type ConnectionLineProps = { @@ -15,6 +22,7 @@ type ConnectionLineProps = { type: ConnectionLineType; style?: CSSProperties; CustomComponent?: ConnectionLineComponent; + connectionStatus: ConnectionStatus | null; }; const oppositePosition = { @@ -30,6 +38,7 @@ const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, + connectionStatus, }: ConnectionLineProps) => { const { fromNode, handleId, toX, toY, connectionMode } = useStore( useCallback( @@ -80,6 +89,7 @@ const ConnectionLine = ({ toY={toY} fromPosition={fromPosition} toPosition={toPosition} + connectionStatus={connectionStatus} /> ); } @@ -127,12 +137,13 @@ const selector = (s: ReactFlowState) => ({ nodeId: s.connectionNodeId, handleType: s.connectionHandleType, nodesConnectable: s.nodesConnectable, + connectionStatus: s.connectionStatus, width: s.width, height: s.height, }); function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { - const { nodeId, handleType, nodesConnectable, width, height } = useStore(selector, shallow); + const { nodeId, handleType, nodesConnectable, width, height, connectionStatus } = useStore(selector, shallow); const isValid = !!(nodeId && handleType && width && nodesConnectable); if (!isValid) { @@ -146,8 +157,15 @@ function ConnectionLineWrapper({ containerStyle, style, type, component }: Conne height={height} className="react-flow__edges react-flow__connectionline react-flow__container" > - - + + ); diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts index 5c35e48a..5c54118f 100644 --- a/packages/core/src/components/Handle/handler.ts +++ b/packages/core/src/components/Handle/handler.ts @@ -2,11 +2,12 @@ import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } fro import { StoreApi } from 'zustand'; import { getHostForElement, calcAutoPan, getEventPosition } from '../../utils'; -import type { OnConnect, HandleType, ReactFlowState, Connection } from '../../types'; +import type { OnConnect, HandleType, ReactFlowState, Connection, ConnectionStatus } from '../../types'; import { pointToRendererPoint, rendererPointToPoint } from '../../utils/graph'; import { ConnectionHandle, getClosestHandle, + getConnectionStatus, getHandleLookup, getHandleType, isValidHandle, @@ -91,6 +92,7 @@ export function handlePointerDown({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType, + connectionStatus: null, }); onConnectStart?.(event, { nodeId, handleId, handleType }); @@ -123,7 +125,7 @@ export function handlePointerDown({ setState({ connectionPosition: - prevClosestHandle && isValid + prevClosestHandle && result.isValid ? rendererPointToPoint( { x: prevClosestHandle.x, @@ -132,6 +134,7 @@ export function handlePointerDown({ transform ) : connectionPosition, + connectionStatus: getConnectionStatus(!!prevClosestHandle, result.isValid), }); if (!prevClosestHandle) { diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts index 235abc92..89e58c49 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 } from '../../types'; +import { ConnectionMode, ConnectionStatus } from '../../types'; import { getEventPosition, internalsSymbol } from '../../utils'; import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types'; @@ -167,3 +167,15 @@ export function getHandleType( export function resetRecentHandle(handleDomNode: Element): void { handleDomNode?.classList.remove('valid', 'connecting', 'react-flow__handle-valid', 'react-flow__handle-connecting'); } + +export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { + let connectionStatus = null; + + if (isHandleValid) { + connectionStatus = 'valid'; + } else if (isInsideConnectionRadius && !isHandleValid) { + connectionStatus = 'invalid'; + } + + return connectionStatus as ConnectionStatus; +} diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 8b17111c..c85225db 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -274,6 +274,7 @@ const createRFStore = () => connectionNodeId: initialState.connectionNodeId, connectionHandleId: initialState.connectionHandleId, connectionHandleType: initialState.connectionHandleType, + connectionStatus: initialState.connectionStatus, }), reset: () => set({ ...initialState }), })); diff --git a/packages/core/src/store/initialState.ts b/packages/core/src/store/initialState.ts index afccec2b..99e9d298 100644 --- a/packages/core/src/store/initialState.ts +++ b/packages/core/src/store/initialState.ts @@ -32,6 +32,7 @@ const initialState: ReactFlowStore = { connectionHandleId: null, connectionHandleType: 'source', connectionPosition: { x: 0, y: 0 }, + connectionStatus: null, connectionMode: ConnectionMode.Strict, domNode: null, paneDragging: false, diff --git a/packages/core/src/types/edges.ts b/packages/core/src/types/edges.ts index c2202788..143c7fa6 100644 --- a/packages/core/src/types/edges.ts +++ b/packages/core/src/types/edges.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { CSSProperties, ComponentType, HTMLAttributes, ReactNode, MouseEvent as ReactMouseEvent } from 'react'; -import { Position } from '.'; +import { ConnectionStatus, Position } from '.'; import type { Connection, HandleElement, HandleType, Node } from '.'; type EdgeLabelOptions = { @@ -155,6 +155,7 @@ export type ConnectionLineComponentProps = { toY: number; fromPosition: Position; toPosition: Position; + connectionStatus: ConnectionStatus | null; }; export type ConnectionLineComponent = ComponentType; diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index 9dbd3dce..4abfa405 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -61,6 +61,8 @@ export interface Connection { targetHandle: string | null; } +export type ConnectionStatus = 'valid' | 'invalid'; + export enum ConnectionMode { Strict = 'strict', Loose = 'loose', @@ -166,6 +168,7 @@ export type ReactFlowStore = { connectionHandleId: string | null; connectionHandleType: HandleType | null; connectionPosition: XYPosition; + connectionStatus: ConnectionStatus | null; connectionMode: ConnectionMode; snapToGrid: boolean;