diff --git a/examples/vite-app/src/App/index.tsx b/examples/vite-app/src/App/index.tsx
index 02eaac1e..8ecdd8b8 100644
--- a/examples/vite-app/src/App/index.tsx
+++ b/examples/vite-app/src/App/index.tsx
@@ -5,6 +5,7 @@ import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import CustomConnectionLine from '../examples/CustomConnectionLine';
+import CustomMiniMapNode from '../examples/CustomMiniMapNode';
import CustomNode from '../examples/CustomNode';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
@@ -77,6 +78,11 @@ const routes: IRoute[] = [
path: '/custom-connectionline',
component: CustomConnectionLine,
},
+ {
+ name: 'Custom Minimap Node',
+ path: '/custom-minimap-node',
+ component: CustomMiniMapNode,
+ },
{
name: 'Custom Node',
path: '/custom-node',
diff --git a/examples/vite-app/src/examples/CustomMiniMapNode/index.tsx b/examples/vite-app/src/examples/CustomMiniMapNode/index.tsx
new file mode 100644
index 00000000..6a8e5807
--- /dev/null
+++ b/examples/vite-app/src/examples/CustomMiniMapNode/index.tsx
@@ -0,0 +1,74 @@
+import { MouseEvent, CSSProperties, useCallback } from 'react';
+
+import ReactFlow, {
+ addEdge,
+ Background,
+ BackgroundVariant,
+ Connection,
+ Controls,
+ Edge,
+ MiniMap,
+ MiniMapNodeProps,
+ Node,
+ ReactFlowInstance,
+ useEdgesState,
+ useNodesState,
+} from 'reactflow';
+
+const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
+const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
+const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
+
+const buttonStyle: CSSProperties = {
+ position: 'absolute',
+ left: 10,
+ top: 10,
+ zIndex: 4,
+};
+
+const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => (
+
+);
+
+const CustomMiniMapNodeFlow = () => {
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+
+ const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
+ const addRandomNode = () => {
+ const nodeId = (nodes.length + 1).toString();
+ const newNode: Node = {
+ id: nodeId,
+ data: { label: `Node: ${nodeId}` },
+ position: {
+ x: Math.random() * window.innerWidth,
+ y: Math.random() * window.innerHeight,
+ },
+ };
+ setNodes((nds) => nds.concat(newNode));
+ };
+
+ return (
+ onConnect(p)}
+ onNodeDragStop={onNodeDragStop}
+ onlyRenderVisibleElements={false}
+ >
+
+
+
+
+
+
+ );
+};
+
+export default CustomMiniMapNodeFlow;
diff --git a/examples/vite-app/src/examples/Figma/index.tsx b/examples/vite-app/src/examples/Figma/index.tsx
index 22f0dcce..64cfb4bb 100644
--- a/examples/vite-app/src/examples/Figma/index.tsx
+++ b/examples/vite-app/src/examples/Figma/index.tsx
@@ -38,6 +38,9 @@ const BasicFlow = () => {
onSelectionContextMenu={onPaneContextMenu}
>
+
+
+
);
};
diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx
index 6cf58fd9..a36d9f18 100644
--- a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx
+++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx
@@ -10,10 +10,21 @@ const controlStyle = {
border: 'none',
};
-const CustomNode: FC = ({ id, data }) => {
+const CustomResizerNode: FC = ({ data }) => {
return (
<>
-
+
@@ -24,4 +35,4 @@ const CustomNode: FC = ({ id, data }) => {
);
};
-export default memo(CustomNode);
+export default memo(CustomResizerNode);
diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx
deleted file mode 100644
index 63ff061b..00000000
--- a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { memo, FC } from 'react';
-import { Handle, Position, NodeProps } from 'reactflow';
-
-import { NodeResizeControl } from '@reactflow/node-resizer';
-import '@reactflow/node-resizer/dist/style.css';
-
-const CustomNode: FC = ({ id, data }) => {
- return (
- <>
-
-
-
- {data.label}
-
- >
- );
-};
-
-export default memo(CustomNode);
diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx
deleted file mode 100644
index 422f2982..00000000
--- a/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { memo, FC } from 'react';
-import { Handle, Position, NodeProps } from 'reactflow';
-
-import { NodeResizeControl, OnResize, ShouldResize } from '@reactflow/node-resizer';
-import '@reactflow/node-resizer/dist/style.css';
-
-const shouldResize: ShouldResize = (event, params) => {
- console.log('before resize', params);
-
- if (params.width > 100) {
- return false;
- }
-
- return true;
-};
-
-const onResize: OnResize = (event, params) => {
- console.log('resize', params.direction);
-};
-
-const CustomNode: FC = ({ id, data }) => {
- return (
- <>
-
-
-
- {data.label}
-
- >
- );
-};
-
-export default memo(CustomNode);
diff --git a/examples/vite-app/src/examples/NodeResizer/DefaultResizer.tsx b/examples/vite-app/src/examples/NodeResizer/DefaultResizer.tsx
new file mode 100644
index 00000000..3d9649a5
--- /dev/null
+++ b/examples/vite-app/src/examples/NodeResizer/DefaultResizer.tsx
@@ -0,0 +1,29 @@
+import { memo, FC } from 'react';
+import { Handle, Position, NodeProps } from 'reactflow';
+import { NodeResizer } from '@reactflow/node-resizer';
+
+import '@reactflow/node-resizer/dist/style.css';
+
+const DefaultResizerNode: FC = ({ data, selected }) => {
+ return (
+ <>
+
+
+ {data.label}
+
+ >
+ );
+};
+
+export default memo(DefaultResizerNode);
diff --git a/examples/vite-app/src/examples/NodeResizer/HorizontalResizer.tsx b/examples/vite-app/src/examples/NodeResizer/HorizontalResizer.tsx
new file mode 100644
index 00000000..2798ca3f
--- /dev/null
+++ b/examples/vite-app/src/examples/NodeResizer/HorizontalResizer.tsx
@@ -0,0 +1,43 @@
+import { memo, FC } from 'react';
+import { Handle, Position, NodeProps } from 'reactflow';
+import { NodeResizeControl } from '@reactflow/node-resizer';
+
+import '@reactflow/node-resizer/dist/style.css';
+
+const HorizontalResizerNode: FC = ({ data }) => {
+ return (
+ <>
+
+
+
+ {data.label}
+
+ >
+ );
+};
+
+export default memo(HorizontalResizerNode);
diff --git a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx
deleted file mode 100644
index 03e4960f..00000000
--- a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { memo, FC } from 'react';
-import { Handle, Position, NodeProps } from 'reactflow';
-
-import { NodeResizer, ShouldResize, OnResize, OnResizeEnd, OnResizeStart } from '@reactflow/node-resizer';
-import '@reactflow/node-resizer/dist/style.css';
-
-const onResizeStart: OnResizeStart = (_, params) => {
- console.log('resize start', params);
-};
-
-const onResize: OnResize = (_, params) => {
- console.log('resize', params);
-};
-
-const onResizeEnd: OnResizeEnd = (_, params) => {
- console.log('resize end', params);
-};
-
-const shouldResize: ShouldResize = (_, params) => {
- console.log('should resize', params);
-
- return true;
-};
-
-const CustomNode: FC = ({ data, selected }) => {
- return (
- <>
-
-
- {data.label}
-
- >
- );
-};
-
-export default memo(CustomNode);
diff --git a/examples/vite-app/src/examples/NodeResizer/VerticalResizer.tsx b/examples/vite-app/src/examples/NodeResizer/VerticalResizer.tsx
new file mode 100644
index 00000000..a3d6515e
--- /dev/null
+++ b/examples/vite-app/src/examples/NodeResizer/VerticalResizer.tsx
@@ -0,0 +1,43 @@
+import { memo, FC } from 'react';
+import { Handle, Position, NodeProps } from 'reactflow';
+
+import { NodeResizeControl } from '@reactflow/node-resizer';
+import '@reactflow/node-resizer/dist/style.css';
+
+const CustomNode: FC = ({ id, data }) => {
+ return (
+ <>
+
+
+
+ {data.label}
+
+ >
+ );
+};
+
+export default memo(CustomNode);
diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx
index 01bb0851..d94a8d8b 100644
--- a/examples/vite-app/src/examples/NodeResizer/index.tsx
+++ b/examples/vite-app/src/examples/NodeResizer/index.tsx
@@ -1,73 +1,110 @@
import { useCallback, useState } from 'react';
-import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState, Panel } from 'reactflow';
+import ReactFlow, { Controls, addEdge, Connection, useNodesState, useEdgesState, Panel, Node, Edge } from 'reactflow';
-import NodeResizerNode from './NodeResizerNode';
+import DefaultResizer from './DefaultResizer';
import CustomResizer from './CustomResizer';
-import CustomResizer2 from './CustomResizer2';
-import CustomResizer3 from './CustomResizer3';
+import VerticalResizer from './VerticalResizer';
+import HorizontalResizer from './HorizontalResizer';
const nodeTypes = {
- resizer: NodeResizerNode,
+ defaultResizer: DefaultResizer,
customResizer: CustomResizer,
- customResizer2: CustomResizer2,
- customResizer3: CustomResizer3,
+ verticalResizer: VerticalResizer,
+ horizontalResizer: HorizontalResizer,
};
-const initialEdges = [
- {
- id: 'e1-2',
- source: '1',
- target: '2',
- },
-];
+const nodeStyle = {
+ border: '1px solid #222',
+ fontSize: 10,
+ backgroundColor: '#ddd',
+};
-const initialNodes = [
+const initialEdges: Edge[] = [];
+
+const initialNodes: Node[] = [
{
id: '1',
- type: 'input',
- data: { label: 'An input node' },
+ type: 'defaultResizer',
+ data: { label: 'default resizer' },
position: { x: 0, y: 0 },
- sourcePosition: Position.Right,
+ style: { ...nodeStyle },
},
{
- id: '2',
- type: 'resizer',
- data: { label: 'default resizer' },
+ id: '1a',
+ type: 'defaultResizer',
+ data: {
+ label: 'default resizer with min and max dimensions',
+ minWidth: 100,
+ minHeight: 80,
+ maxWidth: 200,
+ maxHeight: 200,
+ },
+ position: { x: 0, y: 60 },
+ style: { ...nodeStyle, width: 100, height: 80 },
+ },
+ {
+ id: '1b',
+ type: 'defaultResizer',
+ data: {
+ label: 'default resizer with initial size and aspect ratio',
+ keepAspectRatio: true,
+ minWidth: 100,
+ minHeight: 60,
+ maxWidth: 400,
+ maxHeight: 400,
+ },
position: { x: 250, y: 0 },
style: {
- width: 200,
- height: 150,
- border: '1px solid #222',
- fontSize: 10,
+ width: 174,
+ height: 123,
+ ...nodeStyle,
},
},
{
- id: '3',
+ id: '2',
type: 'customResizer',
- data: { label: 'resize control with child component' },
+ data: { label: 'custom resize icon' },
+ position: { x: 0, y: 200 },
+ style: { width: 100, height: 60, ...nodeStyle },
+ },
+ {
+ id: '3',
+ type: 'verticalResizer',
+ data: { label: 'vertical resizer' },
position: { x: 250, y: 200 },
- style: { border: '1px solid #222', fontSize: 10, width: 100 },
+ style: { ...nodeStyle },
+ },
+ {
+ id: '3a',
+ type: 'verticalResizer',
+ data: {
+ label: 'vertical resizer with min/maxHeight and aspect ratio',
+ minHeight: 50,
+ maxHeight: 200,
+ keepAspectRatio: true,
+ },
+ position: { x: 400, y: 200 },
+ style: { ...nodeStyle, height: 50 },
},
{
id: '4',
- type: 'customResizer2',
- data: { label: 'resize controls' },
- position: { x: 100, y: 150 },
- style: { border: '1px solid #222', fontSize: 10 },
+ type: 'horizontalResizer',
+ data: {
+ label: 'horizontal resizer with aspect ratio',
+ keepAspectRatio: true,
+ minHeight: 20,
+ maxHeight: 80,
+ maxWidth: 300,
+ },
+ position: { x: 250, y: 300 },
+ style: { ...nodeStyle },
},
{
- id: '5',
- type: 'customResizer2',
- data: { label: 'min width and height' },
- position: { x: 250, y: 250 },
- style: { border: '1px solid #222', fontSize: 10 },
- },
- {
- id: '6',
- type: 'customResizer3',
- data: { label: 'resize controls' },
- position: { x: 400, y: 200 },
- style: { border: '1px solid #222', fontSize: 10 },
+ id: '4a',
+ type: 'horizontalResizer',
+ data: { label: 'horizontal resizer with maxWidth', maxWidth: 300 },
+ position: { x: 250, y: 400 },
+ style: { ...nodeStyle },
},
];
diff --git a/examples/vite-app/src/examples/UseOnSelectionChange/index.tsx b/examples/vite-app/src/examples/UseOnSelectionChange/index.tsx
index 160ca769..b6f5531f 100644
--- a/examples/vite-app/src/examples/UseOnSelectionChange/index.tsx
+++ b/examples/vite-app/src/examples/UseOnSelectionChange/index.tsx
@@ -18,6 +18,20 @@ const initialNodes: Node[] = [
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
+ {
+ id: '2',
+ type: 'default',
+ data: { label: 'Node 2' },
+ position: { x: 250, y: 100 },
+ },
+];
+
+const initialEdges: Edge[] = [
+ {
+ id: 'e1-2',
+ source: '1',
+ target: '2',
+ },
];
const SelectionLogger = () => {
@@ -34,7 +48,7 @@ const SelectionLogger = () => {
const Flow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
- const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
return (
@@ -52,6 +66,9 @@ const WrappedFlow = () => (
+
+
+
);
diff --git a/examples/vite-app/src/examples/Validation/index.tsx b/examples/vite-app/src/examples/Validation/index.tsx
index 1488b48c..c55d8178 100644
--- a/examples/vite-app/src/examples/Validation/index.tsx
+++ b/examples/vite-app/src/examples/Validation/index.tsx
@@ -12,6 +12,8 @@ import ReactFlow, {
OnConnectStart,
OnConnectEnd,
OnConnect,
+ updateEdge,
+ Edge,
} from 'reactflow';
import styles from './validation.module.css';
@@ -28,15 +30,15 @@ const isValidConnection = (connection: Connection) => connection.target === 'B';
const CustomInput: FC = () => (
<>
Only connectable with B
-
+
>
);
const CustomNode: FC = ({ id }) => (
<>
-
+
{id}
-
+
>
);
@@ -74,6 +76,11 @@ const ValidationFlow = () => {
[value]
);
+ const onEdgeUpdate = useCallback(
+ (oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)),
+ [setEdges]
+ );
+
return (
{
nodeTypes={nodeTypes}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
+ onEdgeUpdate={onEdgeUpdate}
+ isValidConnection={isValidConnection}
fitView
/>
);
diff --git a/packages/background/CHANGELOG.md b/packages/background/CHANGELOG.md
index 4729e9a4..d179fca5 100644
--- a/packages/background/CHANGELOG.md
+++ b/packages/background/CHANGELOG.md
@@ -1,5 +1,14 @@
# @reactflow/background
+## 11.1.9
+
+### Patch Changes
+
+- [#2895](https://github.com/wbkd/react-flow/pull/2895) [`3d5764ca`](https://github.com/wbkd/react-flow/commit/3d5764cac6548984a30cbf85899024e62fd69425) Thanks [@moklick](https://github.com/moklick)! - add data-testid for controls, minimap and background
+
+- Updated dependencies [[`3d5764ca`](https://github.com/wbkd/react-flow/commit/3d5764cac6548984a30cbf85899024e62fd69425), [`83fc4675`](https://github.com/wbkd/react-flow/commit/83fc467545527729633e817dbccfe59d0040da4b), [`b1190837`](https://github.com/wbkd/react-flow/commit/b11908370bc438ca8d4179497cd4eb1f8c656798), [`5fabd272`](https://github.com/wbkd/react-flow/commit/5fabd2720f6367f75f79a45822d8f675a3b8e1cf), [`8f080bd5`](https://github.com/wbkd/react-flow/commit/8f080bd5e0e7e6c71f51eee9c9f2bc4b25182861), [`b8886514`](https://github.com/wbkd/react-flow/commit/b88865140c72fa7e92a883498768000cb2cc96a7), [`16bf89f2`](https://github.com/wbkd/react-flow/commit/16bf89f2b7bbf8449c00d0e2c07c19c3ff6d2533)]:
+ - @reactflow/core@11.6.0
+
## 11.1.8
### Patch Changes
diff --git a/packages/background/package.json b/packages/background/package.json
index 8dcd3148..5f2a889f 100644
--- a/packages/background/package.json
+++ b/packages/background/package.json
@@ -1,6 +1,6 @@
{
"name": "@reactflow/background",
- "version": "11.1.8",
+ "version": "11.1.9",
"description": "Background component with different variants for React Flow",
"keywords": [
"react",
diff --git a/packages/background/src/Background.tsx b/packages/background/src/Background.tsx
index 10702d81..bf8a4336 100644
--- a/packages/background/src/Background.tsx
+++ b/packages/background/src/Background.tsx
@@ -59,6 +59,7 @@ function Background({
left: 0,
}}
ref={ref}
+ data-testid="rf__background"
>
> = ({
};
return (
-
+
{showZoom && (
<>
true;
+
export default (EdgeComponent: ComponentType) => {
const EdgeWrapper = ({
id,
@@ -94,12 +96,14 @@ export default (EdgeComponent: ComponentType) => {
return;
}
+ const { edges, isValidConnection: isValidConnectionStore } = store.getState();
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
- const isValidConnection = () => true;
+ const isValidConnection = isValidConnectionStore || alwaysValidConnection;
+
const isTarget = isSourceHandle;
- const edge = store.getState().edges.find((e) => e.id === id)!;
+ const edge = edges.find((e) => e.id === id)!;
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx
index dab16653..e2d57d4d 100644
--- a/packages/core/src/components/Handle/index.tsx
+++ b/packages/core/src/components/Handle/index.tsx
@@ -27,7 +27,7 @@ const Handle = forwardRef(
{
type = 'source',
position = Position.Top,
- isValidConnection = alwaysValid,
+ isValidConnection,
isConnectable = true,
id,
onConnect,
@@ -61,8 +61,8 @@ const Handle = forwardRef(
...params,
};
if (hasDefaultEdges) {
- const { edges } = store.getState();
- store.setState({ edges: addEdge(edgeParams, edges) });
+ const { edges, setEdges } = store.getState();
+ setEdges(addEdge(edgeParams, edges));
}
onConnectAction?.(edgeParams);
@@ -81,7 +81,7 @@ const Handle = forwardRef(
isTarget,
getState: store.getState,
setState: store.setState,
- isValidConnection,
+ isValidConnection: isValidConnection || store.getState().isValidConnection || alwaysValid,
});
}
@@ -93,7 +93,12 @@ const Handle = forwardRef(
};
const onClick = (event: ReactMouseEvent) => {
- const { onClickConnectStart, onClickConnectEnd, connectionMode } = store.getState();
+ const {
+ onClickConnectStart,
+ onClickConnectEnd,
+ connectionMode,
+ isValidConnection: isValidConnectionStore,
+ } = store.getState();
if (!connectionStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
@@ -101,6 +106,7 @@ const Handle = forwardRef(
}
const doc = getHostForElement(event.target as HTMLElement);
+ const isValidConnectionHandler = isValidConnection || isValidConnectionStore || alwaysValid;
const { connection, isValid } = isValidHandle(
event,
{
@@ -112,7 +118,7 @@ const Handle = forwardRef(
connectionStartHandle.nodeId,
connectionStartHandle.handleId || null,
connectionStartHandle.type,
- isValidConnection,
+ isValidConnectionHandler,
doc
);
diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts
index 58263781..bfd181e1 100644
--- a/packages/core/src/components/Handle/utils.ts
+++ b/packages/core/src/components/Handle/utils.ts
@@ -104,9 +104,10 @@ export function isValidHandle(
// in strict mode we don't allow target to target or source to source connections
const isValid =
- connectionMode === ConnectionMode.Strict
+ handleToCheck.classList.contains('connectable') &&
+ (connectionMode === ConnectionMode.Strict
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
- : handleNodeId !== fromNodeId || handleId !== fromHandleId;
+ : handleNodeId !== fromNodeId || handleId !== fromHandleId);
if (isValid) {
result.isValid = isValidConnection(connection);
diff --git a/packages/core/src/components/Nodes/utils.ts b/packages/core/src/components/Nodes/utils.ts
index dca9f297..174b1be7 100644
--- a/packages/core/src/components/Nodes/utils.ts
+++ b/packages/core/src/components/Nodes/utils.ts
@@ -1,4 +1,4 @@
-import { MouseEvent } from 'react';
+import { MouseEvent, RefObject } from 'react';
import { StoreApi } from 'zustand';
import { getDimensions } from '../../utils';
@@ -58,6 +58,7 @@ export function handleNodeClick({
id,
store,
unselect = false,
+ nodeRef,
}: {
id: string;
store: {
@@ -65,6 +66,7 @@ export function handleNodeClick({
setState: StoreApi['setState'];
};
unselect?: boolean;
+ nodeRef?: RefObject;
}) {
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
const node = nodeInternals.get(id)!;
@@ -75,5 +77,7 @@ export function handleNodeClick({
addSelectedNodes([id]);
} else if (unselect || (node.selected && multiSelectionActive)) {
unselectNodesAndEdges({ nodes: [node] });
+
+ requestAnimationFrame(() => nodeRef?.current?.blur());
}
}
diff --git a/packages/core/src/components/Nodes/wrapNode.tsx b/packages/core/src/components/Nodes/wrapNode.tsx
index f56f117b..6f95dbdd 100644
--- a/packages/core/src/components/Nodes/wrapNode.tsx
+++ b/packages/core/src/components/Nodes/wrapNode.tsx
@@ -74,6 +74,7 @@ export default (NodeComponent: ComponentType) => {
handleNodeClick({
id,
store,
+ nodeRef,
});
}
@@ -90,13 +91,12 @@ export default (NodeComponent: ComponentType) => {
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
- if (unselect) {
- nodeRef.current?.blur();
- }
+
handleNodeClick({
id,
store,
unselect,
+ nodeRef,
});
} else if (
!disableKeyboardA11y &&
diff --git a/packages/core/src/components/StoreUpdater/index.tsx b/packages/core/src/components/StoreUpdater/index.tsx
index febd3b0c..4491bb94 100644
--- a/packages/core/src/components/StoreUpdater/index.tsx
+++ b/packages/core/src/components/StoreUpdater/index.tsx
@@ -49,6 +49,7 @@ type StoreUpdaterProps = Pick<
| 'autoPanOnNodeDrag'
| 'onError'
| 'connectionRadius'
+ | 'isValidConnection'
> & { rfId: string };
const selector = (s: ReactFlowState) => ({
@@ -127,6 +128,7 @@ const StoreUpdater = ({
autoPanOnNodeDrag,
onError,
connectionRadius,
+ isValidConnection,
}: StoreUpdaterProps) => {
const {
setNodes,
@@ -184,6 +186,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
+ useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useStoreUpdater(nodes, setNodes);
useStoreUpdater(edges, setEdges);
diff --git a/packages/core/src/container/EdgeRenderer/utils.ts b/packages/core/src/container/EdgeRenderer/utils.ts
index edd74543..0bf08e2d 100644
--- a/packages/core/src/container/EdgeRenderer/utils.ts
+++ b/packages/core/src/container/EdgeRenderer/utils.ts
@@ -77,10 +77,10 @@ export function getHandle(bounds: HandleElement[], handleId?: string | null): Ha
return null;
}
- if (handleId) {
- return bounds.find((d) => d.id === handleId)!;
- } else if (bounds.length === 1) {
+ if (bounds.length === 1 || !handleId) {
return bounds[0];
+ } else if (handleId) {
+ return bounds.find((d) => d.id === handleId) || null;
}
return null;
diff --git a/packages/core/src/container/ReactFlow/index.tsx b/packages/core/src/container/ReactFlow/index.tsx
index 9e7b7a49..9fa23f9c 100644
--- a/packages/core/src/container/ReactFlow/index.tsx
+++ b/packages/core/src/container/ReactFlow/index.tsx
@@ -163,6 +163,7 @@ const ReactFlow = forwardRef(
autoPanOnConnect = true,
autoPanOnNodeDrag = true,
connectionRadius = 20,
+ isValidConnection,
onError,
style,
id,
@@ -295,6 +296,7 @@ const ReactFlow = forwardRef(
autoPanOnNodeDrag={autoPanOnNodeDrag}
onError={onError}
connectionRadius={connectionRadius}
+ isValidConnection={isValidConnection}
/>
{children}
diff --git a/packages/core/src/hooks/useDrag/index.ts b/packages/core/src/hooks/useDrag/index.ts
index 22d06daf..8821ab4a 100644
--- a/packages/core/src/hooks/useDrag/index.ts
+++ b/packages/core/src/hooks/useDrag/index.ts
@@ -153,6 +153,7 @@ function useDrag({
handleNodeClick({
id: nodeId,
store,
+ nodeRef: nodeRef as RefObject,
});
}
diff --git a/packages/core/src/hooks/useKeyPress.ts b/packages/core/src/hooks/useKeyPress.ts
index 20dcf37e..3301fc8f 100644
--- a/packages/core/src/hooks/useKeyPress.ts
+++ b/packages/core/src/hooks/useKeyPress.ts
@@ -19,6 +19,9 @@ const doc = typeof document !== 'undefined' ? document : null;
export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: doc }): boolean => {
const [keyPressed, setKeyPressed] = useState(false);
+ // we need to remember if a modifier key is pressed in order to track it
+ const modifierPressed = useRef(false);
+
// we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef(new Set([]));
@@ -43,7 +46,10 @@ export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = {
useEffect(() => {
if (keyCode !== null) {
const downHandler = (event: KeyboardEvent) => {
- if (isInputDOMNode(event)) {
+
+ modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey;
+
+ if (!modifierPressed.current && isInputDOMNode(event)) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
@@ -56,7 +62,7 @@ export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = {
};
const upHandler = (event: KeyboardEvent) => {
- if (isInputDOMNode(event)) {
+ if (!modifierPressed.current && isInputDOMNode(event)) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
@@ -67,6 +73,7 @@ export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = {
} else {
pressedKeys.current.delete(event[keyOrCode]);
}
+ modifierPressed.current = false;
};
const resetHandler = () => {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index c4a5b19e..158c9094 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -8,7 +8,7 @@ export { default as SimpleBezierEdge, getSimpleBezierPath } from './components/E
export { default as SmoothStepEdge, getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { default as BaseEdge } from './components/Edges/BaseEdge';
-export { internalsSymbol, rectToBox, boxToRect, getBoundsOfRects } from './utils';
+export { internalsSymbol, rectToBox, boxToRect, getBoundsOfRects, clamp } from './utils';
export {
isNode,
isEdge,
diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts
index c85225db..056957c3 100644
--- a/packages/core/src/store/index.ts
+++ b/packages/core/src/store/index.ts
@@ -33,8 +33,8 @@ const createRFStore = () =>
return Array.from(get().nodeInternals.values());
},
setEdges: (edges: Edge[]) => {
- const { defaultEdgeOptions = {} } = get();
- set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
+ const { defaultEdgeOptions = null } = get();
+ set({ edges: defaultEdgeOptions ? edges.map((e) => ({ ...defaultEdgeOptions, ...e })) : edges });
},
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
const hasDefaultNodes = typeof nodes !== 'undefined';
diff --git a/packages/core/src/store/initialState.ts b/packages/core/src/store/initialState.ts
index 99e9d298..55aca27f 100644
--- a/packages/core/src/store/initialState.ts
+++ b/packages/core/src/store/initialState.ts
@@ -62,6 +62,7 @@ const initialState: ReactFlowStore = {
autoPanOnNodeDrag: true,
connectionRadius: 20,
onError: devWarn,
+ isValidConnection: undefined,
};
export default initialState;
diff --git a/packages/core/src/types/component-props.ts b/packages/core/src/types/component-props.ts
index 1614805a..cae9da3b 100644
--- a/packages/core/src/types/component-props.ts
+++ b/packages/core/src/types/component-props.ts
@@ -38,6 +38,7 @@ import type {
SelectionMode,
OnError,
} from '.';
+import { ValidConnectionFunc } from '../components/Handle/utils';
export type ReactFlowProps = HTMLAttributes & {
nodes?: Node[];
@@ -144,6 +145,7 @@ export type ReactFlowProps = HTMLAttributes & {
autoPanOnConnect?: boolean;
connectionRadius?: number;
onError?: OnError;
+ isValidConnection?: ValidConnectionFunc;
};
export type ReactFlowRefType = HTMLDivElement;
diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts
index f628727e..549228a8 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 IsValidConnection = (edge: Edge | Connection) => boolean;
+
export type ConnectionStatus = 'valid' | 'invalid';
export enum ConnectionMode {
@@ -223,6 +225,8 @@ export type ReactFlowStore = {
autoPanOnConnect: boolean;
autoPanOnNodeDrag: boolean;
connectionRadius: number;
+
+ isValidConnection?: IsValidConnection;
};
export type ReactFlowActions = {
@@ -277,3 +281,7 @@ export type SelectionRect = Rect & {
};
export type OnError = (id: string, message: string) => void;
+
+export interface UpdateEdgeOptions {
+ shouldReplaceId?: boolean;
+}
\ No newline at end of file
diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts
index c7772a1c..0d28d659 100644
--- a/packages/core/src/utils/graph.ts
+++ b/packages/core/src/utils/graph.ts
@@ -12,6 +12,7 @@ import {
Rect,
NodeInternals,
NodeOrigin,
+ UpdateEdgeOptions,
} from '../types';
import { errorMessages } from '../contants';
@@ -93,32 +94,34 @@ export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] =>
return edges.concat(edge);
};
-export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
+export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[], options: UpdateEdgeOptions = { shouldReplaceId: true }): Edge[] => {
+ const { id: oldEdgeId, ...rest } = oldEdge;
+
if (!newConnection.source || !newConnection.target) {
devWarn('006', errorMessages['006']());
return edges;
}
- const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
+ const foundEdge = edges.find((e) => e.id === oldEdgeId) as Edge;
if (!foundEdge) {
- devWarn('007', errorMessages['007'](oldEdge.id));
+ devWarn('007', errorMessages['007'](oldEdgeId));
return edges;
}
// Remove old edge and create the new edge with parameters of old edge.
const edge = {
- ...oldEdge,
- id: getEdgeId(newConnection),
+ ...rest,
+ id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId,
source: newConnection.source,
target: newConnection.target,
sourceHandle: newConnection.sourceHandle,
targetHandle: newConnection.targetHandle,
} as Edge;
- return edges.filter((e) => e.id !== oldEdge.id).concat(edge);
+ return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
};
export const pointToRendererPoint = (
diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts
index 2d801d05..b4f0ca3f 100644
--- a/packages/core/src/utils/index.ts
+++ b/packages/core/src/utils/index.ts
@@ -104,11 +104,10 @@ export function isInputDOMNode(event: KeyboardEvent | ReactKeyboardEvent): boole
const target = (kbEvent.composedPath?.()?.[0] || event.target) as HTMLElement;
const isInput = ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || target?.hasAttribute('contenteditable');
- // we want to be able to do a multi selection event if we are in an input field
- const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
+
// when an input field is focused we don't want to trigger deletion or movement of nodes
- return (isInput && !isModifierKey) || !!target?.closest('.nokey');
+ return isInput || !!target?.closest('.nokey');
}
export const isMouseEvent = (
diff --git a/packages/minimap/CHANGELOG.md b/packages/minimap/CHANGELOG.md
index e0ab8a44..a1a322a9 100644
--- a/packages/minimap/CHANGELOG.md
+++ b/packages/minimap/CHANGELOG.md
@@ -1,5 +1,18 @@
# @reactflow/minimap
+## 11.4.0
+
+### Minor Changes
+
+- [#2906](https://github.com/wbkd/react-flow/pull/2906) [`4a30185a`](https://github.com/wbkd/react-flow/commit/4a30185a12899691ff61259f0db84bc5494cb573) Thanks [@hayleigh-dot-dev](https://github.com/hayleigh-dot-dev)! - Add `nodeComponent` prop for passing custom component for the nodes
+
+### Patch Changes
+
+- [#2895](https://github.com/wbkd/react-flow/pull/2895) [`3d5764ca`](https://github.com/wbkd/react-flow/commit/3d5764cac6548984a30cbf85899024e62fd69425) - add data-testid for controls, minimap and background
+
+- Updated dependencies [[`3d5764ca`](https://github.com/wbkd/react-flow/commit/3d5764cac6548984a30cbf85899024e62fd69425), [`83fc4675`](https://github.com/wbkd/react-flow/commit/83fc467545527729633e817dbccfe59d0040da4b), [`b1190837`](https://github.com/wbkd/react-flow/commit/b11908370bc438ca8d4179497cd4eb1f8c656798), [`5fabd272`](https://github.com/wbkd/react-flow/commit/5fabd2720f6367f75f79a45822d8f675a3b8e1cf), [`8f080bd5`](https://github.com/wbkd/react-flow/commit/8f080bd5e0e7e6c71f51eee9c9f2bc4b25182861), [`b8886514`](https://github.com/wbkd/react-flow/commit/b88865140c72fa7e92a883498768000cb2cc96a7), [`16bf89f2`](https://github.com/wbkd/react-flow/commit/16bf89f2b7bbf8449c00d0e2c07c19c3ff6d2533)]:
+ - @reactflow/core@11.6.0
+
## 11.3.8
### Patch Changes
diff --git a/packages/minimap/package.json b/packages/minimap/package.json
index 66e22ced..81f0ff1c 100644
--- a/packages/minimap/package.json
+++ b/packages/minimap/package.json
@@ -1,6 +1,6 @@
{
"name": "@reactflow/minimap",
- "version": "11.3.8",
+ "version": "11.4.0",
"description": "Minimap component for React Flow.",
"keywords": [
"react",
diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx
index bb4a2f89..59b3f34d 100644
--- a/packages/minimap/src/MiniMap.tsx
+++ b/packages/minimap/src/MiniMap.tsx
@@ -55,6 +55,9 @@ function MiniMap({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth = 2,
+ // We need to rename the prop to be `CapitalCase` so that JSX will render it as
+ // a component properly.
+ nodeComponent: NodeComponent = MiniMapNode,
maskColor = 'rgb(240, 240, 240, 0.6)',
maskStrokeColor = 'none',
maskStrokeWidth = 1,
@@ -161,7 +164,12 @@ function MiniMap({
: undefined;
return (
-
+