Merge pull request #4383 from xyflow/next

React Flow 12.0.0-next.22 and Svelte Flow 0.1.7
This commit is contained in:
Moritz Klack
2024-06-20 13:45:18 +02:00
committed by GitHub
50 changed files with 1263 additions and 850 deletions
+4 -4
View File
@@ -34,7 +34,7 @@ import Subflow from '../examples/Subflow';
import SwitchFlow from '../examples/Switch'; import SwitchFlow from '../examples/Switch';
import TouchDevice from '../examples/TouchDevice'; import TouchDevice from '../examples/TouchDevice';
import Undirectional from '../examples/Undirectional'; import Undirectional from '../examples/Undirectional';
import UpdatableEdge from '../examples/UpdatableEdge'; import ReconnectEdge from '../examples/ReconnectEdge';
import UpdateNode from '../examples/UpdateNode'; import UpdateNode from '../examples/UpdateNode';
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals'; import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
import UseReactFlow from '../examples/UseReactFlow'; import UseReactFlow from '../examples/UseReactFlow';
@@ -271,9 +271,9 @@ const routes: IRoute[] = [
component: Undirectional, component: Undirectional,
}, },
{ {
name: 'Updatable Edge', name: 'Reconnect Edge',
path: 'updatable-edge', path: 'reconnect-edge',
component: UpdatableEdge, component: ReconnectEdge,
}, },
{ {
name: 'Update Node', name: 'Update Node',
@@ -25,6 +25,7 @@ const defaultNodes: Node[] = [
}, },
{ {
id: '3', id: '3',
type: 'output',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
className: 'light', className: 'light',
@@ -28,6 +28,14 @@ const initialNodes: Node[] = [
data: { label: 'A Node' }, data: { label: 'A Node' },
position: { x: 250, y: 0 }, position: { x: 250, y: 0 },
}, },
{
id: '3',
type: 'b',
sourcePosition: Position.Right,
targetPosition: Position.Left,
data: { label: 'B Node' },
position: { x: 350, y: 0 },
},
]; ];
const buttonStyle: CSSProperties = { const buttonStyle: CSSProperties = {
@@ -78,6 +86,7 @@ const NodeTypeChangeFlow = () => {
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypesObjects[nodeTypesId]} nodeTypes={nodeTypesObjects[nodeTypesId]}
fitView
> >
<button onClick={changeType} style={buttonStyle}> <button onClick={changeType} style={buttonStyle}>
change type change type
@@ -2,7 +2,7 @@ import { useState, useCallback, MouseEvent as ReactMouseEvent } from 'react';
import { import {
ReactFlow, ReactFlow,
Controls, Controls,
updateEdge, reconnectEdge,
addEdge, addEdge,
applyNodeChanges, applyNodeChanges,
applyEdgeChanges, applyEdgeChanges,
@@ -91,21 +91,21 @@ const initialNodes: Node[] = [
]; ];
const initialEdges: Edge[] = [ const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', updatable: 'source' }, { id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', reconnectable: 'source' },
{ id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', updatable: 'target' }, { id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', reconnectable: 'target' },
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' }, { id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
]; ];
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) => const onReconnectStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
console.log(`start update ${handleType} handle`, edge); console.log(`start update ${handleType} handle`, edge);
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => const onReconnectEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
console.log(`end update ${handleType} handle`, edge); console.log(`end update ${handleType} handle`, edge);
const UpdatableEdge = () => { const ReconnectEdge = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => const onReconnect = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els)); const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => { const onNodesChange = useCallback((changes: NodeChange[]) => {
@@ -123,10 +123,10 @@ const UpdatableEdge = () => {
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
snapToGrid={true} snapToGrid={true}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onConnect={onConnect} onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart} onReconnectStart={onReconnectStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onReconnectEnd={onReconnectEnd}
fitView fitView
> >
<Controls /> <Controls />
@@ -134,4 +134,4 @@ const UpdatableEdge = () => {
); );
}; };
export default UpdatableEdge; export default ReconnectEdge;
@@ -10,7 +10,7 @@ import {
Edge, Edge,
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
updateEdge, reconnectEdge,
useNodesState, useNodesState,
useEdgesState, useEdgesState,
} from '@xyflow/react'; } from '@xyflow/react';
@@ -22,55 +22,55 @@ const initialNodes: Node[] = [
id: '00', id: '00',
type: 'custom', type: 'custom',
position: { x: 300, y: 250 }, position: { x: 300, y: 250 },
data: null, data: {},
}, },
{ {
id: '01', id: '01',
type: 'custom', type: 'custom',
position: { x: 100, y: 50 }, position: { x: 100, y: 50 },
data: null, data: {},
}, },
{ {
id: '02', id: '02',
type: 'custom', type: 'custom',
position: { x: 500, y: 50 }, position: { x: 500, y: 50 },
data: null, data: {},
}, },
{ {
id: '03', id: '03',
type: 'custom', type: 'custom',
position: { x: 500, y: 500 }, position: { x: 500, y: 500 },
data: null, data: {},
}, },
{ {
id: '04', id: '04',
type: 'custom', type: 'custom',
position: { x: 100, y: 500 }, position: { x: 100, y: 500 },
data: null, data: {},
}, },
{ {
id: '10', id: '10',
type: 'custom', type: 'custom',
position: { x: 300, y: 5 }, position: { x: 300, y: 5 },
data: null, data: {},
}, },
{ {
id: '20', id: '20',
type: 'custom', type: 'custom',
position: { x: 600, y: 250 }, position: { x: 600, y: 250 },
data: null, data: {},
}, },
{ {
id: '30', id: '30',
type: 'custom', type: 'custom',
position: { x: 300, y: 600 }, position: { x: 300, y: 600 },
data: null, data: {},
}, },
{ {
id: '40', id: '40',
type: 'custom', type: 'custom',
position: { x: 5, y: 250 }, position: { x: 5, y: 250 },
data: null, data: {},
}, },
]; ];
@@ -186,8 +186,8 @@ const UpdateNodeInternalsFlow = () => {
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]); const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
const onEdgeUpdate = useCallback( const onReconnect = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)), (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
[] []
); );
@@ -215,7 +215,7 @@ const UpdateNodeInternalsFlow = () => {
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
connectionLineType={ConnectionLineType.Bezier} connectionLineType={ConnectionLineType.Bezier}
connectionMode={ConnectionMode.Loose} connectionMode={ConnectionMode.Loose}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
/> />
); );
}; };
@@ -1,9 +1,12 @@
import { useKeyPress } from '@xyflow/react'; import { useKeyPress } from '@xyflow/react';
import { useEffect } from 'react';
const UseKeyPressComponent = () => { const UseKeyPressComponent = () => {
const metaPressed = useKeyPress(['Meta']); const metaPressed = useKeyPress(['Meta']);
console.log({ metaPressed }); useEffect(() => {
console.log({ metaPressed });
}, [metaPressed]);
return <div />; return <div />;
}; };
@@ -13,7 +13,7 @@ import {
OnConnectStart, OnConnectStart,
OnConnectEnd, OnConnectEnd,
OnConnect, OnConnect,
updateEdge, reconnectEdge,
Edge, Edge,
IsValidConnection, IsValidConnection,
OnBeforeDelete, OnBeforeDelete,
@@ -24,10 +24,10 @@ import ConnectionStatus from './ConnectionStatus';
import styles from './validation.module.css'; import styles from './validation.module.css';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: null }, { id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: {} },
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: null }, { id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: {} },
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: null }, { id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: {} },
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 }, data: null }, { id: 'C', type: 'customnode', position: { x: 250, y: 300 }, data: {} },
]; ];
const isValidConnection: IsValidConnection = (connection) => connection.target === 'B'; const isValidConnection: IsValidConnection = (connection) => connection.target === 'B';
@@ -41,7 +41,7 @@ const CustomInput: FC<NodeProps> = () => (
const CustomNode: FC<NodeProps> = ({ id }) => ( const CustomNode: FC<NodeProps> = ({ id }) => (
<> <>
<Handle type="target" position={Position.Left} isConnectableStart={false} /> <Handle type="target" position={Position.Top} isConnectableStart={false} />
<div>{id}</div> <div>{id}</div>
<Handle type="source" position={Position.Right} /> <Handle type="source" position={Position.Right} />
</> </>
@@ -81,8 +81,8 @@ const ValidationFlow = () => {
[value] [value]
); );
const onEdgeUpdate = useCallback( const onReconnect = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)), (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
[setEdges] [setEdges]
); );
@@ -102,7 +102,7 @@ const ValidationFlow = () => {
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onConnectStart={onConnectStart} onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
isValidConnection={isValidConnection} isValidConnection={isValidConnection}
onBeforeDelete={onBeforeDelete} onBeforeDelete={onBeforeDelete}
fitView fitView
@@ -149,7 +149,7 @@ export default {
// source: '9', // source: '9',
// target: '11', // target: '11',
// label: 'focusable', // label: 'focusable',
// updatable: true // reconnectable: true
// }, // },
// { // {
// id: 'not-focusable', // id: 'not-focusable',
@@ -146,7 +146,7 @@ export default {
// source: '9', // source: '9',
// target: '11', // target: '11',
// label: 'focusable', // label: 'focusable',
// updatable: true // reconnectable: true
// }, // },
// { // {
// id: 'not-focusable', // id: 'not-focusable',
@@ -24,8 +24,8 @@
data: { label: 'only connectable with B' }, data: { label: 'only connectable with B' },
...nodeDefaults ...nodeDefaults
}, },
{ id: 'A', position: { x: 250, y: 0 }, data: { label: 'A' }, ...nodeDefaults }, { id: 'A', position: { x: 250, y: 0 }, data: { label: 'A' } },
{ id: 'B', position: { x: 250, y: 150 }, data: { label: 'B' }, ...nodeDefaults }, { id: 'B', position: { x: 250, y: 150 }, data: { label: 'B' } },
{ id: 'C', position: { x: 250, y: 300 }, data: { label: 'C' }, ...nodeDefaults } { id: 'C', position: { x: 250, y: 300 }, data: { label: 'C' }, ...nodeDefaults }
]); ]);
+8 -7
View File
@@ -14,10 +14,10 @@
"test:svelte:ui": "pnpm --filter=playwright run test:svelte:ui", "test:svelte:ui": "pnpm --filter=playwright run test:svelte:ui",
"test:react": "pnpm --filter=playwright run test:react", "test:react": "pnpm --filter=playwright run test:react",
"test:react:ui": "pnpm --filter=playwright run test:react:ui", "test:react:ui": "pnpm --filter=playwright run test:react:ui",
"build": "turbo run build", "build:all": "turbo run build",
"test": "turbo run test", "build": "turbo run build --filter=./packages/**",
"lint": "turbo run lint", "lint": "turbo run lint --filter=./packages/**",
"typecheck": "turbo run typecheck", "typecheck": "turbo run typecheck --filter=./packages/**",
"release": "changeset publish", "release": "changeset publish",
"clean": "pnpm -r --parallel exec rimraf dist .turbo node_modules" "clean": "pnpm -r --parallel exec rimraf dist .turbo node_modules"
}, },
@@ -35,8 +35,9 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^3.23.0", "rollup": "^4.18.0",
"turbo": "^1.10.0", "turbo": "^2.0.3",
"typescript": "5.1.3" "typescript": "5.1.3"
} },
"packageManager": "pnpm@9.2.0"
} }
+7
View File
@@ -1,5 +1,12 @@
# @xyflow/react # @xyflow/react
## 12.0.0-next.22
- ⚠️ rename `updateEdge` to `reconnectEdge` and realted APIs [#4373](https://github.com/xyflow/xyflow/pull/4373)
- revise selection usability (capture while dragging out of the flow)
- use correct end handle position when drawing a connection lines
- determine correct end positions for connection lines
## 12.0.0-next.21 ## 12.0.0-next.21
- fix node origin bug - fix node origin bug
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.0.0-next.21", "version": "12.0.0-next.22",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [ "keywords": [
"react", "react",
@@ -39,7 +39,7 @@ const ConnectionLine = ({
CustomComponent, CustomComponent,
connectionStatus, connectionStatus,
}: ConnectionLineProps) => { }: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore( const { fromNode, handleId, toX, toY, connectionMode, endPosition, isValid } = useStore(
useCallback( useCallback(
(s: ReactFlowStore) => ({ (s: ReactFlowStore) => ({
fromNode: s.nodeLookup.get(nodeId), fromNode: s.nodeLookup.get(nodeId),
@@ -47,11 +47,14 @@ const ConnectionLine = ({
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
endPosition: s.connectionEndHandle?.position,
isValid: s.connectionStatus === 'valid',
}), }),
[nodeId] [nodeId]
), ),
shallow shallow
); );
const fromHandleBounds = fromNode?.internals.handleBounds; const fromHandleBounds = fromNode?.internals.handleBounds;
let handleBounds = fromHandleBounds?.[handleType]; let handleBounds = fromHandleBounds?.[handleType];
@@ -69,7 +72,7 @@ const ConnectionLine = ({
const fromX = fromNode.internals.positionAbsolute.x + fromHandleX; const fromX = fromNode.internals.positionAbsolute.x + fromHandleX;
const fromY = fromNode.internals.positionAbsolute.y + fromHandleY; const fromY = fromNode.internals.positionAbsolute.y + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null; const toPosition = isValid && endPosition ? endPosition : fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition || !toPosition) { if (!fromPosition || !toPosition) {
return null; return null;
@@ -1,4 +1,4 @@
// Updatable edges have a anchors around their handles to update the edge. // Reconnectable edges have a anchors around their handles to reconnect the edge.
import { XYHandle, type Connection, EdgePosition } from '@xyflow/system'; import { XYHandle, type Connection, EdgePosition } from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor'; import { EdgeAnchor } from '../Edges/EdgeAnchor';
@@ -7,20 +7,20 @@ import { useStoreApi } from '../../hooks/useStore';
type EdgeUpdateAnchorsProps<EdgeType extends Edge = Edge> = { type EdgeUpdateAnchorsProps<EdgeType extends Edge = Edge> = {
edge: EdgeType; edge: EdgeType;
isUpdatable: boolean | 'source' | 'target'; isReconnectable: boolean | 'source' | 'target';
edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius']; reconnectRadius: EdgeWrapperProps['reconnectRadius'];
sourceHandleId: Edge['sourceHandle']; sourceHandleId: Edge['sourceHandle'];
targetHandleId: Edge['targetHandle']; targetHandleId: Edge['targetHandle'];
onEdgeUpdate: EdgeWrapperProps<EdgeType>['onEdgeUpdate']; onReconnect: EdgeWrapperProps<EdgeType>['onReconnect'];
onEdgeUpdateStart: EdgeWrapperProps<EdgeType>['onEdgeUpdateStart']; onReconnectStart: EdgeWrapperProps<EdgeType>['onReconnectStart'];
onEdgeUpdateEnd: EdgeWrapperProps<EdgeType>['onEdgeUpdateEnd']; onReconnectEnd: EdgeWrapperProps<EdgeType>['onReconnectEnd'];
setUpdateHover: (hover: boolean) => void; setUpdateHover: (hover: boolean) => void;
setUpdating: (updating: boolean) => void; setReconnecting: (updating: boolean) => void;
} & EdgePosition; } & EdgePosition;
export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
isUpdatable, isReconnectable,
edgeUpdaterRadius, reconnectRadius,
edge, edge,
targetHandleId, targetHandleId,
sourceHandleId, sourceHandleId,
@@ -30,10 +30,10 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
targetY, targetY,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
onEdgeUpdate, onReconnect,
onEdgeUpdateStart, onReconnectStart,
onEdgeUpdateEnd, onReconnectEnd,
setUpdating, setReconnecting,
setUpdateHover, setUpdateHover,
}: EdgeUpdateAnchorsProps<EdgeType>) { }: EdgeUpdateAnchorsProps<EdgeType>) {
const store = useStoreApi(); const store = useStoreApi();
@@ -65,15 +65,15 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
const isTarget = isSourceHandle; const isTarget = isSourceHandle;
setUpdating(true); setReconnecting(true);
onEdgeUpdateStart?.(event, edge, handleType); onReconnectStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => { const _onReconnectEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false); setReconnecting(false);
onEdgeUpdateEnd?.(evt, edge, handleType); onReconnectEnd?.(evt, edge, handleType);
}; };
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, { XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect, autoPanOnConnect,
@@ -93,43 +93,43 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
onConnect: onConnectEdge, onConnect: onConnectEdge,
onConnectStart, onConnectStart,
onConnectEnd, onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd, onReconnectEnd: _onReconnectEnd,
updateConnection, updateConnection,
getTransform: () => store.getState().transform, getTransform: () => store.getState().transform,
getConnectionStartHandle: () => store.getState().connectionStartHandle, getConnectionStartHandle: () => store.getState().connectionStartHandle,
}); });
}; };
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => const onReconnectSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true); handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => const onReconnectTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false); handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true); const onReconnectMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false); const onReconnectMouseOut = () => setUpdateHover(false);
return ( return (
<> <>
{(isUpdatable === 'source' || isUpdatable === true) && ( {(isReconnectable === 'source' || isReconnectable === true) && (
<EdgeAnchor <EdgeAnchor
position={sourcePosition} position={sourcePosition}
centerX={sourceX} centerX={sourceX}
centerY={sourceY} centerY={sourceY}
radius={edgeUpdaterRadius} radius={reconnectRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown} onMouseDown={onReconnectSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onReconnectMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut} onMouseOut={onReconnectMouseOut}
type="source" type="source"
/> />
)} )}
{(isUpdatable === 'target' || isUpdatable === true) && ( {(isReconnectable === 'target' || isReconnectable === true) && (
<EdgeAnchor <EdgeAnchor
position={targetPosition} position={targetPosition}
centerX={targetX} centerX={targetX}
centerY={targetY} centerY={targetY}
radius={edgeUpdaterRadius} radius={reconnectRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown} onMouseDown={onReconnectTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onReconnectMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut} onMouseOut={onReconnectMouseOut}
type="target" type="target"
/> />
)} )}
@@ -18,7 +18,7 @@ import type { Edge, EdgeWrapperProps } from '../../types';
export function EdgeWrapper<EdgeType extends Edge = Edge>({ export function EdgeWrapper<EdgeType extends Edge = Edge>({
id, id,
edgesFocusable, edgesFocusable,
edgesUpdatable, edgesReconnectable,
elementsSelectable, elementsSelectable,
onClick, onClick,
onDoubleClick, onDoubleClick,
@@ -26,10 +26,10 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
onMouseEnter, onMouseEnter,
onMouseMove, onMouseMove,
onMouseLeave, onMouseLeave,
edgeUpdaterRadius, reconnectRadius,
onEdgeUpdate, onReconnect,
onEdgeUpdateStart, onReconnectStart,
onEdgeUpdateEnd, onReconnectEnd,
rfId, rfId,
edgeTypes, edgeTypes,
noPanClassName, noPanClassName,
@@ -50,14 +50,14 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
} }
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable = const isReconnectable =
typeof onEdgeUpdate !== 'undefined' && typeof onReconnect !== 'undefined' &&
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined')); (edge.reconnectable || (edgesReconnectable && typeof edge.reconnectable === 'undefined'));
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined')); const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
const edgeRef = useRef<SVGGElement>(null); const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false); const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false); const [reconnecting, setReconnecting] = useState<boolean>(false);
const store = useStoreApi(); const store = useStoreApi();
const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore( const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(
@@ -207,7 +207,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined} aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef} ref={edgeRef}
> >
{!updating && ( {!reconnecting && (
<EdgeComponent <EdgeComponent
id={id} id={id}
source={edge.source} source={edge.source}
@@ -239,14 +239,14 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
interactionWidth={edge.interactionWidth} interactionWidth={edge.interactionWidth}
/> />
)} )}
{isUpdatable && ( {isReconnectable && (
<EdgeUpdateAnchors<EdgeType> <EdgeUpdateAnchors<EdgeType>
edge={edge} edge={edge}
isUpdatable={isUpdatable} isReconnectable={isReconnectable}
edgeUpdaterRadius={edgeUpdaterRadius} reconnectRadius={reconnectRadius}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onEdgeUpdateStart={onEdgeUpdateStart} onReconnectStart={onReconnectStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onReconnectEnd={onReconnectEnd}
sourceX={sourceX} sourceX={sourceX}
sourceY={sourceY} sourceY={sourceY}
targetX={targetX} targetX={targetX}
@@ -254,7 +254,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
sourcePosition={sourcePosition} sourcePosition={sourcePosition}
targetPosition={targetPosition} targetPosition={targetPosition}
setUpdateHover={setUpdateHover} setUpdateHover={setUpdateHover}
setUpdating={setUpdating} setReconnecting={setReconnecting}
sourceHandleId={edge.sourceHandle} sourceHandleId={edge.sourceHandle}
targetHandleId={edge.targetHandle} targetHandleId={edge.targetHandle}
/> />
@@ -26,7 +26,7 @@ const reactFlowFieldsToTrack = [
'nodesConnectable', 'nodesConnectable',
'nodesFocusable', 'nodesFocusable',
'edgesFocusable', 'edgesFocusable',
'edgesUpdatable', 'edgesReconnectable',
'elevateNodesOnSelect', 'elevateNodesOnSelect',
'elevateEdgesOnSelect', 'elevateEdgesOnSelect',
'minZoom', 'minZoom',
@@ -14,14 +14,14 @@ type EdgeRendererProps<EdgeType extends Edge = Edge> = Pick<
| 'onEdgeDoubleClick' | 'onEdgeDoubleClick'
| 'defaultMarkerColor' | 'defaultMarkerColor'
| 'onlyRenderVisibleElements' | 'onlyRenderVisibleElements'
| 'onEdgeUpdate' | 'onReconnect'
| 'onEdgeContextMenu' | 'onEdgeContextMenu'
| 'onEdgeMouseEnter' | 'onEdgeMouseEnter'
| 'onEdgeMouseMove' | 'onEdgeMouseMove'
| 'onEdgeMouseLeave' | 'onEdgeMouseLeave'
| 'onEdgeUpdateStart' | 'onReconnectStart'
| 'onEdgeUpdateEnd' | 'onReconnectEnd'
| 'edgeUpdaterRadius' | 'reconnectRadius'
| 'noPanClassName' | 'noPanClassName'
| 'rfId' | 'rfId'
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
@@ -34,7 +34,7 @@ const selector = (s: ReactFlowState) => ({
width: s.width, width: s.width,
height: s.height, height: s.height,
edgesFocusable: s.edgesFocusable, edgesFocusable: s.edgesFocusable,
edgesUpdatable: s.edgesUpdatable, edgesReconnectable: s.edgesReconnectable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
onError: s.onError, onError: s.onError,
@@ -46,19 +46,19 @@ function EdgeRendererComponent<EdgeType extends Edge = Edge>({
rfId, rfId,
edgeTypes, edgeTypes,
noPanClassName, noPanClassName,
onEdgeUpdate, onReconnect,
onEdgeContextMenu, onEdgeContextMenu,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseMove, onEdgeMouseMove,
onEdgeMouseLeave, onEdgeMouseLeave,
onEdgeClick, onEdgeClick,
edgeUpdaterRadius, reconnectRadius,
onEdgeDoubleClick, onEdgeDoubleClick,
onEdgeUpdateStart, onReconnectStart,
onEdgeUpdateEnd, onReconnectEnd,
disableKeyboardA11y, disableKeyboardA11y,
}: EdgeRendererProps<EdgeType>) { }: EdgeRendererProps<EdgeType>) {
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const { edgesFocusable, edgesReconnectable, elementsSelectable, onError } = useStore(selector, shallow);
const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements); const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
return ( return (
@@ -71,19 +71,19 @@ function EdgeRendererComponent<EdgeType extends Edge = Edge>({
key={id} key={id}
id={id} id={id}
edgesFocusable={edgesFocusable} edgesFocusable={edgesFocusable}
edgesUpdatable={edgesUpdatable} edgesReconnectable={edgesReconnectable}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onContextMenu={onEdgeContextMenu} onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter} onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove} onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave} onMouseLeave={onEdgeMouseLeave}
onClick={onEdgeClick} onClick={onEdgeClick}
edgeUpdaterRadius={edgeUpdaterRadius} reconnectRadius={reconnectRadius}
onDoubleClick={onEdgeDoubleClick} onDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={onEdgeUpdateStart} onReconnectStart={onReconnectStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onReconnectEnd={onReconnectEnd}
rfId={rfId} rfId={rfId}
onError={onError} onError={onError}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
@@ -70,8 +70,8 @@ function FlowRendererComponent<NodeType extends Node = Node>({
isControlledViewport, isControlledViewport,
}: FlowRendererProps<NodeType>) { }: FlowRendererProps<NodeType>) {
const { nodesSelectionActive, userSelectionActive } = useStore(selector); const { nodesSelectionActive, userSelectionActive } = useStore(selector);
const selectionKeyPressed = useKeyPress(selectionKeyCode); const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: window });
const panActivationKeyPressed = useKeyPress(panActivationKeyCode); const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: window });
const panOnDrag = panActivationKeyPressed || _panOnDrag; const panOnDrag = panActivationKeyPressed || _panOnDrag;
const panOnScroll = panActivationKeyPressed || _panOnScroll; const panOnScroll = panActivationKeyPressed || _panOnScroll;
@@ -113,6 +113,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
panOnDrag={panOnDrag} panOnDrag={panOnDrag}
isSelecting={!!isSelecting} isSelecting={!!isSelecting}
selectionMode={selectionMode} selectionMode={selectionMode}
selectionKeyPressed={selectionKeyPressed}
> >
{children} {children}
{nodesSelectionActive && ( {nodesSelectionActive && (
@@ -85,14 +85,14 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
onPaneMouseLeave, onPaneMouseLeave,
onPaneScroll, onPaneScroll,
onPaneContextMenu, onPaneContextMenu,
onEdgeUpdate,
onEdgeContextMenu, onEdgeContextMenu,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseMove, onEdgeMouseMove,
onEdgeMouseLeave, onEdgeMouseLeave,
edgeUpdaterRadius, reconnectRadius,
onEdgeUpdateStart, onReconnect,
onEdgeUpdateEnd, onReconnectStart,
onReconnectEnd,
noDragClassName, noDragClassName,
noWheelClassName, noWheelClassName,
noPanClassName, noPanClassName,
@@ -153,15 +153,15 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick} onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onlyRenderVisibleElements={onlyRenderVisibleElements} onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu} onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter} onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart} reconnectRadius={reconnectRadius}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor} defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
+35 -19
View File
@@ -2,7 +2,12 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift * The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/ */
import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react'; import {
useRef,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type ReactNode,
} from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system'; import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system';
@@ -15,6 +20,7 @@ import type { ReactFlowProps, ReactFlowState } from '../../types';
type PaneProps = { type PaneProps = {
isSelecting: boolean; isSelecting: boolean;
selectionKeyPressed: boolean;
children: ReactNode; children: ReactNode;
} & Partial< } & Partial<
Pick< Pick<
@@ -52,6 +58,7 @@ const selector = (s: ReactFlowState) => ({
export function Pane({ export function Pane({
isSelecting, isSelecting,
selectionKeyPressed,
selectionMode = SelectionMode.Full, selectionMode = SelectionMode.Full,
panOnDrag, panOnDrag,
onSelectionStart, onSelectionStart,
@@ -72,6 +79,10 @@ export function Pane({
const edgeIdLookup = useRef<Map<string, Set<string>>>(new Map()); const edgeIdLookup = useRef<Map<string, Set<string>>>(new Map());
const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow); const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
// Used to prevent click events when the user lets go of the selectionKey during a selection
const selectionInProgress = useRef<boolean>(false);
const resetUserSelection = () => { const resetUserSelection = () => {
store.setState({ userSelectionActive: false, userSelectionRect: null }); store.setState({ userSelectionActive: false, userSelectionRect: null });
@@ -81,6 +92,12 @@ export function Pane({
}; };
const onClick = (event: ReactMouseEvent) => { const onClick = (event: ReactMouseEvent) => {
// We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress.current) {
selectionInProgress.current = false;
return;
}
onPaneClick?.(event); onPaneClick?.(event);
store.getState().resetSelectedElements(); store.getState().resetSelectedElements();
store.setState({ nodesSelectionActive: false }); store.setState({ nodesSelectionActive: false });
@@ -97,9 +114,10 @@ export function Pane({
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined; const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
const onMouseDown = (event: ReactMouseEvent): void => { const onPointerDown = (event: ReactPointerEvent): void => {
const { resetSelectedElements, domNode, edgeLookup } = store.getState(); const { resetSelectedElements, domNode, edgeLookup } = store.getState();
containerBounds.current = domNode?.getBoundingClientRect(); containerBounds.current = domNode?.getBoundingClientRect();
container.current?.setPointerCapture(event.pointerId);
if ( if (
!elementsSelectable || !elementsSelectable ||
@@ -136,14 +154,16 @@ export function Pane({
onSelectionStart?.(event); onSelectionStart?.(event);
}; };
const onMouseMove = (event: ReactMouseEvent): void => { const onPointerMove = (event: ReactPointerEvent): void => {
const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
store.getState(); store.getState();
if (!isSelecting || !containerBounds.current || !userSelectionRect) { if (!containerBounds.current || !userSelectionRect) {
return; return;
} }
selectionInProgress.current = true;
const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current); const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current);
const { startX, startY } = userSelectionRect; const { startX, startY } = userSelectionRect;
@@ -199,10 +219,11 @@ export function Pane({
}); });
}; };
const onMouseUp = (event: ReactMouseEvent) => { const onPointerUp = (event: ReactPointerEvent) => {
if (event.button !== 0) { if (event.button !== 0) {
return; return;
} }
container.current?.releasePointerCapture(event.pointerId);
const { userSelectionRect } = store.getState(); const { userSelectionRect } = store.getState();
// We only want to trigger click functions when in selection mode if // We only want to trigger click functions when in selection mode if
// the user did not move the mouse. // the user did not move the mouse.
@@ -214,30 +235,25 @@ export function Pane({
resetUserSelection(); resetUserSelection();
onSelectionEnd?.(event); onSelectionEnd?.(event);
};
const onMouseLeave = (event: ReactMouseEvent) => { // If the user kept holding the selectionKey during the selection,
if (userSelectionActive) { // we need to reset the selectionInProgress, so the next click event is not prevented
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); if (selectionKeyPressed) {
onSelectionEnd?.(event); selectionInProgress.current = false;
} }
resetUserSelection();
}; };
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
return ( return (
<div <div
className={cc(['react-flow__pane', { draggable: panOnDrag, dragging, selection: isSelecting }])} className={cc(['react-flow__pane', { draggable: panOnDrag, dragging, selection: isSelecting }])}
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)} onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onContextMenu={wrapHandler(onContextMenu, container)} onContextMenu={wrapHandler(onContextMenu, container)}
onWheel={wrapHandler(onWheel, container)} onWheel={wrapHandler(onWheel, container)}
onMouseEnter={hasActiveSelection ? undefined : onPaneMouseEnter} onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
onMouseDown={hasActiveSelection ? onMouseDown : undefined} onPointerDown={hasActiveSelection ? onPointerDown : onPaneMouseMove}
onMouseMove={hasActiveSelection ? onMouseMove : onPaneMouseMove} onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
onMouseUp={hasActiveSelection ? onMouseUp : undefined} onPointerUp={hasActiveSelection ? onPointerUp : undefined}
onMouseLeave={hasActiveSelection ? onMouseLeave : onPaneMouseLeave} onPointerLeave={onPaneMouseLeave}
ref={container} ref={container}
style={containerStyle} style={containerStyle}
> >
@@ -81,7 +81,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
nodesFocusable, nodesFocusable,
nodeOrigin = defaultNodeOrigin, nodeOrigin = defaultNodeOrigin,
edgesFocusable, edgesFocusable,
edgesUpdatable, edgesReconnectable,
elementsSelectable = true, elementsSelectable = true,
defaultViewport = initViewport, defaultViewport = initViewport,
minZoom = 0.5, minZoom = 0.5,
@@ -104,15 +104,15 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onPaneScroll, onPaneScroll,
onPaneContextMenu, onPaneContextMenu,
children, children,
onEdgeUpdate, onReconnect,
onReconnectStart,
onReconnectEnd,
onEdgeContextMenu, onEdgeContextMenu,
onEdgeDoubleClick, onEdgeDoubleClick,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseMove, onEdgeMouseMove,
onEdgeMouseLeave, onEdgeMouseLeave,
onEdgeUpdateStart, reconnectRadius = 10,
onEdgeUpdateEnd,
edgeUpdaterRadius = 10,
onNodesChange, onNodesChange,
onEdgesChange, onEdgesChange,
noDragClassName = 'nodrag', noDragClassName = 'nodrag',
@@ -202,15 +202,15 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onSelectionContextMenu={onSelectionContextMenu} onSelectionContextMenu={onSelectionContextMenu}
onSelectionStart={onSelectionStart} onSelectionStart={onSelectionStart}
onSelectionEnd={onSelectionEnd} onSelectionEnd={onSelectionEnd}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onEdgeContextMenu={onEdgeContextMenu} onEdgeContextMenu={onEdgeContextMenu}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeMouseEnter={onEdgeMouseEnter} onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart} reconnectRadius={reconnectRadius}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor} defaultMarkerColor={defaultMarkerColor}
noDragClassName={noDragClassName} noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName} noWheelClassName={noWheelClassName}
@@ -236,7 +236,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
nodesConnectable={nodesConnectable} nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable} nodesFocusable={nodesFocusable}
edgesFocusable={edgesFocusable} edgesFocusable={edgesFocusable}
edgesUpdatable={edgesUpdatable} edgesReconnectable={edgesReconnectable}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
elevateNodesOnSelect={elevateNodesOnSelect} elevateNodesOnSelect={elevateNodesOnSelect}
elevateEdgesOnSelect={elevateEdgesOnSelect} elevateEdgesOnSelect={elevateEdgesOnSelect}
@@ -26,7 +26,7 @@ export function useGlobalKeyHandler({
const { deleteElements } = useReactFlow(); const { deleteElements } = useReactFlow();
const deleteKeyPressed = useKeyPress(deleteKeyCode, deleteKeyOptions); const deleteKeyPressed = useKeyPress(deleteKeyCode, deleteKeyOptions);
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode, { target: window });
useEffect(() => { useEffect(() => {
if (deleteKeyPressed) { if (deleteKeyPressed) {
+1 -1
View File
@@ -116,6 +116,6 @@ export {
getIncomers, getIncomers,
getOutgoers, getOutgoers,
addEdge, addEdge,
updateEdge, reconnectEdge,
getConnectedEdges, getConnectedEdges,
} from '@xyflow/system'; } from '@xyflow/system';
+1 -1
View File
@@ -92,7 +92,7 @@ const getInitialState = ({
nodesConnectable: true, nodesConnectable: true,
nodesFocusable: true, nodesFocusable: true,
edgesFocusable: true, edgesFocusable: true,
edgesUpdatable: true, edgesReconnectable: true,
elementsSelectable: true, elementsSelectable: true,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
elevateEdgesOnSelect: false, elevateEdgesOnSelect: false,
+6 -6
View File
@@ -29,7 +29,7 @@ import type {
Node, Node,
Edge, Edge,
ConnectionLineComponent, ConnectionLineComponent,
OnEdgeUpdateFunc, OnReconnect,
OnInit, OnInit,
DefaultEdgeOptions, DefaultEdgeOptions,
FitViewOptions, FitViewOptions,
@@ -129,9 +129,9 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onEdgeMouseLeave?: EdgeMouseHandler<EdgeType>; onEdgeMouseLeave?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when a user double clicks on an edge */ /** This event handler is called when a user double clicks on an edge */
onEdgeDoubleClick?: EdgeMouseHandler<EdgeType>; onEdgeDoubleClick?: EdgeMouseHandler<EdgeType>;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; onReconnect?: OnReconnect<EdgeType>;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onEdgeUpdate?: OnEdgeUpdateFunc<EdgeType>; onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
/** This event handler is called when a Node is updated /** This event handler is called when a Node is updated
* @example // Use NodesState hook to create edges and get onNodesChange handler * @example // Use NodesState hook to create edges and get onNodesChange handler
* import ReactFlow, { useNodesState } from '@xyflow/react'; * import ReactFlow, { useNodesState } from '@xyflow/react';
@@ -330,7 +330,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
/** Controls if all edges should be updateable /** Controls if all edges should be updateable
* @default true * @default true
*/ */
edgesUpdatable?: boolean; edgesReconnectable?: boolean;
/** Controls if all elements should (nodes & edges) be selectable /** Controls if all elements should (nodes & edges) be selectable
* @default true * @default true
*/ */
@@ -413,7 +413,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
panOnScrollMode?: PanOnScrollMode; panOnScrollMode?: PanOnScrollMode;
/** Controls if the viewport should zoom by double clicking somewhere on the flow */ /** Controls if the viewport should zoom by double clicking somewhere on the flow */
zoomOnDoubleClick?: boolean; zoomOnDoubleClick?: boolean;
edgeUpdaterRadius?: number; reconnectRadius?: number;
noDragClassName?: string; noDragClassName?: string;
noWheelClassName?: string; noWheelClassName?: string;
noPanClassName?: string; noPanClassName?: string;
+7 -9
View File
@@ -28,8 +28,6 @@ export type EdgeLabelOptions = {
labelBgBorderRadius?: number; labelBgBorderRadius?: number;
}; };
export type EdgeUpdatable = boolean | HandleType;
/** /**
* The Edge type is mainly used for the `edges` that get passed to the ReactFlow component * The Edge type is mainly used for the `edges` that get passed to the ReactFlow component
* @public * @public
@@ -41,7 +39,7 @@ export type Edge<
EdgeLabelOptions & { EdgeLabelOptions & {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
updatable?: EdgeUpdatable; reconnectable?: boolean | HandleType;
focusable?: boolean; focusable?: boolean;
}; };
@@ -69,19 +67,19 @@ export type EdgeMouseHandler<EdgeType extends Edge = Edge> = (event: ReactMouseE
export type EdgeWrapperProps<EdgeType extends Edge = Edge> = { export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
id: string; id: string;
edgesFocusable: boolean; edgesFocusable: boolean;
edgesUpdatable: boolean; edgesReconnectable: boolean;
elementsSelectable: boolean; elementsSelectable: boolean;
noPanClassName: string; noPanClassName: string;
onClick?: EdgeMouseHandler<EdgeType>; onClick?: EdgeMouseHandler<EdgeType>;
onDoubleClick?: EdgeMouseHandler<EdgeType>; onDoubleClick?: EdgeMouseHandler<EdgeType>;
onEdgeUpdate?: OnEdgeUpdateFunc<EdgeType>; onReconnect?: OnReconnect<EdgeType>;
onContextMenu?: EdgeMouseHandler<EdgeType>; onContextMenu?: EdgeMouseHandler<EdgeType>;
onMouseEnter?: EdgeMouseHandler<EdgeType>; onMouseEnter?: EdgeMouseHandler<EdgeType>;
onMouseMove?: EdgeMouseHandler<EdgeType>; onMouseMove?: EdgeMouseHandler<EdgeType>;
onMouseLeave?: EdgeMouseHandler<EdgeType>; onMouseLeave?: EdgeMouseHandler<EdgeType>;
edgeUpdaterRadius?: number; reconnectRadius?: number;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
rfId?: string; rfId?: string;
edgeTypes?: EdgeTypes; edgeTypes?: EdgeTypes;
onError?: OnError; onError?: OnError;
@@ -191,7 +189,7 @@ export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'tar
*/ */
export type SimpleBezierEdgeProps = EdgeComponentProps; export type SimpleBezierEdgeProps = EdgeComponentProps;
export type OnEdgeUpdateFunc<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void; export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void;
export type ConnectionLineComponentProps = { export type ConnectionLineComponentProps = {
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
+1 -1
View File
@@ -89,7 +89,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
nodesConnectable: boolean; nodesConnectable: boolean;
nodesFocusable: boolean; nodesFocusable: boolean;
edgesFocusable: boolean; edgesFocusable: boolean;
edgesUpdatable: boolean; edgesReconnectable: boolean;
elementsSelectable: boolean; elementsSelectable: boolean;
elevateNodesOnSelect: boolean; elevateNodesOnSelect: boolean;
elevateEdgesOnSelect: boolean; elevateEdgesOnSelect: boolean;
+7
View File
@@ -1,5 +1,12 @@
# @xyflow/svelte # @xyflow/svelte
## 0.1.7
- revise selection usability (capture while dragging out of the flow)
- only prevent shift scrolling when selection is actually in progress
- use correct end handle position when drawing a connection lines
- determine correct end positions for connection lines
## 0.1.6 ## 0.1.6
- fix node origin bug - fix node origin bug
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "0.1.6", "version": "0.1.7",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [ "keywords": [
"svelte", "svelte",
@@ -18,8 +18,8 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"css": "postcss src/styles/{base,style}.css --config ./../../tooling/postcss-config --dir dist", "css": "postcss src/styles/{base,style}.css --config ./../../tooling/postcss-config --dir dist",
"css-watch": "pnpm css --watch", "css-watch": "pnpm css --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --check . && eslint ./src",
"format": "prettier --plugin-search-dir . --write .", "format": "prettier --write .",
"typecheck": "pnpm check" "typecheck": "pnpm check"
}, },
"type": "module", "type": "module",
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { NodeProps } from '$lib/types'; import type { NodeProps } from '$lib/types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface $$Props extends NodeProps {} interface $$Props extends NodeProps {}
// this is a workaround for suppressing the warning about unused props // this is a workaround for suppressing the warning about unused props
@@ -75,15 +75,24 @@
$selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true); $selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true);
$: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user'); $: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user');
function onClick(event: MouseEvent | TouchEvent) { // Used to prevent click events when the user lets go of the selectionKey during a selection
dispatch('paneclick', { event }); let selectionInProgress = false;
function onClick(event: MouseEvent | TouchEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress) {
selectionInProgress = false;
return;
}
dispatch('paneclick', { event });
unselectNodesAndEdges(); unselectNodesAndEdges();
selectionRectMode.set(null); selectionRectMode.set(null);
} }
function onMouseDown(event: MouseEvent) { function onPointerDown(event: PointerEvent) {
containerBounds = container.getBoundingClientRect(); containerBounds = container.getBoundingClientRect();
container.setPointerCapture(event.pointerId);
if ( if (
!elementsSelectable || !elementsSelectable ||
@@ -111,10 +120,13 @@
// onSelectionStart?.(event); // onSelectionStart?.(event);
} }
function onMouseMove(event: MouseEvent) { function onPointerMove(event: PointerEvent) {
if (!isSelecting || !containerBounds || !$selectionRect) { if (!isSelecting || !containerBounds || !$selectionRect) {
return; return;
} }
selectionInProgress = true;
const mousePos = getEventPosition(event, containerBounds); const mousePos = getEventPosition(event, containerBounds);
const startX = $selectionRect.startX ?? 0; const startX = $selectionRect.startX ?? 0;
const startY = $selectionRect.startY ?? 0; const startY = $selectionRect.startY ?? 0;
@@ -157,11 +169,13 @@
selectionRect.set(nextUserSelectRect); selectionRect.set(nextUserSelectRect);
} }
function onMouseUp(event: MouseEvent) { function onPointerUp(event: PointerEvent) {
if (event.button !== 0) { if (event.button !== 0) {
return; return;
} }
container.releasePointerCapture(event.pointerId);
// We only want to trigger click functions when in selection mode if // We only want to trigger click functions when in selection mode if
// the user did not move the mouse. // the user did not move the mouse.
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) { if (!isSelecting && $selectionRectMode === 'user' && event.target === container) {
@@ -173,17 +187,14 @@
$selectionRectMode = 'nodes'; $selectionRectMode = 'nodes';
} }
// onSelectionEnd?.(event); // If the user kept holding the selectionKey during the selection,
} // we need to reset the selectionInProgress, so the next click event is not prevented
if ($selectionKeyPressed) {
const onMouseLeave = () => { selectionInProgress = false;
if ($selectionRectMode === 'user') {
selectionRectMode.set(selectedNodes.length > 0 ? 'nodes' : null);
// onSelectionEnd?.(event);
} }
selectionRect.set(null); // onSelectionEnd?.(event);
}; }
const onContextMenu = (event: MouseEvent) => { const onContextMenu = (event: MouseEvent) => {
if (Array.isArray(_panOnDrag) && _panOnDrag?.includes(2)) { if (Array.isArray(_panOnDrag) && _panOnDrag?.includes(2)) {
@@ -204,10 +215,9 @@
class:dragging={$dragging} class:dragging={$dragging}
class:selection={isSelecting} class:selection={isSelecting}
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)} on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
on:mousedown={hasActiveSelection ? onMouseDown : undefined} on:pointerdown={hasActiveSelection ? onPointerDown : undefined}
on:mousemove={hasActiveSelection ? onMouseMove : undefined} on:pointermove={hasActiveSelection ? onPointerMove : undefined}
on:mouseup={hasActiveSelection ? onMouseUp : undefined} on:pointerup={hasActiveSelection ? onPointerUp : undefined}
on:mouseleave={hasActiveSelection ? onMouseLeave : undefined}
on:contextmenu={wrapHandler(onContextMenu, container)} on:contextmenu={wrapHandler(onContextMenu, container)}
> >
<slot /> <slot />
@@ -23,7 +23,7 @@
const { const {
viewport, viewport,
panZoom, panZoom,
selectionKeyPressed, selectionRect,
minZoom, minZoom,
maxZoom, maxZoom,
dragging, dragging,
@@ -66,7 +66,7 @@
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true, preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
noPanClassName: 'nopan', noPanClassName: 'nopan',
noWheelClassName: 'nowheel', noWheelClassName: 'nowheel',
userSelectionActive: $selectionKeyPressed, userSelectionActive: !!$selectionRect,
translateExtent: $translateExtent, translateExtent: $translateExtent,
lib: $lib lib: $lib
}} }}
+1 -2
View File
@@ -124,6 +124,5 @@ export {
getIncomers, getIncomers,
getOutgoers, getOutgoers,
getConnectedEdges, getConnectedEdges,
addEdge, addEdge
updateEdge
} from '@xyflow/system'; } from '@xyflow/system';
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { getNodesBounds, Position, type Rect, getNodeToolbarTransform } from '@xyflow/system'; import { getNodesBounds, Position, getNodeToolbarTransform } from '@xyflow/system';
import portal from '$lib/actions/portal'; import portal from '$lib/actions/portal';
import type { InternalNode } from '$lib/types'; import type { InternalNode } from '$lib/types';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -88,7 +88,9 @@ export function getDerivedConnectionProps(
const fromX = (fromNode?.internals.positionAbsolute.x ?? 0) + fromHandleX; const fromX = (fromNode?.internals.positionAbsolute.x ?? 0) + fromHandleX;
const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY; const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined; const toPosition =
connection.connectionEndHandle?.position ??
(fromPosition ? oppositePosition[fromPosition] : undefined);
const pathParams = { const pathParams = {
sourceX: fromX, sourceX: fromX,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.29", "version": "0.0.30",
"description": "xyflow core system that powers React Flow and Svelte Flow.", "description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [ "keywords": [
"node-based UI", "node-based UI",
+1
View File
@@ -16,6 +16,7 @@ export type ConnectingHandle = {
nodeId: string; nodeId: string;
type: HandleType; type: HandleType;
handleId?: string | null; handleId?: string | null;
position?: Position | null;
}; };
export type ConnectionHandle = { export type ConnectionHandle = {
+4 -4
View File
@@ -132,23 +132,23 @@ export const addEdge = <EdgeType extends EdgeBase>(
return edges.concat(edge); return edges.concat(edge);
}; };
export type UpdateEdgeOptions = { export type ReconnectEdgeOptions = {
shouldReplaceId?: boolean; shouldReplaceId?: boolean;
}; };
/** /**
* A handy utility to update an existing Edge with new properties * A handy utility to reconnect an existing edge with new properties
* @param oldEdge - The edge you want to update * @param oldEdge - The edge you want to update
* @param newConnection - The new connection you want to update the edge with * @param newConnection - The new connection you want to update the edge with
* @param edges - The array of all current edges * @param edges - The array of all current edges
* @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id * @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id
* @returns the updated edges array * @returns the updated edges array
*/ */
export const updateEdge = <EdgeType extends EdgeBase>( export const reconnectEdge = <EdgeType extends EdgeBase>(
oldEdge: EdgeType, oldEdge: EdgeType,
newConnection: Connection, newConnection: Connection,
edges: EdgeType[], edges: EdgeType[],
options: UpdateEdgeOptions = { shouldReplaceId: true } options: ReconnectEdgeOptions = { shouldReplaceId: true }
): EdgeType[] => { ): EdgeType[] => {
const { id: oldEdgeId, ...rest } = oldEdge; const { id: oldEdgeId, ...rest } = oldEdge;
+10 -5
View File
@@ -42,8 +42,6 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []), : (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
params.targetHandle params.targetHandle
); );
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) { if (!sourceHandle || !targetHandle) {
params.onError?.( params.onError?.(
@@ -58,8 +56,10 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null; return null;
} }
const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle); const sourcePosition = sourceHandle?.position || Position.Bottom;
const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle); const targetPosition = targetHandle?.position || Position.Top;
const [sourceX, sourceY] = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
const [targetX, targetY] = getHandlePosition(targetNode, targetHandle, targetPosition);
return { return {
sourceX, sourceX,
@@ -96,10 +96,15 @@ function toHandleBounds(handles?: NodeHandle[]) {
}; };
} }
function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] { export function getHandlePosition(
node: InternalNodeBase,
handle: HandleElement | null,
fallbackPosition: Position = Position.Left
): number[] {
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x; const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y; const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
const { width, height } = handle ?? getNodeDimensions(node); const { width, height } = handle ?? getNodeDimensions(node);
const position = handle?.position ?? fallbackPosition;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
+12 -12
View File
@@ -13,6 +13,7 @@ import {
type IsValidConnection, type IsValidConnection,
type ConnectionHandle, type ConnectionHandle,
NodeLookup, NodeLookup,
Position,
} from '../types'; } from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils';
@@ -36,7 +37,7 @@ export type OnPointerDownParams = {
onConnect?: OnConnect; onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd; onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection; isValidConnection?: IsValidConnection;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void; onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
getTransform: () => Transform; getTransform: () => Transform;
getConnectionStartHandle: () => ConnectingHandle | null; getConnectionStartHandle: () => ConnectingHandle | null;
}; };
@@ -89,7 +90,7 @@ function onPointerDown(
onConnect, onConnect,
onConnectEnd, onConnectEnd,
isValidConnection = alwaysValid, isValidConnection = alwaysValid,
onEdgeUpdateEnd, onReconnectEnd,
updateConnection, updateConnection,
getTransform, getTransform,
getConnectionStartHandle, getConnectionStartHandle,
@@ -138,12 +139,12 @@ function onPointerDown(
nodeId, nodeId,
handleId, handleId,
type: handleType, type: handleType,
position: (clickedHandle?.getAttribute('data-handlepos') as Position) || Position.Top,
}; };
updateConnection({ updateConnection({
connectionPosition, connectionPosition,
connectionStatus: null, connectionStatus: null,
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionStartHandle, connectionStartHandle,
connectionEndHandle: null, connectionEndHandle: null,
}); });
@@ -211,7 +212,7 @@ function onPointerDown(
onConnectEnd?.(event); onConnectEnd?.(event);
if (edgeUpdaterType) { if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event); onReconnectEnd?.(event);
} }
cancelConnection(); cancelConnection();
@@ -297,15 +298,14 @@ function isValidHandle(
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target') ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId); : handleNodeId !== fromNodeId || handleId !== fromHandleId);
if (isValid) { result.isValid = isValid && isValidConnection(connection);
result.endHandle = {
nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
};
result.isValid = isValidConnection(connection); result.endHandle = {
} nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
position: handleToCheck.getAttribute('data-handlepos') as Position,
};
} }
return result; return result;
+7 -5
View File
@@ -1,3 +1,4 @@
import { getHandlePosition } from '../utils';
import { import {
ConnectionStatus, ConnectionStatus,
type HandleType, type HandleType,
@@ -16,14 +17,15 @@ export function getHandles(
type: HandleType, type: HandleType,
currentHandle: string currentHandle: string
): ConnectionHandle[] { ): ConnectionHandle[] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => { return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, handle) => {
if (`${node.id}-${h.id}-${type}` !== currentHandle) { if (`${node.id}-${handle.id}-${type}` !== currentHandle) {
const [x, y] = getHandlePosition(node, handle);
res.push({ res.push({
id: h.id || null, id: handle.id || null,
type, type,
nodeId: node.id, nodeId: node.id,
x: node.internals.positionAbsolute.x + h.x + h.width / 2, x,
y: node.internals.positionAbsolute.y + h.y + h.height / 2, y,
}); });
} }
return res; return res;
+5
View File
@@ -38,6 +38,7 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
zoomable = true, zoomable = true,
inversePan = false, inversePan = false,
}: XYMinimapUpdate) { }: XYMinimapUpdate) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => { const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => {
const transform = getTransform(); const transform = getTransform();
@@ -55,6 +56,7 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
}; };
let panStart = [0, 0]; let panStart = [0, 0];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const panStartHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => { const panStartHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (event.sourceEvent.type === 'mousedown' || event.sourceEvent.type === 'touchstart') { if (event.sourceEvent.type === 'mousedown' || event.sourceEvent.type === 'touchstart') {
panStart = [ panStart = [
@@ -64,6 +66,7 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
} }
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const panHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => { const panHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
const transform = getTransform(); const transform = getTransform();
@@ -101,8 +104,10 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
const zoomAndPanHandler = zoom() const zoomAndPanHandler = zoom()
.on('start', panStartHandler) .on('start', panStartHandler)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
.on('zoom', pannable ? panHandler : null) .on('zoom', pannable ? panHandler : null)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
.on('zoom.wheel', zoomable ? zoomHandler : null); .on('zoom.wheel', zoomable ? zoomHandler : null);
+316 -225
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -32,14 +32,14 @@ test.describe('Node Toolbar', async () => {
}); });
test('all toolbars are positioned correctly', async ({ page }) => { test('all toolbars are positioned correctly', async ({ page }) => {
permutations.forEach(async (permutation) => { const tests = permutations.map((permutation) => async () => {
const toolbar = page const toolbar = page
.locator(`[data-id="${permutation.id}"]`) .locator(`[data-id="${permutation.id}"]`)
.and(page.locator(`.${FRAMEWORK}-flow__node-toolbar`)); .and(page.locator(`.${FRAMEWORK}-flow__node-toolbar`));
const node = page.locator(`[data-id="${permutation.id}"]`).and(page.locator(`.${FRAMEWORK}-flow__node`)); const node = page.locator(`[data-id="${permutation.id}"]`).and(page.locator(`.${FRAMEWORK}-flow__node`));
await expect(toolbar).toBeAttached(); await expect(toolbar).toBeAttached({ timeout: 5000 });
await expect(node).toBeAttached(); await expect(node).toBeAttached({ timeout: 5000 });
const toolbarBox = await toolbar.boundingBox(); const toolbarBox = await toolbar.boundingBox();
const nodeBox = await node.boundingBox(); const nodeBox = await node.boundingBox();
@@ -78,6 +78,7 @@ test.describe('Node Toolbar', async () => {
break; break;
} }
}); });
await Promise.all(tests.map((t) => t()));
}); });
test('toolbar default behaviour', async ({ page }) => { test('toolbar default behaviour', async ({ page }) => {
+622 -372
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -13,15 +13,15 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.42.1", "@playwright/test": "^1.44.1",
"@types/node": "^18.7.16" "@types/node": "^20.14.6"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-react": "^1.42.1", "@playwright/experimental-ct-react": "^1.44.1",
"@types/react": "^18.2.31", "@types/react": "^18.3.3",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.3.0",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.3.1",
"typescript": "^5.2.2" "typescript": "^5.4.5"
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ export function sharedConfigWithPort({ port, framework }: ConfigParams): Playwri
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: process.env.CI ? 'dot' : 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
+2 -2
View File
@@ -7,7 +7,7 @@
"devDependencies": { "devDependencies": {
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-config-turbo": "latest", "eslint-config-turbo": "^2.0.3",
"eslint-plugin-react": "latest" "eslint-plugin-react": "^7.33.2"
} }
} }
+3 -3
View File
@@ -1,6 +1,7 @@
{ {
"$schema": "https://turborepo.org/schema.json", "$schema": "https://turborepo.org/schema.json",
"pipeline": { "globalEnv": ["NODE_ENV"],
"tasks": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"outputs": ["dist/**"] "outputs": ["dist/**"]
@@ -20,6 +21,5 @@
"dependsOn": ["^build"], "dependsOn": ["^build"],
"outputs": [] "outputs": []
} }
}, }
"globalEnv": ["NODE_ENV"]
} }