Merge pull request #5187 from xyflow/migrate/svelte5-diff

implemented new fitView logic/merged master
This commit is contained in:
Peter Kogo
2025-04-09 10:54:20 +02:00
committed by GitHub
131 changed files with 5502 additions and 2295 deletions
+1 -1
View File
@@ -38,7 +38,7 @@
"cypress": "13.6.6", "cypress": "13.6.6",
"cypress-real-events": "1.12.0", "cypress-real-events": "1.12.0",
"start-server-and-test": "^2.0.2", "start-server-and-test": "^2.0.2",
"typescript": "5.2.2", "typescript": "5.4.5",
"vite": "4.5.0" "vite": "4.5.0"
} }
} }
+6
View File
@@ -1,5 +1,6 @@
import Basic from '../examples/Basic'; import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds'; import Backgrounds from '../examples/Backgrounds';
import BrokenNodes from '../examples/BrokenNodes';
import ColorMode from '../examples/ColorMode'; import ColorMode from '../examples/ColorMode';
import ClickDistance from '../examples/ClickDistance'; import ClickDistance from '../examples/ClickDistance';
import ControlledUncontrolled from '../examples/ControlledUncontrolled'; import ControlledUncontrolled from '../examples/ControlledUncontrolled';
@@ -77,6 +78,11 @@ const routes: IRoute[] = [
path: 'backgrounds', path: 'backgrounds',
component: Backgrounds, component: Backgrounds,
}, },
{
name: 'Broken Nodes',
path: 'broken-nodes',
component: BrokenNodes,
},
{ {
name: 'Color Mode', name: 'Color Mode',
path: 'color-mode', path: 'color-mode',
+17 -2
View File
@@ -56,8 +56,18 @@ const initialEdges: Edge[] = [
const defaultEdgeOptions = {}; const defaultEdgeOptions = {};
const BasicFlow = () => { const BasicFlow = () => {
const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } = const {
useReactFlow(); addNodes,
setNodes,
getNodes,
setEdges,
getEdges,
deleteElements,
updateNodeData,
toObject,
setViewport,
fitView,
} = useReactFlow();
const updatePos = () => { const updatePos = () => {
setNodes((nodes) => setNodes((nodes) =>
@@ -104,6 +114,7 @@ const BasicFlow = () => {
]); ]);
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]); setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
fitView();
}; };
const onUpdateNode = () => { const onUpdateNode = () => {
@@ -117,6 +128,7 @@ const BasicFlow = () => {
position: { x: Math.random() * 300, y: Math.random() * 300 }, position: { x: Math.random() * 300, y: Math.random() * 300 },
className: 'light', className: 'light',
}); });
fitView();
}; };
return ( return (
@@ -134,6 +146,9 @@ const BasicFlow = () => {
minZoom={0.2} minZoom={0.2}
maxZoom={4} maxZoom={4}
fitView fitView
fitViewOptions={{
padding: { top: '100px', left: '0%', right: '10%', bottom: 0.1 },
}}
defaultEdgeOptions={defaultEdgeOptions} defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false} selectNodesOnDrag={false}
elevateEdgesOnSelect elevateEdgesOnSelect
@@ -0,0 +1,80 @@
import { useCallback, useState } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, OnNodeDrag } from '@xyflow/react';
const nodesInit: Node[] = [
{
id: '1a',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
ariaLabel: 'Input Node 1',
},
{
id: '2a',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
ariaLabel: 'Default Node 2',
},
{
id: '3a',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4a',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const edgesInit: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const onNodesChange = () => {};
const onEdgesChange = () => {};
const BasicFlow = () => {
const [nodes, setNodes] = useState(nodesInit);
const [edges, setEdges] = useState(edgesInit);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const onNodeDrag: OnNodeDrag = useCallback((e, node) => {
if (isNaN(node.position.x) || isNaN(node.position.y)) {
console.log('received NaN', node.position);
}
setNodes((nds) => {
return nds.map((item) => {
if (item.id === node.id) {
return {
...item,
position: {
x: node.position.x,
y: node.position.y,
},
};
}
return item;
});
});
}, []);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDrag={onNodeDrag}
></ReactFlow>
);
};
export default BasicFlow;
+13 -3
View File
@@ -12,6 +12,8 @@ import {
Controls, Controls,
Background, Background,
Panel, Panel,
ReactFlowProvider,
useReactFlow,
} from '@xyflow/react'; } from '@xyflow/react';
import { getNodesAndEdges } from './utils'; import { getNodesAndEdges } from './utils';
@@ -22,6 +24,7 @@ const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
const StressFlow = () => { const StressFlow = () => {
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 { fitView } = useReactFlow();
const onConnect = useCallback((connection: Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds)); setEdges((eds) => addEdge(connection, eds));
}, []); }, []);
@@ -191,12 +194,13 @@ const StressFlow = () => {
return { return {
...n, ...n,
position: { position: {
x: Math.random() * window.innerWidth, x: Math.random() * window.innerWidth * 4,
y: Math.random() * window.innerHeight, y: Math.random() * window.innerHeight * 4,
}, },
}; };
}); });
}); });
fitView();
}; };
const updateElements = () => { const updateElements = () => {
@@ -240,4 +244,10 @@ const StressFlow = () => {
); );
}; };
export default StressFlow; export default function StressFlowProvider() {
return (
<ReactFlowProvider>
<StressFlow />
</ReactFlowProvider>
);
}
@@ -10,7 +10,6 @@ import {
useEdgesState, useEdgesState,
useOnSelectionChange, useOnSelectionChange,
OnSelectionChangeParams, OnSelectionChangeParams,
OnSelectionChangeFunc,
} from '@xyflow/react'; } from '@xyflow/react';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
+12 -12
View File
@@ -12,20 +12,20 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.1", "@sveltejs/kit": "^2.20.4",
"@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.22.0", "@typescript-eslint/parser": "^8.29.1",
"eslint": "^8.53.0", "eslint": "^9.24.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^3.5.1",
"prettier": "^3.4.2", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.19.5", "svelte": "^5.25.8",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.5",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.7.3", "typescript": "^5.8.3",
"vite": "^6.0.11" "vite": "^6.2.5"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+1 -6
View File
@@ -25,20 +25,15 @@
"@changesets/changelog-github": "^0.4.7", "@changesets/changelog-github": "^0.4.7",
"@changesets/cli": "^2.25.0", "@changesets/cli": "^2.25.0",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "latest",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"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": "^4.18.0", "rollup": "^4.18.0",
"turbo": "^2.0.3", "turbo": "^2.0.3",
"typescript": "5.1.3" "typescript": "5.4.5"
}, },
"packageManager": "pnpm@9.2.0" "packageManager": "pnpm@9.2.0"
} }
+153
View File
@@ -1,5 +1,158 @@
# @xyflow/react # @xyflow/react
## 12.5.5
### Patch Changes
- [#5172](https://github.com/xyflow/xyflow/pull/5172) [`e6139a00`](https://github.com/xyflow/xyflow/commit/e6139a00d4414ba2c1d3e500cdfa67d7e66e655a) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useNodesData`, `useReactFlow`, `isNode` and `isEdge`
- [#5165](https://github.com/xyflow/xyflow/pull/5165) [`d536abea`](https://github.com/xyflow/xyflow/commit/d536abea9240bad7f5c1064efc0a4713ebef87d1) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useViewport`, `useUpdateNodeInternals`, `useOnSelectionChange`, `useNodesInitialized` hooks and `UseOnSelectionChangeOptions`, `UseNodesInitializedOptions` types
- [#5171](https://github.com/xyflow/xyflow/pull/5171) [`62d87409`](https://github.com/xyflow/xyflow/commit/62d874097337a022bebe54b559834c0d582f435e) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `ReactFlowProps`
- [#5154](https://github.com/xyflow/xyflow/pull/5154) [`d0237166`](https://github.com/xyflow/xyflow/commit/d02371662669aab91cd2ac7c45b412491c3377bd) Thanks [@ibagov](https://github.com/ibagov)! - Improve TSDoc comments for `onNodesChange`
- [#5174](https://github.com/xyflow/xyflow/pull/5174) [`ae585d13`](https://github.com/xyflow/xyflow/commit/ae585d136e34c9e9ad9d45f75c059bc5367e73ae) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `BaseEdgeProps`
- [#5159](https://github.com/xyflow/xyflow/pull/5159) [`0c1436d6`](https://github.com/xyflow/xyflow/commit/0c1436d6a371240cfa0adecea573c44fd42df7b3) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useConnection` hook
- [#5167](https://github.com/xyflow/xyflow/pull/5167) [`934ea42d`](https://github.com/xyflow/xyflow/commit/934ea42d9af6027a3164e1196a78140bdd05d347) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useEdges`, `useInternalNode`, `useNodes` and `useNodeId` hooks
- [#5163](https://github.com/xyflow/xyflow/pull/5163) [`ab800054`](https://github.com/xyflow/xyflow/commit/ab800054a50c68f9c27dfad2b0d3833b782f4797) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useNodesState` and `useEdgesState` hook
- [#5160](https://github.com/xyflow/xyflow/pull/5160) [`b357f43d`](https://github.com/xyflow/xyflow/commit/b357f43dfa262205059ac9714198196b4aaf8870) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseHandleConnectionsParams` and `useHandleConnections` hook
- [#5162](https://github.com/xyflow/xyflow/pull/5162) [`29f4aeb2`](https://github.com/xyflow/xyflow/commit/29f4aeb260df0a5d83e775c7c2ed788f997006a7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseNodeConnectionsParams` and `useNodeConnections` hook
- [#5164](https://github.com/xyflow/xyflow/pull/5164) [`09021550`](https://github.com/xyflow/xyflow/commit/09021550dc72ac240fcbb6adb3cf530d91575f79) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseOnViewportChangeOptions` and `useOnViewportChange` hook
- [#5166](https://github.com/xyflow/xyflow/pull/5166) [`701ad17e`](https://github.com/xyflow/xyflow/commit/701ad17ed3f523389e7d0bf9a006bef249645c3d) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useStore` hook
- [#5170](https://github.com/xyflow/xyflow/pull/5170) [`eb2a33c6`](https://github.com/xyflow/xyflow/commit/eb2a33c6dfb0629e851ab3a0f2cd70eca92efc42) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `interface GetSimpleBezierPathParams` and `getSimpleBezierPath`
- [#5161](https://github.com/xyflow/xyflow/pull/5161) [`c4efe749`](https://github.com/xyflow/xyflow/commit/c4efe749208e854dc9ced5bdd00933269d5b4382) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseKeyPressOptions` and `useKeyPress` hook
- Updated dependencies [[`02a3b746`](https://github.com/xyflow/xyflow/commit/02a3b74645799a3f0ce670b69365fa86ecb0616e), [`cbe305e1`](https://github.com/xyflow/xyflow/commit/cbe305e15a5c5d3b92583e0ec12364b2509f49bd), [`1f671bd4`](https://github.com/xyflow/xyflow/commit/1f671bd48f06230da841fdd1d7a312413ef16d03), [`aaebc462`](https://github.com/xyflow/xyflow/commit/aaebc462951ded8e91374c3e084d77af5ed7380a), [`6ec942fc`](https://github.com/xyflow/xyflow/commit/6ec942fc6501f81009c278cc995764bef3e8d03b)]:
- @xyflow/system@0.0.55
## 12.5.4
### Patch Changes
- [#5134](https://github.com/xyflow/xyflow/pull/5134) [`7acab1e1`](https://github.com/xyflow/xyflow/commit/7acab1e123944c296180fbf826a3fd488963608f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Adjust `@default` TSDoc tags for `BackgroundProps`
- [#5135](https://github.com/xyflow/xyflow/pull/5135) [`754a1671`](https://github.com/xyflow/xyflow/commit/754a167134f22fa00a139de4c7e10aaaa1953ac8) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `NodeResizerProps` and `ResizeControlProps`
- [#5140](https://github.com/xyflow/xyflow/pull/5140) [`82e6860e`](https://github.com/xyflow/xyflow/commit/82e6860e1354b8bb8047399b7773fd090be206d7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `MiniMapProps` and `PanelProps`
- [#5147](https://github.com/xyflow/xyflow/pull/5147) [`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a) Thanks [@moklick](https://github.com/moklick)! - Pass dimensions to final resize change event
- [#5143](https://github.com/xyflow/xyflow/pull/5143) [`b1e1cc11`](https://github.com/xyflow/xyflow/commit/b1e1cc1125d106cf1521a1524286404483e38f30) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `ReactFlowInstance` and `GeneralHelpers`
- [#5138](https://github.com/xyflow/xyflow/pull/5138) [`d4eb8d52`](https://github.com/xyflow/xyflow/commit/d4eb8d52d0e26e9534ec5fc347211ce91d1ddd32) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `ViewportHelperFunctions` and `NodeToolbarProps`
- [#5144](https://github.com/xyflow/xyflow/pull/5144) [`5a1ce56e`](https://github.com/xyflow/xyflow/commit/5a1ce56e8ce83f01e01ef531d90c52181c3e3a1a) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Compare `nodeStrokeWidth` with `number`, not with `string` within `MiniMap`
- [#5141](https://github.com/xyflow/xyflow/pull/5141) [`06cf4c10`](https://github.com/xyflow/xyflow/commit/06cf4c10f5d8a43f57ee0fde19d9a3fe1044cf48) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `Edge`, `BaseEdgeProps` and `ConnectionLineComponentProps`
- Updated dependencies [[`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a), [`24a1bc89`](https://github.com/xyflow/xyflow/commit/24a1bc89348817ed9b5c87f74bf2519c705143be), [`36657cd6`](https://github.com/xyflow/xyflow/commit/36657cd66322c911e87eb37275c584a80025adfe), [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654), [`2ac6e155`](https://github.com/xyflow/xyflow/commit/2ac6e155e35256ca436281df16344366e7d05761), [`f0f378e5`](https://github.com/xyflow/xyflow/commit/f0f378e5b6918c2c30d9dc1e32587063cb942d4e)]:
- @xyflow/system@0.0.54
## 12.5.3
### Patch Changes
- [#5132](https://github.com/xyflow/xyflow/pull/5132) [`75ab8942`](https://github.com/xyflow/xyflow/commit/75ab89420e3cd0fdc34baf06eabdc50113d4de7c) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working when onNodesChange is not defined.
## 12.5.2
### Patch Changes
- [#5124](https://github.com/xyflow/xyflow/pull/5124) [`b76f7f9e`](https://github.com/xyflow/xyflow/commit/b76f7f9eb4841f139b1468b8eda0430ddd19a1ae) Thanks [@bjornosal](https://github.com/bjornosal)! - Export NodeConnection type
- [#5127](https://github.com/xyflow/xyflow/pull/5127) [`3079c2c9`](https://github.com/xyflow/xyflow/commit/3079c2c911426f54e8d295083ddbe97ed3aad201) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `fitView` not working when returning early in `onNodesChange`.
## 12.5.1
### Patch Changes
- [#5120](https://github.com/xyflow/xyflow/pull/5120) [`6dfea686`](https://github.com/xyflow/xyflow/commit/6dfea6863a3cbd91f932bf54a6dba549bd248bd5) Thanks [@moklick](https://github.com/moklick)! - Handle fitView for uncontrolled flows
## 12.5.0
### Minor Changes
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`acba901d`](https://github.com/xyflow/xyflow/commit/acba901d861aa84cb5beba60b24fff4cfde7ada6) Thanks [@peterkogo](https://github.com/peterkogo)! - You can now express paddings in fitViewOptions as pixels ('30px'), as viewport percentages ('20%') and define different paddings for each side.
### Patch Changes
- [#5109](https://github.com/xyflow/xyflow/pull/5109) [`0cdda42c`](https://github.com/xyflow/xyflow/commit/0cdda42cdd1cd43d43d43c44e54b7b9f7a716ca9) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `BackgroundProps`
- [#5059](https://github.com/xyflow/xyflow/pull/5059) [`065ff89d`](https://github.com/xyflow/xyflow/commit/065ff89d10488f9c76c56870511e45eaed299778) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Prevent onPaneClick when connection is in progress. Closes [#5057](https://github.com/xyflow/xyflow/issues/5057)
- [#5110](https://github.com/xyflow/xyflow/pull/5110) [`7eb6eb07`](https://github.com/xyflow/xyflow/commit/7eb6eb0709e451d7628bfdbc3ced89b3bb57b626) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `EdgeLabelOptions` and `BaseEdgeProps`
- [#5113](https://github.com/xyflow/xyflow/pull/5113) [`bce8542d`](https://github.com/xyflow/xyflow/commit/bce8542df19c33f3cd9225f483435e8a7aa4ed94) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `ControlProps`
- [#5116](https://github.com/xyflow/xyflow/pull/5116) [`58942154`](https://github.com/xyflow/xyflow/commit/589421542386906ec49d8469cac551b8f7ea1c47) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `NodeToolbarProps`
- [#5114](https://github.com/xyflow/xyflow/pull/5114) [`ba2bfbb4`](https://github.com/xyflow/xyflow/commit/ba2bfbb49aafac979f94b0136bb408faea12d5c6) Thanks [@dimaMachina](https://github.com/dimaMachina)! - feat: export `EdgeLabelRendererProps`
- [#5107](https://github.com/xyflow/xyflow/pull/5107) [`c5a8c237`](https://github.com/xyflow/xyflow/commit/c5a8c23773e5985be6b37abacdac743911be8c09) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations for exported edges
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working immediately after adding new nodes
- [#5115](https://github.com/xyflow/xyflow/pull/5115) [`c2154557`](https://github.com/xyflow/xyflow/commit/c215455735385ef5e12e4130164b9d01f9c18aa2) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `EdgeLabelOptions` and `EdgeTextProps`
- [#5093](https://github.com/xyflow/xyflow/pull/5093) [`65825e89`](https://github.com/xyflow/xyflow/commit/65825e89a6e2e7591087eb41ac89da4da7095f8f) Thanks [@moklick](https://github.com/moklick)! - Hidden nodes are not displayed in the mini map anymore
- [#5090](https://github.com/xyflow/xyflow/pull/5090) [`8da1748a`](https://github.com/xyflow/xyflow/commit/8da1748a6ad5cdde9f03737ff786bd29c9c968de) Thanks [@moklick](https://github.com/moklick)! - Release key even when an inout field is focused
- Updated dependencies [[`5d15b01b`](https://github.com/xyflow/xyflow/commit/5d15b01ba8cb349d6397a6ed8162848b4dfec293), [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe), [`a79f30b3`](https://github.com/xyflow/xyflow/commit/a79f30b3dd7c8ff6400c8d22214b2c2282e5bac1)]:
- @xyflow/system@0.0.53
## 12.4.4
### Patch Changes
- [#5052](https://github.com/xyflow/xyflow/pull/5052) [`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90) Thanks [@moklick](https://github.com/moklick)! - Show an error if user drags uninitialized node
- [#5042](https://github.com/xyflow/xyflow/pull/5042) [`2fe0e850`](https://github.com/xyflow/xyflow/commit/2fe0e850a8c415c6a3113796a2c5c80e7cad2376) Thanks [@moklick](https://github.com/moklick)! - Allow click connections when target sets `isConnectableStart`
- [#5047](https://github.com/xyflow/xyflow/pull/5047) [`b3bf5693`](https://github.com/xyflow/xyflow/commit/b3bf5693c659069cea90bf1cb215ae65d06c5509) Thanks [@moklick](https://github.com/moklick)! - Pass generics to OnSelectionChangeFunc so that users can type it correctly
- [#5053](https://github.com/xyflow/xyflow/pull/5053) [`25fb45b5`](https://github.com/xyflow/xyflow/commit/25fb45b5e9d6da391b9aff652b8e6e34eaf757fc) Thanks [@moklick](https://github.com/moklick)! - Remove incorrect deprecation warning
- [#5033](https://github.com/xyflow/xyflow/pull/5033) [`7b4a81fb`](https://github.com/xyflow/xyflow/commit/7b4a81fb6b3d88f8ee7b4f070aef7ac3b962d5a6) Thanks [@dimaMachina](https://github.com/dimaMachina)! - lint: use `React.JSX` type instead of the deprecated global `JSX` namespace
- [#5043](https://github.com/xyflow/xyflow/pull/5043) [`0292ad20`](https://github.com/xyflow/xyflow/commit/0292ad20109a3b2518dc686a82e100a0a6964fb8) Thanks [@moklick](https://github.com/moklick)! - Use current expandParent value on drag to be able to update it while dragging
- [#5032](https://github.com/xyflow/xyflow/pull/5032) [`5867bba8`](https://github.com/xyflow/xyflow/commit/5867bba8050d07378a45a2026557c4bce7bda239) Thanks [@dimaMachina](https://github.com/dimaMachina)! - lint: remove unnecessary type assertions
- Updated dependencies [[`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90)]:
- @xyflow/system@0.0.52
## 12.4.3
### Patch Changes
- [#5010](https://github.com/xyflow/xyflow/pull/5010) [`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919) Thanks [@moklick](https://github.com/moklick)! - Add more TSDocs to components, hooks, utils funcs and types
- [#4991](https://github.com/xyflow/xyflow/pull/4991) [`ea54d9bc`](https://github.com/xyflow/xyflow/commit/ea54d9bcb197d02d248ef3e4eaabc033a43d966a) Thanks [@waynetee](https://github.com/waynetee)! - Fix viewport shifting on node focus
- [#5013](https://github.com/xyflow/xyflow/pull/5013) [`cde899c5`](https://github.com/xyflow/xyflow/commit/cde899c5be9715c4ff2cc331ea93821102604c62) Thanks [@moklick](https://github.com/moklick)! - Pass `NodeType` type argument from `ReactFlowProps` to `connectionLineComponent` property.
- [#5008](https://github.com/xyflow/xyflow/pull/5008) [`12d859fe`](https://github.com/xyflow/xyflow/commit/12d859fe297593d44cf8493a4d6bf2c664b9139c) Thanks [@moklick](https://github.com/moklick)! - Add package.json to exports
- [#5012](https://github.com/xyflow/xyflow/pull/5012) [`4d3f19e8`](https://github.com/xyflow/xyflow/commit/4d3f19e88b984ce6743970560d7367d174500f32) Thanks [@moklick](https://github.com/moklick)! - Add snapGrid option to screenToFlowPosition and set snapToGrid to false
- [#5003](https://github.com/xyflow/xyflow/pull/5003) [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - repair lint command
- [#4991](https://github.com/xyflow/xyflow/pull/4991) [`4c62f19b`](https://github.com/xyflow/xyflow/commit/4c62f19b3afac4b3db84b14e2c36f8c9e0a96116) Thanks [@waynetee](https://github.com/waynetee)! - Prevent viewport shift after using Tab
- Updated dependencies [[`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919), [`4947029c`](https://github.com/xyflow/xyflow/commit/4947029cd6cda0f695e1fb4815e4030adb232234), [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7)]:
- @xyflow/system@0.0.51
## 12.4.2 ## 12.4.2
### Patch Changes ### Patch Changes
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.4.2", "version": "12.5.5",
"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",
@@ -28,6 +28,7 @@
"module": "dist/esm/index.js", "module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts", "types": "dist/esm/index.d.ts",
"exports": { "exports": {
"./package.json": "./package.json",
".": { ".": {
"node": { "node": {
"types": "./dist/esm/index.d.ts", "types": "./dist/esm/index.d.ts",
@@ -84,7 +85,7 @@
"postcss-nested": "^6.0.0", "postcss-nested": "^6.0.0",
"postcss-rename": "^0.6.1", "postcss-rename": "^0.6.1",
"react": "^18.2.0", "react": "^18.2.0",
"typescript": "5.1.3" "typescript": "5.4.5"
}, },
"rollup": { "rollup": {
"globals": { "globals": {
@@ -90,4 +90,57 @@ function BackgroundComponent({
BackgroundComponent.displayName = 'Background'; BackgroundComponent.displayName = 'Background';
/**
* The `<Background />` component makes it convenient to render different types of backgrounds common in node-based UIs. It comes with three variants: lines, dots and cross.
*
* @example
*
* A simple example of how to use the Background component.
*
* ```tsx
* import { useState } from 'react';
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background color="#ccc" variant={BackgroundVariant.Dots} />
* </ReactFlow>
* );
* }
* ```
*
* @example
*
* In this example you can see how to combine multiple backgrounds
*
* ```tsx
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
* import '@xyflow/react/dist/style.css';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background
* id="1"
* gap={10}
* color="#f1f1f1"
* variant={BackgroundVariant.Lines}
* />
* <Background
* id="2"
* gap={100}
* color="#ccc"
* variant={BackgroundVariant.Lines}
* />
* </ReactFlow>
* );
* }
* ```
*
* @remarks
*
* When combining multiple <Background /> components its important to give each of them a unique id prop!
*
*/
export const Background = memo(BackgroundComponent); export const Background = memo(BackgroundComponent);
@@ -1,34 +1,60 @@
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
/**
* The three variants are exported as an enum for convenience. You can either import
* the enum and use it like `BackgroundVariant.Lines` or you can use the raw string
* value directly.
* @public
*/
export enum BackgroundVariant { export enum BackgroundVariant {
Lines = 'lines', Lines = 'lines',
Dots = 'dots', Dots = 'dots',
Cross = 'cross', Cross = 'cross',
} }
/**
* @expand
*/
export type BackgroundProps = { export type BackgroundProps = {
/** When multiple backgrounds are present on the page, each one should have a unique id. */
id?: string; id?: string;
/** Color of the pattern */ /** Color of the pattern. */
color?: string; color?: string;
/** Color of the background */ /** Color of the background. */
bgColor?: string; bgColor?: string;
/** Class applied to the container */ /** Class applied to the container. */
className?: string; className?: string;
/** Class applied to the pattern */ /** Class applied to the pattern. */
patternClassName?: string; patternClassName?: string;
/** Gap between repetitions of the pattern */ /**
* The gap between patterns. Passing in a tuple allows you to control the x and y gap
* independently.
* @default 20
*/
gap?: number | [number, number]; gap?: number | [number, number];
/** Size of a single pattern element */ /**
* The radius of each dot or the size of each rectangle if `BackgroundVariant.Dots` or
* `BackgroundVariant.Cross` is used. This defaults to 1 or 6 respectively, or ignored if
* `BackgroundVariant.Lines` is used.
*/
size?: number; size?: number;
/** Offset of the pattern */ /**
* Offset of the pattern.
* @default 0
*/
offset?: number | [number, number]; offset?: number | [number, number];
/** Line width of the Line pattern */ /**
* The stroke thickness used when drawing the pattern.
* @default 1
*/
lineWidth?: number; lineWidth?: number;
/** Variant of the pattern /**
* Variant of the pattern.
* @default BackgroundVariant.Dots
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross * @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
* 'lines', 'dots', 'cross' * 'lines', 'dots', 'cross'
*/ */
variant?: BackgroundVariant; variant?: BackgroundVariant;
/** Style applied to the container */ /** Style applied to the container. */
style?: CSSProperties; style?: CSSProperties;
}; };
@@ -2,6 +2,29 @@ import cc from 'classcat';
import type { ControlButtonProps } from './types'; import type { ControlButtonProps } from './types';
/**
* You can add buttons to the control panel by using the `<ControlButton />` component
* and pass it as a child to the [`<Controls />`](/api-reference/components/controls) component.
*
* @public
* @example
*```jsx
*import { MagicWand } from '@radix-ui/react-icons'
*import { ReactFlow, Controls, ControlButton } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls>
* <ControlButton onClick={() => alert('Something magical just happened. ✨')}>
* <MagicWand />
* </ControlButton>
* </Controls>
* </ReactFlow>
* )
*}
*```
*/
export function ControlButton({ children, className, ...rest }: ControlButtonProps) { export function ControlButton({ children, className, ...rest }: ControlButtonProps) {
return ( return (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}> <button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>
@@ -125,4 +125,25 @@ function ControlsComponent({
ControlsComponent.displayName = 'Controls'; ControlsComponent.displayName = 'Controls';
/**
* The `<Controls />` component renders a small panel that contains convenient
* buttons to zoom in, zoom out, fit the view, and lock the viewport.
*
* @public
* @example
*```tsx
*import { ReactFlow, Controls } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls />
* </ReactFlow>
* )
*}
*```
*
* @remarks To extend or customise the controls, you can use the [`<ControlButton />`](/api-reference/components/control-button) component
*
*/
export const Controls = memo(ControlsComponent); export const Controls = memo(ControlsComponent);
@@ -3,24 +3,46 @@ import type { PanelPosition } from '@xyflow/system';
import type { FitViewOptions } from '../../types'; import type { FitViewOptions } from '../../types';
/**
* @expand
*/
export type ControlProps = { export type ControlProps = {
/** Show button for zoom in/out */ /**
* Whether or not to show the zoom in and zoom out buttons. These buttons will adjust the viewport
* zoom by a fixed amount each press.
* @default true
*/
showZoom?: boolean; showZoom?: boolean;
/** Show button for fit view */ /**
* Whether or not to show the fit view button. By default, this button will adjust the viewport so
* that all nodes are visible at once.
* @default true
*/
showFitView?: boolean; showFitView?: boolean;
/** Show button for toggling interactivity */ /**
* Show button for toggling interactivity
* @default true
*/
showInteractive?: boolean; showInteractive?: boolean;
/** Options being used when fit view button is clicked */ /**
* Customise the options for the fit view button. These are the same options you would pass to the
* fitView function.
*/
fitViewOptions?: FitViewOptions; fitViewOptions?: FitViewOptions;
/** Callback when zoom in button is clicked */ /** Called in addition the default zoom behavior when the zoom in button is clicked. */
onZoomIn?: () => void; onZoomIn?: () => void;
/** Callback when zoom out button is clicked */ /** Called in addition the default zoom behavior when the zoom out button is clicked. */
onZoomOut?: () => void; onZoomOut?: () => void;
/** Callback when fit view button is clicked */ /**
* Called when the fit view button is clicked. When this is not provided, the viewport will be
* adjusted so that all nodes are visible.
*/
onFitView?: () => void; onFitView?: () => void;
/** Callback when interactivity is toggled */ /** Called when the interactive (lock) button is clicked. */
onInteractiveChange?: (interactiveStatus: boolean) => void; onInteractiveChange?: (interactiveStatus: boolean) => void;
/** Position of the controls on the pane /**
* Position of the controls on the pane
* @default PanelPosition.BottomLeft
* @example PanelPosition.TopLeft, PanelPosition.TopRight, * @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight * PanelPosition.BottomLeft, PanelPosition.BottomRight
*/ */
@@ -28,10 +50,19 @@ export type ControlProps = {
children?: ReactNode; children?: ReactNode;
/** Style applied to container */ /** Style applied to container */
style?: React.CSSProperties; style?: React.CSSProperties;
/** ClassName applied to container */ /** Class name applied to container */
className?: string; className?: string;
/**
* @default 'React Flow controls'
*/
'aria-label'?: string; 'aria-label'?: string;
/**
* @default 'vertical'
*/
orientation?: 'horizontal' | 'vertical'; orientation?: 'horizontal' | 'vertical';
}; };
/**
* @expand
*/
export type ControlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>; export type ControlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
@@ -15,6 +15,8 @@ import type { MiniMapProps } from './types';
const defaultWidth = 200; const defaultWidth = 200;
const defaultHeight = 150; const defaultHeight = 150;
const filterHidden = (node: Node) => !node.hidden;
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
const viewBB: Rect = { const viewBB: Rect = {
x: -s.transform[0] / s.transform[2], x: -s.transform[0] / s.transform[2],
@@ -25,7 +27,10 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, viewBB,
boundingRect: s.nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup), viewBB) : viewBB, boundingRect:
s.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { filter: filterHidden }), viewBB)
: viewBB,
rfId: s.rfId, rfId: s.rfId,
panZoom: s.panZoom, panZoom: s.panZoom,
translateExtent: s.translateExtent, translateExtent: s.translateExtent,
@@ -44,8 +49,10 @@ function MiniMapComponent<NodeType extends Node = Node>({
nodeClassName = '', nodeClassName = '',
nodeBorderRadius = 5, nodeBorderRadius = 5,
nodeStrokeWidth, nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as /*
// a component properly. * We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent, nodeComponent,
bgColor, bgColor,
maskColor, maskColor,
@@ -118,7 +125,7 @@ function MiniMapComponent<NodeType extends Node = Node>({
const onSvgNodeClick = onNodeClick const onSvgNodeClick = onNodeClick
? useCallback((event: MouseEvent, nodeId: string) => { ? useCallback((event: MouseEvent, nodeId: string) => {
const node = store.getState().nodeLookup.get(nodeId)!; const node: NodeType = store.getState().nodeLookup.get(nodeId)!.internals.userNode;
onNodeClick(event, node); onNodeClick(event, node);
}, []) }, [])
: undefined; : undefined;
@@ -136,7 +143,7 @@ function MiniMapComponent<NodeType extends Node = Node>({
typeof maskStrokeWidth === 'number' ? maskStrokeWidth * viewScale : undefined, typeof maskStrokeWidth === 'number' ? maskStrokeWidth * viewScale : undefined,
'--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined, '--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
'--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined, '--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined, '--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'number' ? nodeStrokeWidth : undefined,
} as CSSProperties } as CSSProperties
} }
className={cc(['react-flow__minimap', className])} className={cc(['react-flow__minimap', className])}
@@ -176,4 +183,24 @@ function MiniMapComponent<NodeType extends Node = Node>({
MiniMapComponent.displayName = 'MiniMap'; MiniMapComponent.displayName = 'MiniMap';
/**
* The `<MiniMap />` component can be used to render an overview of your flow. It
* renders each node as an SVG element and visualizes where the current viewport is
* in relation to the rest of the flow.
*
* @public
* @example
*
* ```jsx
*import { ReactFlow, MiniMap } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]]} edges={[...]]}>
* <MiniMap nodeStrokeWidth={3} />
* </ReactFlow>
* );
*}
*```
*/
export const MiniMap = memo(MiniMapComponent) as typeof MiniMapComponent; export const MiniMap = memo(MiniMapComponent) as typeof MiniMapComponent;
@@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { MiniMapNode } from './MiniMapNode'; import { MiniMapNode } from './MiniMapNode';
import type { ReactFlowState, Node, InternalNode } from '../../types'; import type { ReactFlowState, Node } from '../../types';
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types'; import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
declare const window: any; declare const window: any;
@@ -21,8 +21,10 @@ function MiniMapNodes<NodeType extends Node>({
nodeClassName = '', nodeClassName = '',
nodeBorderRadius = 5, nodeBorderRadius = 5,
nodeStrokeWidth, nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as /*
// a component properly. * We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent: NodeComponent = MiniMapNode, nodeComponent: NodeComponent = MiniMapNode,
onClick, onClick,
}: MiniMapNodesProps<NodeType>) { }: MiniMapNodesProps<NodeType>) {
@@ -36,11 +38,13 @@ function MiniMapNodes<NodeType extends Node>({
return ( return (
<> <>
{nodeIds.map((nodeId) => ( {nodeIds.map((nodeId) => (
// The split of responsibilities between MiniMapNodes and /*
// NodeComponentWrapper may appear weird. However, its designed to * The split of responsibilities between MiniMapNodes and
// minimize the cost of updates when individual nodes change. * NodeComponentWrapper may appear weird. However, its designed to
// * minimize the cost of updates when individual nodes change.
// For more details, see a similar commit in `NodeRenderer/index.tsx`. *
* For more details, see a similar commit in `NodeRenderer/index.tsx`.
*/
<NodeComponentWrapper<NodeType> <NodeComponentWrapper<NodeType>
key={nodeId} key={nodeId}
id={nodeId} id={nodeId}
@@ -80,8 +84,9 @@ function NodeComponentWrapperInner<NodeType extends Node>({
shapeRendering: string; shapeRendering: string;
}) { }) {
const { node, x, y, width, height } = useStore((s) => { const { node, x, y, width, height } = useStore((s) => {
const node = s.nodeLookup.get(id) as InternalNode<NodeType>; const { internals } = s.nodeLookup.get(id)!;
const { x, y } = node.internals.positionAbsolute; const node = internals.userNode as NodeType;
const { x, y } = internals.positionAbsolute;
const { width, height } = getNodeDimensions(node); const { width, height } = getNodeDimensions(node);
return { return {
@@ -6,47 +6,97 @@ import type { Node } from '../../types';
export type GetMiniMapNodeAttribute<NodeType extends Node = Node> = (node: NodeType) => string; export type GetMiniMapNodeAttribute<NodeType extends Node = Node> = (node: NodeType) => string;
/**
* @expand
*/
export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & { export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & {
/** Color of nodes on minimap */ /**
* Color of nodes on minimap.
* @default "#e2e2e2"
*/
nodeColor?: string | GetMiniMapNodeAttribute<NodeType>; nodeColor?: string | GetMiniMapNodeAttribute<NodeType>;
/** Stroke color of nodes on minimap */ /**
* Stroke color of nodes on minimap.
* @default "transparent"
*/
nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeType>; nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeType>;
/** ClassName applied to nodes on minimap */ /**
* Class name applied to nodes on minimap.
* @default ""
*/
nodeClassName?: string | GetMiniMapNodeAttribute<NodeType>; nodeClassName?: string | GetMiniMapNodeAttribute<NodeType>;
/** Border radius of nodes on minimap */ /**
* Border radius of nodes on minimap.
* @default 5
*/
nodeBorderRadius?: number; nodeBorderRadius?: number;
/** Stroke width of nodes on minimap */ /**
* Stroke width of nodes on minimap.
* @default 2
*/
nodeStrokeWidth?: number; nodeStrokeWidth?: number;
/** Component used to render nodes on minimap */ /**
* A custom component to render the nodes in the minimap. This component must render an SVG
* element!
*/
nodeComponent?: ComponentType<MiniMapNodeProps>; nodeComponent?: ComponentType<MiniMapNodeProps>;
/** Background color of minimap */ /** Background color of minimap. */
bgColor?: string; bgColor?: string;
/** Color of mask representing viewport */ /**
* The color of the mask that covers the portion of the minimap not currently visible in the
* viewport.
* @default "rgba(240, 240, 240, 0.6)"
*/
maskColor?: string; maskColor?: string;
/** Stroke color of mask representing viewport */ /**
* Stroke color of mask representing viewport.
* @default transparent
*/
maskStrokeColor?: string; maskStrokeColor?: string;
/** Stroke width of mask representing viewport */ /**
* Stroke width of mask representing viewport.
* @default 1
*/
maskStrokeWidth?: number; maskStrokeWidth?: number;
/** Position of minimap on pane /**
* Position of minimap on pane.
* @default PanelPosition.BottomRight
* @example PanelPosition.TopLeft, PanelPosition.TopRight, * @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight * PanelPosition.BottomLeft, PanelPosition.BottomRight
*/ */
position?: PanelPosition; position?: PanelPosition;
/** Callback caled when minimap is clicked*/ /** Callback called when minimap is clicked. */
onClick?: (event: MouseEvent, position: XYPosition) => void; onClick?: (event: MouseEvent, position: XYPosition) => void;
/** Callback called when node on minimap is clicked */ /** Callback called when node on minimap is clicked. */
onNodeClick?: (event: MouseEvent, node: NodeType) => void; onNodeClick?: (event: MouseEvent, node: NodeType) => void;
/** If true, viewport is pannable via mini map component */ /**
* Determines whether you can pan the viewport by dragging inside the minimap.
* @default false
*/
pannable?: boolean; pannable?: boolean;
/** If true, viewport is zoomable via mini map component */ /**
* Determines whether you can zoom the viewport by scrolling inside the minimap.
* @default false
*/
zoomable?: boolean; zoomable?: boolean;
/** The aria-label attribute */ /**
* There is no text inside the minimap for a screen reader to use as an accessible name, so it's
* important we provide one to make the minimap accessible. The default is sufficient, but you may
* want to replace it with something more relevant to your app or product.
* @default "React Flow mini map"
*/
ariaLabel?: string | null; ariaLabel?: string | null;
/** Invert direction when panning the minimap viewport */ /** Invert direction when panning the minimap viewport. */
inversePan?: boolean; inversePan?: boolean;
/** Step size for zooming in/out on minimap */ /**
* Step size for zooming in/out on minimap.
* @default 10
*/
zoomStep?: number; zoomStep?: number;
/** Offset the viewport on the minmap, acts like a padding */ /**
* Offset the viewport on the minimap, acts like a padding.
* @default 5
*/
offsetScale?: number; offsetScale?: number;
}; };
@@ -57,6 +107,12 @@ export type MiniMapNodes<NodeType extends Node = Node> = Pick<
onClick?: (event: MouseEvent, nodeId: string) => void; onClick?: (event: MouseEvent, nodeId: string) => void;
}; };
/**
* The props that are passed to the MiniMapNode component
*
* @public
* @expand
*/
export type MiniMapNodeProps = { export type MiniMapNodeProps = {
id: string; id: string;
x: number; x: number;
@@ -74,8 +74,8 @@ function ResizeControl({
if (node && node.expandParent && node.parentId) { if (node && node.expandParent && node.parentId) {
const origin = node.origin ?? nodeOrigin; const origin = node.origin ?? nodeOrigin;
const width = change.width ?? node.measured.width!; const width = change.width ?? node.measured.width ?? 0;
const height = change.height ?? node.measured.height!; const height = change.height ?? node.measured.height ?? 0;
const child: ParentExpandChild = { const child: ParentExpandChild = {
id: node.id, id: node.id,
@@ -99,8 +99,10 @@ function ResizeControl({
const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin); const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
// when the parent was expanded by the child node, its position will be clamped at /*
// 0,0 when node origin is 0,0 and to width, height if it's 1,1 * when the parent was expanded by the child node, its position will be clamped at
* 0,0 when node origin is 0,0 and to width, height if it's 1,1
*/
nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined; nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined; nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
} }
@@ -140,11 +142,15 @@ function ResizeControl({
triggerNodeChanges(changes); triggerNodeChanges(changes);
}, },
onEnd: () => { onEnd: ({ width, height }) => {
const dimensionChange: NodeDimensionChange = { const dimensionChange: NodeDimensionChange = {
id: id, id: id,
type: 'dimensions', type: 'dimensions',
resizing: false, resizing: false,
dimensions: {
width,
height,
},
}; };
store.getState().triggerNodeChanges([dimensionChange]); store.getState().triggerNodeChanges([dimensionChange]);
}, },
@@ -201,4 +207,9 @@ export function ResizeControlLine(props: ResizeControlLineProps) {
return <ResizeControl {...props} variant={ResizeControlVariant.Line} />; return <ResizeControl {...props} variant={ResizeControlVariant.Line} />;
} }
/**
* To create your own resizing UI, you can use the `NodeResizeControl` component where you can pass children (such as icons).
* @public
*
*/
export const NodeResizeControl = memo(ResizeControl); export const NodeResizeControl = memo(ResizeControl);
@@ -3,6 +3,30 @@ import { ResizeControlVariant, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSI
import { NodeResizeControl } from './NodeResizeControl'; import { NodeResizeControl } from './NodeResizeControl';
import type { NodeResizerProps } from './types'; import type { NodeResizerProps } from './types';
/**
* The `<NodeResizer />` component can be used to add a resize functionality to your
* nodes. It renders draggable controls around the node to resize in all directions.
* @public
*
* @example
*```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeResizer } from '@xyflow/react';
*
*function ResizableNode({ data }) {
* return (
* <>
* <NodeResizer minWidth={100} minHeight={30} />
* <Handle type="target" position={Position.Left} />
* <div style={{ padding: 10 }}>{data.label}</div>
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(ResizableNode);
*```
*/
export function NodeResizer({ export function NodeResizer({
nodeId, nodeId,
isVisible = true, isVisible = true,
@@ -9,43 +9,68 @@ import type {
OnResizeEnd, OnResizeEnd,
} from '@xyflow/system'; } from '@xyflow/system';
/**
* @expand
*/
export type NodeResizerProps = { export type NodeResizerProps = {
/** Id of the node it is resizing /**
* Id of the node it is resizing.
* @remarks optional if used inside custom node * @remarks optional if used inside custom node
*/ */
nodeId?: string; nodeId?: string;
/** Color of the resize handle */ /** Color of the resize handle. */
color?: string; color?: string;
/** ClassName applied to handle */ /** Class name applied to handle. */
handleClassName?: string; handleClassName?: string;
/** Style applied to handle */ /** Style applied to handle. */
handleStyle?: CSSProperties; handleStyle?: CSSProperties;
/** ClassName applied to line */ /** Class name applied to line. */
lineClassName?: string; lineClassName?: string;
/** Style applied to line */ /** Style applied to line. */
lineStyle?: CSSProperties; lineStyle?: CSSProperties;
/** Are the controls visible */ /**
* Are the controls visible.
* @default true
*/
isVisible?: boolean; isVisible?: boolean;
/** Minimum width of node */ /**
* Minimum width of node.
* @default 10
*/
minWidth?: number; minWidth?: number;
/** Minimum height of node */ /**
* Minimum height of node.
* @default 10
*/
minHeight?: number; minHeight?: number;
/** Maximum width of node */ /**
* Maximum width of node.
* @default Number.MAX_VALUE
*/
maxWidth?: number; maxWidth?: number;
/** Maximum height of node */ /**
* Maximum height of node.
* @default Number.MAX_VALUE
*/
maxHeight?: number; maxHeight?: number;
/** Keep aspect ratio when resizing */ /**
* Keep aspect ratio when resizing.
* @default false
*/
keepAspectRatio?: boolean; keepAspectRatio?: boolean;
/** Callback to determine if node should resize */ /** Callback to determine if node should resize. */
shouldResize?: ShouldResize; shouldResize?: ShouldResize;
/** Callback called when resizing starts */ /** Callback called when resizing starts. */
onResizeStart?: OnResizeStart; onResizeStart?: OnResizeStart;
/** Callback called when resizing */ /** Callback called when resizing. */
onResize?: OnResize; onResize?: OnResize;
/** Callback called when resizing ends */ /** Callback called when resizing ends. */
onResizeEnd?: OnResizeEnd; onResizeEnd?: OnResizeEnd;
}; };
/**
* @expand
*/
export type ResizeControlProps = Pick< export type ResizeControlProps = Pick<
NodeResizerProps, NodeResizerProps,
| 'nodeId' | 'nodeId'
@@ -60,12 +85,15 @@ export type ResizeControlProps = Pick<
| 'onResize' | 'onResize'
| 'onResizeEnd' | 'onResizeEnd'
> & { > & {
/** Position of the control /**
* Position of the control.
* @example ControlPosition.TopLeft, ControlPosition.TopRight, * @example ControlPosition.TopLeft, ControlPosition.TopRight,
* ControlPosition.BottomLeft, ControlPosition.BottomRight * ControlPosition.BottomLeft, ControlPosition.BottomRight
*/ */
position?: ControlPosition; position?: ControlPosition;
/** Variant of the control /**
* Variant of the control.
* @default "handle"
* @example ResizeControlVariant.Handle, ResizeControlVariant.Line * @example ResizeControlVariant.Handle, ResizeControlVariant.Line
*/ */
variant?: ResizeControlVariant; variant?: ResizeControlVariant;
@@ -74,6 +102,9 @@ export type ResizeControlProps = Pick<
children?: ReactNode; children?: ReactNode;
}; };
/**
* @expand
*/
export type ResizeControlLineProps = ResizeControlProps & { export type ResizeControlLineProps = ResizeControlProps & {
position?: ControlLinePosition; position?: ControlLinePosition;
}; };
@@ -38,6 +38,41 @@ const storeSelector = (state: ReactFlowState) => ({
selectedNodesCount: state.nodes.filter((node) => node.selected).length, selectedNodesCount: state.nodes.filter((node) => node.selected).length,
}); });
/**
* This component can render a toolbar or tooltip to one side of a custom node. This
* toolbar doesn't scale with the viewport so that the content is always visible.
*
* @public
* @example
* ```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeToolbar } from '@xyflow/react';
*
*function CustomNode({ data }) {
* return (
* <>
* <NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition}>
* <button>delete</button>
* <button>copy</button>
* <button>expand</button>
* </NodeToolbar>
*
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(CustomNode);
*```
* @remarks By default, the toolbar is only visible when a node is selected. If multiple
* nodes are selected it will not be visible to prevent overlapping toolbars or
* clutter. You can override this behavior by setting the `isVisible` prop to `true`.
*/
export function NodeToolbar({ export function NodeToolbar({
nodeId, nodeId,
children, children,
@@ -74,7 +109,7 @@ export function NodeToolbar({
const isActive = const isActive =
typeof isVisible === 'boolean' typeof isVisible === 'boolean'
? isVisible ? isVisible
: nodes.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1; : nodes.size === 1 && nodes.values().next().value?.selected && selectedNodesCount === 1;
if (!isActive || !nodes.size) { if (!isActive || !nodes.size) {
return null; return null;
@@ -1,19 +1,31 @@
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import type { Position, Align } from '@xyflow/system'; import type { Position, Align } from '@xyflow/system';
/**
* @expand
*/
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & { export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
/** Id of the node, or array of ids the toolbar should be displayed at */ /**
* By passing in an array of node id's you can render a single tooltip for a group or collection
* of nodes.
*/
nodeId?: string | string[]; nodeId?: string | string[];
/** If true, node toolbar is visible even if node is not selected */ /** If `true`, node toolbar is visible even if node is not selected. */
isVisible?: boolean; isVisible?: boolean;
/** Position of the toolbar relative to the node /**
* @example Position.TopLeft, Position.TopRight, * Position of the toolbar relative to the node.
* Position.BottomLeft, Position.BottomRight * @default Position.Top
* @example Position.TopLeft, Position.TopRight, Position.BottomLeft, Position.BottomRight
*/ */
position?: Position; position?: Position;
/** Offset the toolbar from the node */ /**
* The space between the node and the toolbar, measured in pixels.
* @default 10
*/
offset?: number; offset?: number;
/** Align the toolbar relative to the node /**
* Align the toolbar relative to the node.
* @default "center"
* @example Align.Start, Align.Center, Align.End * @example Align.Start, Align.Center, Align.End
*/ */
align?: Align; align?: Align;
@@ -28,33 +28,49 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
const store = useStoreApi<NodeType, EdgeType>(); const store = useStoreApi<NodeType, EdgeType>();
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => { const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued } = store.getState();
// This is essentially an `Array.reduce` in imperative clothing. Processing /*
// this queue is a relatively hot path so we'd like to avoid the overhead of * This is essentially an `Array.reduce` in imperative clothing. Processing
// array methods where we can. * this queue is a relatively hot path so we'd like to avoid the overhead of
let next = nodes as NodeType[]; * array methods where we can.
*/
let next = nodes;
for (const payload of queueItems) { for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload; next = typeof payload === 'function' ? payload(next) : payload;
} }
if (hasDefaultNodes) { if (hasDefaultNodes) {
setNodes(next); setNodes(next);
} else if (onNodesChange) { } else {
onNodesChange( // When a controlled flow is used we need to collect the changes
getElementsDiffChanges({ const changes = getElementsDiffChanges({
items: next, items: next,
lookup: nodeLookup, lookup: nodeLookup,
}) as NodeChange<NodeType>[] }) as NodeChange<NodeType>[];
);
// We only want to fire onNodesChange if there are changes to the nodes
if (changes.length > 0) {
onNodesChange?.(changes);
} else if (fitViewQueued) {
// If there are no changes to the nodes, we still need to call setNodes
// to trigger a re-render and fitView.
window.requestAnimationFrame(() => {
const { fitViewQueued, nodes, setNodes } = store.getState();
if (fitViewQueued) {
setNodes(nodes);
}
});
}
} }
}, []); }, []);
const nodeQueue = useQueue<NodeType>(nodeQueueHandler); const nodeQueue = useQueue<NodeType>(nodeQueueHandler);
const edgeQueueHandler = useCallback((queueItems: QueueItem<EdgeType>[]) => { const edgeQueueHandler = useCallback((queueItems: QueueItem<EdgeType>[]) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState(); const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState();
let next = edges as EdgeType[]; let next = edges;
for (const payload of queueItems) { for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload; next = typeof payload === 'function' ? payload(next) : payload;
} }
@@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types';
* @returns a Queue object * @returns a Queue object
*/ */
export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) { export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
// Because we're using a ref above, we need some way to let React know when to /*
// actually process the queue. We increment this number any time we mutate the * Because we're using a ref above, we need some way to let React know when to
// queue, creating a new state to trigger the layout effect below. * actually process the queue. We increment this number any time we mutate the
// Using a boolean dirty flag here instead would lead to issues related to * queue, creating a new state to trigger the layout effect below.
// automatic batching. (https://github.com/xyflow/xyflow/issues/4779) * Using a boolean dirty flag here instead would lead to issues related to
* automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
*/
const [serial, setSerial] = useState(BigInt(0)); const [serial, setSerial] = useState(BigInt(0));
// A reference of all the batched updates to process before the next render. We /*
// want a reference here so multiple synchronous calls to `setNodes` etc can be * A reference of all the batched updates to process before the next render. We
// batched together. * want a reference here so multiple synchronous calls to `setNodes` etc can be
* batched together.
*/
const [queue] = useState(() => createQueue<T>(() => setSerial(n => n + BigInt(1)))); const [queue] = useState(() => createQueue<T>(() => setSerial(n => n + BigInt(1))));
// Layout effects are guaranteed to run before the next render which means we /*
// shouldn't run into any issues with stale state or weird issues that come from * Layout effects are guaranteed to run before the next render which means we
// rendering things one frame later than expected (we used to use `setTimeout`). * shouldn't run into any issues with stale state or weird issues that come from
* rendering things one frame later than expected (we used to use `setTimeout`).
*/
useIsomorphicLayoutEffect(() => { useIsomorphicLayoutEffect(() => {
const queueItems = queue.get(); const queueItems = queue.get();
@@ -11,12 +11,12 @@ import {
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState } from '../../types'; import type { ConnectionLineComponent, Node, ReactFlowState } from '../../types';
import { useConnection } from '../../hooks/useConnection'; import { useConnection } from '../../hooks/useConnection';
type ConnectionLineWrapperProps = { type ConnectionLineWrapperProps<NodeType extends Node = Node> = {
type: ConnectionLineType; type: ConnectionLineType;
component?: ConnectionLineComponent; component?: ConnectionLineComponent<NodeType>;
containerStyle?: CSSProperties; containerStyle?: CSSProperties;
style?: CSSProperties; style?: CSSProperties;
}; };
@@ -29,7 +29,12 @@ const selector = (s: ReactFlowState) => ({
height: s.height, height: s.height,
}); });
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { export function ConnectionLineWrapper<NodeType extends Node = Node>({
containerStyle,
style,
type,
component,
}: ConnectionLineWrapperProps<NodeType>) {
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow); const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const renderConnection = !!(width && nodesConnectable && inProgress); const renderConnection = !!(width && nodesConnectable && inProgress);
@@ -45,21 +50,27 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
className="react-flow__connectionline react-flow__container" className="react-flow__connectionline react-flow__container"
> >
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}> <g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} /> <ConnectionLine<NodeType> style={style} type={type} CustomComponent={component} isValid={isValid} />
</g> </g>
</svg> </svg>
); );
} }
type ConnectionLineProps = { type ConnectionLineProps<NodeType extends Node = Node> = {
type: ConnectionLineType; type: ConnectionLineType;
style?: CSSProperties; style?: CSSProperties;
CustomComponent?: ConnectionLineComponent; CustomComponent?: ConnectionLineComponent<NodeType>;
isValid: boolean | null; isValid: boolean | null;
}; };
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => { const ConnectionLine = <NodeType extends Node = Node>({
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useConnection(); style,
type = ConnectionLineType.Bezier,
CustomComponent,
isValid,
}: ConnectionLineProps<NodeType>) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } =
useConnection<NodeType>();
if (!inProgress) { if (!inProgress) {
return; return;
@@ -6,7 +6,52 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer'); const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer');
export function EdgeLabelRenderer({ children }: { children: ReactNode }) { export type EdgeLabelRendererProps = {
children: ReactNode
}
/**
* Edges are SVG-based. If you want to render more complex labels you can use the
* `<EdgeLabelRenderer />` component to access a div based renderer. This component
* is a portal that renders the label in a `<div />` that is positioned on top of
* the edges. You can see an example usage of the component in the
* [edge label renderer example](/examples/edges/edge-label-renderer).
* @public
*
* @example
* ```jsx
* import React from 'react';
* import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react';
*
* export function CustomEdge({ id, data, ...props }) {
* const [edgePath, labelX, labelY] = getBezierPath(props);
*
* return (
* <>
* <BaseEdge id={id} path={edgePath} />
* <EdgeLabelRenderer>
* <div
* style={{
* position: 'absolute',
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
* background: '#ffcc00',
* padding: 10,
* }}
* className="nodrag nopan"
* >
* {data.label}
* </div>
* </EdgeLabelRenderer>
* </>
* );
* };
* ```
*
* @remarks The `<EdgeLabelRenderer />` has no pointer events by default. If you want to
* add mouse interactions you need to set the style `pointerEvents: all` and add
* the `nopan` class on the label or the element you want to interact with.
*/
export function EdgeLabelRenderer({ children }: EdgeLabelRendererProps) {
const edgeLabelRenderer = useStore(selector); const edgeLabelRenderer = useStore(selector);
if (!edgeLabelRenderer) { if (!edgeLabelRenderer) {
@@ -1,4 +1,4 @@
import { useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react'; import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { import {
@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
const onEdgeDoubleClick = onDoubleClick const onEdgeDoubleClick = onDoubleClick
? (event: React.MouseEvent) => { ? (event: React.MouseEvent) => {
onDoubleClick(event, { ...edge }); onDoubleClick(event, { ...edge });
} }
: undefined; : undefined;
const onEdgeContextMenu = onContextMenu const onEdgeContextMenu = onContextMenu
? (event: React.MouseEvent) => { ? (event: React.MouseEvent) => {
onContextMenu(event, { ...edge }); onContextMenu(event, { ...edge });
} }
: undefined; : undefined;
const onEdgeMouseEnter = onMouseEnter const onEdgeMouseEnter = onMouseEnter
? (event: React.MouseEvent) => { ? (event: React.MouseEvent) => {
onMouseEnter(event, { ...edge }); onMouseEnter(event, { ...edge });
} }
: undefined; : undefined;
const onEdgeMouseMove = onMouseMove const onEdgeMouseMove = onMouseMove
? (event: React.MouseEvent) => { ? (event: React.MouseEvent) => {
onMouseMove(event, { ...edge }); onMouseMove(event, { ...edge });
} }
: undefined; : undefined;
const onEdgeMouseLeave = onMouseLeave const onEdgeMouseLeave = onMouseLeave
? (event: React.MouseEvent) => { ? (event: React.MouseEvent) => {
onMouseLeave(event, { ...edge }); onMouseLeave(event, { ...edge });
} }
: undefined; : undefined;
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
@@ -4,6 +4,33 @@ import cc from 'classcat';
import { EdgeText } from './EdgeText'; import { EdgeText } from './EdgeText';
import type { BaseEdgeProps } from '../../types'; import type { BaseEdgeProps } from '../../types';
/**
* The `<BaseEdge />` component gets used internally for all the edges. It can be
* used inside a custom edge and handles the invisible helper edge and the edge label
* for you.
*
* @public
* @example
* ```jsx
*import { BaseEdge } from '@xyflow/react';
*
*export function CustomEdge({ sourceX, sourceY, targetX, targetY, ...props }) {
* const [edgePath] = getStraightPath({
* sourceX,
* sourceY,
* targetX,
* targetY,
* });
*
* return <BaseEdge path={edgePath} {...props} />;
*}
*```
*
* @remarks If you want to use an edge marker with the [`<BaseEdge />`](/api-reference/components/base-edge) component,
* you can pass the `markerStart` or `markerEnd` props passed to your custom edge
* through to the [`<BaseEdge />`](/api-reference/components/base-edge) component.
* You can see all the props passed to a custom edge by looking at the [`EdgeProps`](/api-reference/types/edge-props) type.
*/
export function BaseEdge({ export function BaseEdge({
path, path,
labelX, labelX,
@@ -61,7 +61,34 @@ function createBezierEdge(params: { isInternal: boolean }) {
); );
} }
/**
* Component that can be used inside a custom edge to render a bezier curve.
*
* @public
* @example
*
* ```tsx
* import { BezierEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <BezierEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const BezierEdge = createBezierEdge({ isInternal: false }); const BezierEdge = createBezierEdge({ isInternal: false });
/**
* @internal
*/
const BezierEdgeInternal = createBezierEdge({ isInternal: true }); const BezierEdgeInternal = createBezierEdge({ isInternal: true });
BezierEdge.displayName = 'BezierEdge'; BezierEdge.displayName = 'BezierEdge';
@@ -27,6 +27,9 @@ export interface EdgeAnchorProps extends SVGAttributes<SVGGElement> {
const EdgeUpdaterClassName = 'react-flow__edgeupdater'; const EdgeUpdaterClassName = 'react-flow__edgeupdater';
/**
* @internal
*/
export function EdgeAnchor({ export function EdgeAnchor({
position, position,
centerX, centerX,
@@ -8,9 +8,9 @@ function EdgeTextComponent({
x, x,
y, y,
label, label,
labelStyle = {}, labelStyle,
labelShowBg = true, labelShowBg = true,
labelBgStyle = {}, labelBgStyle,
labelBgPadding = [2, 4], labelBgPadding = [2, 4],
labelBgBorderRadius = 2, labelBgBorderRadius = 2,
children, children,
@@ -34,7 +34,7 @@ function EdgeTextComponent({
} }
}, [label]); }, [label]);
if (typeof label === 'undefined' || !label) { if (!label) {
return null; return null;
} }
@@ -73,4 +73,30 @@ function EdgeTextComponent({
EdgeTextComponent.displayName = 'EdgeText'; EdgeTextComponent.displayName = 'EdgeText';
/**
* You can use the `<EdgeText />` component as a helper component to display text
* within your custom edges.
*
* @public
*
* @example
* ```jsx
* import { EdgeText } from '@xyflow/react';
*
* export function CustomEdgeLabel({ label }) {
* return (
* <EdgeText
* x={100}
* y={100}
* label={label}
* labelStyle={{ fill: 'white' }}
* labelShowBg
* labelBgStyle={{ fill: 'red' }}
* labelBgPadding={[2, 4]}
* labelBgBorderRadius={2}
* />
* );
* }
*```
*/
export const EdgeText = memo(EdgeTextComponent); export const EdgeText = memo(EdgeTextComponent);
@@ -7,9 +7,11 @@ import type { SimpleBezierEdgeProps } from '../../types';
export interface GetSimpleBezierPathParams { export interface GetSimpleBezierPathParams {
sourceX: number; sourceX: number;
sourceY: number; sourceY: number;
/** @default Position.Bottom */
sourcePosition?: Position; sourcePosition?: Position;
targetX: number; targetX: number;
targetY: number; targetY: number;
/** @default Position.Top */
targetPosition?: Position; targetPosition?: Position;
} }
@@ -29,6 +31,19 @@ function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number]
return [x1, 0.5 * (y1 + y2)]; return [x1, 0.5 * (y1 + y2)];
} }
/**
* The `getSimpleBezierPath` util returns everything you need to render a simple
* bezier edge between two nodes.
* @public
* @returns
* - `path`: the path to use in an SVG `<path>` element.
* - `labelX`: the `x` position you can use to render a label for this edge.
* - `labelY`: the `y` position you can use to render a label for this edge.
* - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
* middle of this path.
* - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
* middle of this path.
*/
export function getSimpleBezierPath({ export function getSimpleBezierPath({
sourceX, sourceX,
sourceY, sourceY,
@@ -80,8 +95,8 @@ function createSimpleBezierEdge(params: { isInternal: boolean }) {
sourceY, sourceY,
targetX, targetX,
targetY, targetY,
sourcePosition = Position.Bottom, sourcePosition,
targetPosition = Position.Top, targetPosition,
label, label,
labelStyle, labelStyle,
labelShowBg, labelShowBg,
@@ -62,7 +62,34 @@ function createSmoothStepEdge(params: { isInternal: boolean }) {
); );
} }
/**
* Component that can be used inside a custom edge to render a smooth step edge.
*
* @public
* @example
*
* ```tsx
* import { SmoothStepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <SmoothStepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false }); const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
/**
* @internal
*/
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true }); const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
SmoothStepEdge.displayName = 'SmoothStepEdge'; SmoothStepEdge.displayName = 'SmoothStepEdge';
@@ -21,7 +21,34 @@ function createStepEdge(params: { isInternal: boolean }) {
}); });
} }
/**
* Component that can be used inside a custom edge to render a step edge.
*
* @public
* @example
*
* ```tsx
* import { StepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <StepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const StepEdge = createStepEdge({ isInternal: false }); const StepEdge = createStepEdge({ isInternal: false });
/**
* @internal
*/
const StepEdgeInternal = createStepEdge({ isInternal: true }); const StepEdgeInternal = createStepEdge({ isInternal: true });
StepEdge.displayName = 'StepEdge'; StepEdge.displayName = 'StepEdge';
@@ -50,7 +50,32 @@ function createStraightEdge(params: { isInternal: boolean }) {
); );
} }
/**
* Component that can be used inside a custom edge to render a straight line.
*
* @public
* @example
*
* ```tsx
* import { StraightEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY }) {
* return (
* <StraightEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* />
* );
* }
* ```
*/
const StraightEdge = createStraightEdge({ isInternal: false }); const StraightEdge = createStraightEdge({ isInternal: false });
/**
* @internal
*/
const StraightEdgeInternal = createStraightEdge({ isInternal: true }); const StraightEdgeInternal = createStraightEdge({ isInternal: true });
StraightEdge.displayName = 'StraightEdge'; StraightEdge.displayName = 'StraightEdge';
+5 -3
View File
@@ -1,6 +1,8 @@
// We distinguish between internal and exported edges /*
// The internal edges are used directly like custom edges and always get an id, source and target props * We distinguish between internal and exported edges
// If you import an edge from the library, the id is optional and source and target are not used at all * The internal edges are used directly like custom edges and always get an id, source and target props
* If you import an edge from the library, the id is optional and source and target are not used at all
*/
export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge'; export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge';
export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge'; export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge';
+46 -15
View File
@@ -28,10 +28,14 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import { type ReactFlowState } from '../../types'; import { type ReactFlowState } from '../../types';
import { fixedForwardRef } from '../../utils'; import { fixedForwardRef } from '../../utils';
export interface HandleProps extends HandlePropsSystem, Omit<HTMLAttributes<HTMLDivElement>, 'id'> { /**
/** Callback called when connection is made */ * @expand
onConnect?: OnConnect; */
} export type HandleProps = HandlePropsSystem &
Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
/** Callback called when connection is made */
onConnect?: OnConnect;
};
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
connectOnClick: s.connectOnClick, connectOnClick: s.connectOnClick,
@@ -42,9 +46,7 @@ const selector = (s: ReactFlowState) => ({
const connectingSelector = const connectingSelector =
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => { (nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state; const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state;
const { fromHandle, toHandle, isValid } = connection; const { fromHandle, toHandle, isValid } = connection;
const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type; const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type;
return { return {
@@ -56,6 +58,7 @@ const connectingSelector =
? fromHandle?.type !== type ? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id, : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
connectionInProcess: !!fromHandle, connectionInProcess: !!fromHandle,
clickConnectionInProcess: !!clickHandle,
valid: connectingTo && isValid, valid: connectingTo && isValid,
}; };
}; };
@@ -83,11 +86,15 @@ function HandleComponent(
const store = useStoreApi(); const store = useStoreApi();
const nodeId = useNodeId(); const nodeId = useNodeId();
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow); const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore( const {
connectingSelector(nodeId, handleId, type), connectingFrom,
shallow connectingTo,
); clickConnecting,
isPossibleEndHandle,
connectionInProcess,
clickConnectionInProcess,
valid,
} = useStore(connectingSelector(nodeId, handleId, type), shallow);
if (!nodeId) { if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']()); store.getState().onError?.('010', errorMessages['error010']());
} }
@@ -228,12 +235,14 @@ function HandleComponent(
connectingfrom: connectingFrom, connectingfrom: connectingFrom,
connectingto: connectingTo, connectingto: connectingTo,
valid, valid,
// shows where you can start a connection from /*
// and where you can end it while connecting * shows where you can start a connection from
* and where you can end it while connecting
*/
connectionindicator: connectionindicator:
isConnectable && isConnectable &&
(!connectionInProcess || isPossibleEndHandle) && (!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart), (connectionInProcess || clickConnectionInProcess ? isConnectableEnd : isConnectableStart),
}, },
])} ])}
onMouseDown={onPointerDown} onMouseDown={onPointerDown}
@@ -248,6 +257,28 @@ function HandleComponent(
} }
/** /**
* The Handle component is a UI element that is used to connect nodes. * The `<Handle />` component is used in your [custom nodes](/learn/customization/custom-nodes)
* to define connection points.
*
*@public
*
*@example
*
*```jsx
*import { Handle, Position } from '@xyflow/react';
*
*export function CustomNode({ data }) {
* return (
* <>
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*```
*/ */
export const Handle = memo(fixedForwardRef(HandleComponent)); export const Handle = memo(fixedForwardRef(HandleComponent));
@@ -37,7 +37,6 @@ export function NodeWrapper<NodeType extends Node>({
disableKeyboardA11y, disableKeyboardA11y,
rfId, rfId,
nodeTypes, nodeTypes,
nodeExtent,
nodeClickDistance, nodeClickDistance,
onError, onError,
}: NodeWrapperProps<NodeType>) { }: NodeWrapperProps<NodeType>) {
@@ -109,8 +108,10 @@ export function NodeWrapper<NodeType extends Node>({
const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) { if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true /*
// here we only need to call it when selectNodesOnDrag=false * this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
* here we only need to call it when selectNodesOnDrag=false
*/
handleNodeClick({ handleNodeClick({
id, id,
store, store,
@@ -49,8 +49,10 @@ export function useNodeObserver({
useEffect(() => { useEffect(() => {
if (nodeRef.current) { if (nodeRef.current) {
// when the user programmatically changes the source or handle position, we need to update the internals /*
// to make sure the edges are updated correctly * when the user programmatically changes the source or handle position, we need to update the internals
* to make sure the edges are updated correctly
*/
const typeChanged = prevType.current !== nodeType; const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition; const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition; const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
@@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = {
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>( export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: InternalNode<NodeType> node: InternalNode<NodeType>
): { ): {
width: number | string | undefined; width: number | string | undefined;
height: number | string | undefined; height: number | string | undefined;
} { } {
if (node.internals.handleBounds === undefined) { if (node.internals.handleBounds === undefined) {
return { return {
width: node.width ?? node.initialWidth ?? node.style?.width, width: node.width ?? node.initialWidth ?? node.style?.width,
+6 -4
View File
@@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system';
import type { ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
// this handler is called by /*
// 1. the click handler when node is not draggable or selectNodesOnDrag = false * this handler is called by
// or * 1. the click handler when node is not draggable or selectNodesOnDrag = false
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true * or
* 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
*/
export function handleNodeClick({ export function handleNodeClick({
id, id,
store, store,
@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
const onContextMenu = onSelectionContextMenu const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => { ? (event: MouseEvent) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected); const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes); onSelectionContextMenu(event, selectedNodes);
} }
: undefined; : undefined;
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
+34 -4
View File
@@ -1,20 +1,48 @@
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'; import { HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import type { PanelPosition } from '@xyflow/system'; import type { PanelPosition } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
/**
* @expand
*/
export type PanelProps = HTMLAttributes<HTMLDivElement> & { export type PanelProps = HTMLAttributes<HTMLDivElement> & {
/** Set position of the panel /**
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' * The position of the panel.
* @default "top-left"
*/ */
position?: PanelPosition; position?: PanelPosition;
children: ReactNode;
}; };
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all'); const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
/**
* The `<Panel />` component helps you position content above the viewport.
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
* and [`<Controls />`](/api-reference/components/controls) components.
*
* @public
*
* @example
* ```jsx
*import { ReactFlow, Background, Panel } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[]} fitView>
* <Panel position="top-left">top-left</Panel>
* <Panel position="top-center">top-center</Panel>
* <Panel position="top-right">top-right</Panel>
* <Panel position="bottom-left">bottom-left</Panel>
* <Panel position="bottom-center">bottom-center</Panel>
* <Panel position="bottom-right">bottom-right</Panel>
* </ReactFlow>
* );
*}
*```
*/
export const Panel = forwardRef<HTMLDivElement, PanelProps>( export const Panel = forwardRef<HTMLDivElement, PanelProps>(
({ position = 'top-left', children, className, style, ...rest }, ref) => { ({ position = 'top-left', children, className, style, ...rest }, ref) => {
const pointerEvents = useStore(selector); const pointerEvents = useStore(selector);
@@ -32,3 +60,5 @@ export const Panel = forwardRef<HTMLDivElement, PanelProps>(
); );
} }
); );
Panel.displayName = 'Panel';
@@ -19,6 +19,40 @@ export type ReactFlowProviderProps = {
children: ReactNode; children: ReactNode;
}; };
/**
* The `<ReactFlowProvider />` component is a [context provider](https://react.dev/learn/passing-data-deeply-with-context#)
* that makes it possible to access a flow's internal state outside of the
* [`<ReactFlow />`](/api-reference/react-flow) component. Many of the hooks we
* provide rely on this component to work.
* @public
*
* @example
* ```tsx
*import { ReactFlow, ReactFlowProvider, useNodes } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlowProvider>
* <ReactFlow nodes={...} edges={...} />
* <Sidebar />
* </ReactFlowProvider>
* );
*}
*
*function Sidebar() {
* // This hook will only work if the component it's used in is a child of a
* // <ReactFlowProvider />.
* const nodes = useNodes()
*
* return <aside>do something with nodes</aside>;
*}
*```
*
* @remarks If you're using a router and want your flow's state to persist across routes,
* it's vital that you place the `<ReactFlowProvider />` component _outside_ of
* your router. If you have multiple flows on the same page you will need to use a separate
* `<ReactFlowProvider />` for each flow.
*/
export function ReactFlowProvider({ export function ReactFlowProvider({
initialNodes: nodes, initialNodes: nodes,
initialEdges: edges, initialEdges: edges,
@@ -10,8 +10,8 @@ import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import type { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types'; import type { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
type SelectionListenerProps = { type SelectionListenerProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
onSelectionChange?: OnSelectionChangeFunc; onSelectionChange?: OnSelectionChangeFunc<NodeType, EdgeType>;
}; };
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
@@ -44,12 +44,14 @@ function areEqual(a: SelectorSlice, b: SelectorSlice) {
); );
} }
function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) { function SelectionListenerInner<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
const store = useStoreApi(); onSelectionChange,
}: SelectionListenerProps<NodeType, EdgeType>) {
const store = useStoreApi<NodeType, EdgeType>();
const { selectedNodes, selectedEdges } = useStore(selector, areEqual); const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
useEffect(() => { useEffect(() => {
const params = { nodes: selectedNodes, edges: selectedEdges }; const params = { nodes: selectedNodes as NodeType[], edges: selectedEdges as EdgeType[] };
onSelectionChange?.(params); onSelectionChange?.(params);
store.getState().onSelectionChangeHandlers.forEach((fn) => fn(params)); store.getState().onSelectionChangeHandlers.forEach((fn) => fn(params));
@@ -60,11 +62,13 @@ function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) {
const changeSelector = (s: ReactFlowState) => !!s.onSelectionChangeHandlers; const changeSelector = (s: ReactFlowState) => !!s.onSelectionChangeHandlers;
export function SelectionListener({ onSelectionChange }: SelectionListenerProps) { export function SelectionListener<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
onSelectionChange,
}: SelectionListenerProps<NodeType, EdgeType>) {
const storeHasSelectionChangeHandlers = useStore(changeSelector); const storeHasSelectionChangeHandlers = useStore(changeSelector);
if (onSelectionChange || storeHasSelectionChangeHandlers) { if (onSelectionChange || storeHasSelectionChangeHandlers) {
return <SelectionListenerInner onSelectionChange={onSelectionChange} />; return <SelectionListenerInner<NodeType, EdgeType> onSelectionChange={onSelectionChange} />;
} }
return null; return null;
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
import { defaultNodeOrigin } from '../../container/ReactFlow/init-values'; import { defaultNodeOrigin } from '../../container/ReactFlow/init-values';
// these fields exist in the global store and we need to keep them up to date // These fields exist in the global store, and we need to keep them up to date
const reactFlowFieldsToTrack = [ const reactFlowFieldsToTrack = [
'nodes', 'nodes',
'edges', 'edges',
@@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({
}); });
const initPrevValues = { const initPrevValues = {
// these are values that are also passed directly to other components /*
// than the StoreUpdater. We can reduce the number of setStore calls * these are values that are also passed directly to other components
// by setting the same values here as prev fields. * than the StoreUpdater. We can reduce the number of setStore calls
* by setting the same values here as prev fields.
*/
translateExtent: infiniteExtent, translateExtent: infiniteExtent,
nodeOrigin: defaultNodeOrigin, nodeOrigin: defaultNodeOrigin,
minZoom: 0.5, minZoom: 0.5,
@@ -152,8 +154,8 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent); else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number); else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
// Renamed fields // Renamed fields
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean }); else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions }); else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
// General case // General case
else store.setState({ [fieldName]: fieldValue }); else store.setState({ [fieldName]: fieldValue });
} }
@@ -6,6 +6,31 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal'); const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal');
/**
* The `<ViewportPortal />` component can be used to add components to the same viewport
* of the flow where nodes and edges are rendered. This is useful when you want to render
* your own components that are adhere to the same coordinate system as the nodes & edges
* and are also affected by zooming and panning
* @public
* @example
*
* ```jsx
*import React from 'react';
*import { ViewportPortal } from '@xyflow/react';
*
*export default function () {
* return (
* <ViewportPortal>
* <div
* style={{ transform: 'translate(100px, 100px)', position: 'absolute' }}
* >
* This div is positioned at [100, 100] on the flow.
* </div>
* </ViewportPortal>
* );
*}
*```
*/
export function ViewportPortal({ children }: { children: ReactNode }) { export function ViewportPortal({ children }: { children: ReactNode }) {
const viewPortalDiv = useStore(selector); const viewPortalDiv = useStore(selector);
@@ -42,9 +42,11 @@ const Marker = ({
); );
}; };
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore /*
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper * when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// that we can then use for creating our unique marker ids * when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
* that we can then use for creating our unique marker ids
*/
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const edges = useStore((s) => s.edges); const edges = useStore((s) => s.edges);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions); const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);
@@ -170,7 +170,7 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
rfId={rfId} rfId={rfId}
/> />
<ConnectionLineWrapper <ConnectionLineWrapper<NodeType>
style={connectionLineStyle} style={connectionLineStyle}
type={connectionLineType} type={connectionLineType}
component={connectionLineComponent} component={connectionLineComponent}
@@ -44,29 +44,31 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
<div className="react-flow__nodes" style={containerStyle}> <div className="react-flow__nodes" style={containerStyle}>
{nodeIds.map((nodeId) => { {nodeIds.map((nodeId) => {
return ( return (
// The split of responsibilities between NodeRenderer and /*
// NodeComponentWrapper may appear weird. However, its designed to * The split of responsibilities between NodeRenderer and
// minimize the cost of updates when individual nodes change. * NodeComponentWrapper may appear weird. However, its designed to
// * minimize the cost of updates when individual nodes change.
// For example, when youre dragging a single node, that node gets *
// updated multiple times per second. If `NodeRenderer` were to update * For example, when youre dragging a single node, that node gets
// every time, it would have to re-run the `nodes.map()` loop every * updated multiple times per second. If `NodeRenderer` were to update
// time. This gets pricey with hundreds of nodes, especially if every * every time, it would have to re-run the `nodes.map()` loop every
// loop cycle does more than just rendering a JSX element! * time. This gets pricey with hundreds of nodes, especially if every
// * loop cycle does more than just rendering a JSX element!
// As a result of this choice, we took the following implementation *
// decisions: * As a result of this choice, we took the following implementation
// - NodeRenderer subscribes *only* to node IDs and therefore * decisions:
// rerender *only* when visible nodes are added or removed. * - NodeRenderer subscribes *only* to node IDs and therefore
// - NodeRenderer performs all operations the result of which can be * rerender *only* when visible nodes are added or removed.
// shared between nodes (such as creating the `ResizeObserver` * - NodeRenderer performs all operations the result of which can be
// instance, or subscribing to `selector`). This means extra prop * shared between nodes (such as creating the `ResizeObserver`
// drilling into `NodeComponentWrapper`, but it means we need to run * instance, or subscribing to `selector`). This means extra prop
// these operations only once instead of once per node. * drilling into `NodeComponentWrapper`, but it means we need to run
// - Any operations that youd normally write inside `nodes.map` are * these operations only once instead of once per node.
// moved into `NodeComponentWrapper`. This ensures they are * - Any operations that youd normally write inside `nodes.map` are
// memorized so if `NodeRenderer` *has* to rerender, it only * moved into `NodeComponentWrapper`. This ensures they are
// needs to regenerate the list of nodes, nothing else. * memorized so if `NodeRenderer` *has* to rerender, it only
* needs to regenerate the list of nodes, nothing else.
*/
<NodeWrapper<NodeType> <NodeWrapper<NodeType>
key={nodeId} key={nodeId}
id={nodeId} id={nodeId}
+12 -6
View File
@@ -61,6 +61,7 @@ const wrapHandler = (
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive, userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
connectionInProgress: s.connection.inProgress,
dragging: s.paneDragging, dragging: s.paneDragging,
}); });
@@ -81,7 +82,7 @@ export function Pane({
children, children,
}: PaneProps) { }: PaneProps) {
const store = useStoreApi(); const store = useStoreApi();
const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow); const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive); const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
const container = useRef<HTMLDivElement | null>(null); const container = useRef<HTMLDivElement | null>(null);
@@ -96,7 +97,8 @@ 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 // We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress.current) { // We also prevent click events when a connection is in progress
if (selectionInProgress.current || connectionInProgress) {
selectionInProgress.current = false; selectionInProgress.current = false;
return; return;
} }
@@ -233,8 +235,10 @@ export function Pane({
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId); (event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
const { userSelectionRect } = store.getState(); const { userSelectionRect } = store.getState();
// We only want to trigger click functions when in selection mode if /*
// the user did not move the mouse. * We only want to trigger click functions when in selection mode if
* the user did not move the mouse.
*/
if (!userSelectionActive && userSelectionRect && event.target === container.current) { if (!userSelectionActive && userSelectionRect && event.target === container.current) {
onClick?.(event); onClick?.(event);
} }
@@ -246,8 +250,10 @@ export function Pane({
}); });
onSelectionEnd?.(event); 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 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 || selectionOnDrag) { if (selectionKeyPressed || selectionOnDrag) {
selectionInProgress.current = false; selectionInProgress.current = false;
} }
@@ -31,8 +31,10 @@ export function Wrapper({
const isWrapped = useContext(StoreContext); const isWrapped = useContext(StoreContext);
if (isWrapped) { if (isWrapped) {
// we need to wrap it with a fragment because it's not allowed for children to be a ReactNode /*
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 * we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
*/
return <>{children}</>; return <>{children}</>;
} }
@@ -1,4 +1,4 @@
import { ForwardedRef, type CSSProperties } from 'react'; import { ForwardedRef, useCallback, type CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { ConnectionLineType, PanOnScrollMode, SelectionMode, infiniteExtent, isMacOs } from '@xyflow/system'; import { ConnectionLineType, PanOnScrollMode, SelectionMode, infiniteExtent, isMacOs } from '@xyflow/system';
@@ -144,6 +144,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
height, height,
colorMode = 'light', colorMode = 'light',
debug, debug,
onScroll,
...rest ...rest
}: ReactFlowProps<NodeType, EdgeType>, }: ReactFlowProps<NodeType, EdgeType>,
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>
@@ -151,10 +152,20 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
const rfId = id || '1'; const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode); const colorModeClassName = useColorModeClass(colorMode);
// Undo scroll events, preventing viewport from shifting when nodes outside of it are focused
const wrapperOnScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>) => {
e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'instant' });
onScroll?.(e);
},
[onScroll]
);
return ( return (
<div <div
data-testid="rf__wrapper" data-testid="rf__wrapper"
{...rest} {...rest}
onScroll={wrapperOnScroll}
style={{ ...style, ...wrapperStyle }} style={{ ...style, ...wrapperStyle }}
ref={ref} ref={ref}
className={cc(['react-flow', className, colorModeClassName])} className={cc(['react-flow', className, colorModeClassName])}
@@ -292,7 +303,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
paneClickDistance={paneClickDistance} paneClickDistance={paneClickDistance}
debug={debug} debug={debug}
/> />
<SelectionListener onSelectionChange={onSelectionChange} /> <SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} />
{children} {children}
<Attribution proOptions={proOptions} position={attributionPosition} /> <Attribution proOptions={proOptions} position={attributionPosition} />
<A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} /> <A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} />
@@ -301,4 +312,24 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
); );
} }
/**
* The `<ReactFlow />` component is the heart of your React Flow application.
* It renders your nodes and edges and handles user interaction
*
* @public
*
* @example
* ```tsx
*import { ReactFlow } from '@xyflow/react'
*
*export default function Flow() {
* return (<ReactFlow
* nodes={...}
* edges={...}
* onNodesChange={...}
* ...
* />);
*}
*```
*/
export default fixedForwardRef(ReactFlow); export default fixedForwardRef(ReactFlow);
@@ -95,7 +95,7 @@ export function ZoomPane({
}, },
}); });
const { x, y, zoom } = panZoom.current!.getViewport(); const { x, y, zoom } = panZoom.current.getViewport();
store.setState({ store.setState({
panZoom: panZoom.current, panZoom: panZoom.current,
@@ -4,6 +4,34 @@ export const NodeIdContext = createContext<string | null>(null);
export const Provider = NodeIdContext.Provider; export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer; export const Consumer = NodeIdContext.Consumer;
/**
* You can use this hook to get the id of the node it is used inside. It is useful
* if you need the node's id deeper in the render tree but don't want to manually
* drill down the id as a prop.
*
* @public
* @returns The id for a node in the flow.
*
* @example
*```jsx
*import { useNodeId } from '@xyflow/react';
*
*export default function CustomNode() {
* return (
* <div>
* <span>This node has an id of </span>
* <NodeIdDisplay />
* </div>
* );
*}
*
*function NodeIdDisplay() {
* const nodeId = useNodeId();
*
* return <span>{nodeId}</span>;
*}
*```
*/
export const useNodeId = (): string | null => { export const useNodeId = (): string | null => {
const nodeId = useContext(NodeIdContext); const nodeId = useContext(NodeIdContext);
return nodeId; return nodeId;
+24 -1
View File
@@ -24,9 +24,32 @@ function getSelector<NodeType extends Node = Node, SelectorReturn = ConnectionSt
return storeSelector; return storeSelector;
} }
/** /**
* Hook for accessing the connection state. * The `useConnection` hook returns the current connection when there is an active
* connection interaction. If no connection interaction is active, it returns null
* for every property. A typical use case for this hook is to colorize handles
* based on a certain condition (e.g. if the connection is valid or not).
* *
* @public * @public
* @param connectionSelector - An optional selector function used to extract a slice of the
* `ConnectionState` data. Using a selector can prevent component re-renders where data you don't
* otherwise care about might change. If a selector is not provided, the entire `ConnectionState`
* object is returned unchanged.
* @example
*
* ```tsx
*import { useConnection } from '@xyflow/react';
*
*function App() {
* const connection = useConnection();
*
* return (
* <div> {connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'}
*
* </div>
* );
* }
* ```
*
* @returns ConnectionState * @returns ConnectionState
*/ */
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>( export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(
+14 -2
View File
@@ -6,10 +6,22 @@ import type { Edge, ReactFlowState } from '../types';
const edgesSelector = (state: ReactFlowState) => state.edges; const edgesSelector = (state: ReactFlowState) => state.edges;
/** /**
* Hook for getting the current edges from the store. * This hook returns an array of the current edges. Components that use this hook
* will re-render **whenever any edge changes**.
* *
* @public * @public
* @returns An array of edges * @returns An array of all edges currently in the flow.
*
* @example
* ```tsx
*import { useEdges } from '@xyflow/react';
*
*export default function () {
* const edges = useEdges();
*
* return <div>There are currently {edges.length} edges!</div>;
*}
*```
*/ */
export function useEdges<EdgeType extends Edge = Edge>(): EdgeType[] { export function useEdges<EdgeType extends Edge = Edge>(): EdgeType[] {
const edges = useStore(edgesSelector, shallow) as EdgeType[]; const edges = useStore(edgesSelector, shallow) as EdgeType[];
@@ -10,11 +10,16 @@ import {
import { useStore } from './useStore'; import { useStore } from './useStore';
import { useNodeId } from '../contexts/NodeIdContext'; import { useNodeId } from '../contexts/NodeIdContext';
type useHandleConnectionsParams = { type UseHandleConnectionsParams = {
/** What type of handle connections do you want to observe? */
type: HandleType; type: HandleType;
/** The handle id (this is only needed if the node has multiple handles of the same type). */
id?: string | null; id?: string | null;
/** If node id is not provided, the node id from the `NodeIdContext` is used. */
nodeId?: string; nodeId?: string;
/** Gets called when a connection is established. */
onConnect?: (connections: Connection[]) => void; onConnect?: (connections: Connection[]) => void;
/** Gets called when a connection is removed. */
onDisconnect?: (connections: Connection[]) => void; onDisconnect?: (connections: Connection[]) => void;
}; };
@@ -23,12 +28,7 @@ type useHandleConnectionsParams = {
* *
* @public * @public
* @deprecated Use `useNodeConnections` instead. * @deprecated Use `useNodeConnections` instead.
* @param param.type - handle type 'source' or 'target' * @returns An array with handle connections.
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with handle connections
*/ */
export function useHandleConnections({ export function useHandleConnections({
type, type,
@@ -36,7 +36,7 @@ export function useHandleConnections({
nodeId, nodeId,
onConnect, onConnect,
onDisconnect, onDisconnect,
}: useHandleConnectionsParams): HandleConnection[] { }: UseHandleConnectionsParams): HandleConnection[] {
console.warn( console.warn(
'[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections' '[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections'
); );
+23 -3
View File
@@ -5,11 +5,31 @@ import { useStore } from './useStore';
import type { InternalNode, Node } from '../types'; import type { InternalNode, Node } from '../types';
/** /**
* Hook for getting an internal node by id * This hook returns the internal representation of a specific node.
* Components that use this hook will re-render **whenever the node changes**,
* including when a node is selected or moved.
* *
* @public * @public
* @param id - id of the node * @param id - The ID of a node you want to observe.
* @returns array with visible node ids * @returns The `InternalNode` object for the node with the given ID.
*
* @example
* ```tsx
*import { useInternalNode } from '@xyflow/react';
*
*export default function () {
* const internalNode = useInternalNode('node-1');
* const absolutePosition = internalNode.internals.positionAbsolute;
*
* return (
* <div>
* The absolute position of the node is at:
* <p>x: {absolutePosition.x}</p>
* <p>y: {absolutePosition.y}</p>
* </div>
* );
*}
*```
*/ */
export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined { export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
const node = useStore( const node = useStore(
+71 -31
View File
@@ -6,25 +6,58 @@ type PressedKeys = Set<string>;
type KeyOrCode = 'key' | 'code'; type KeyOrCode = 'key' | 'code';
export type UseKeyPressOptions = { export type UseKeyPressOptions = {
/**
* Listen to key presses on a specific element.
* @default document
*/
target?: Window | Document | HTMLElement | ShadowRoot | null; target?: Window | Document | HTMLElement | ShadowRoot | null;
/**
* You can use this flag to prevent triggering the key press hook when an input field is focused.
* @default true
*/
actInsideInputWithModifier?: boolean; actInsideInputWithModifier?: boolean;
preventDefault?: boolean;
}; };
const defaultDoc = typeof document !== 'undefined' ? document : null; const defaultDoc = typeof document !== 'undefined' ? document : null;
/** /**
* Hook for handling key events. * This hook lets you listen for specific key codes and tells you whether they are
* currently pressed or not.
* *
* @public * @public
* @param param.keyCode - The key code (string or array of strings) to use * @param options - Options
* @param param.options - Options *
* @returns boolean * @example
* ```tsx
*import { useKeyPress } from '@xyflow/react';
*
*export default function () {
* const spacePressed = useKeyPress('Space');
* const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']);
*
* return (
* <div>
* {spacePressed && <p>Space pressed!</p>}
* {cmdAndSPressed && <p>Cmd + S pressed!</p>}
* </div>
* );
*}
*```
*/ */
export function useKeyPress( export function useKeyPress(
// the keycode can be a string 'a' or an array of strings ['a', 'a+d'] /**
// a string means a single key 'a' or a combination when '+' is used 'a+d' * The key code (string or array of strings) specifies which key(s) should trigger
// an array means different possibilites. Explainer: ['a', 'd+s'] here the * an action.
// user can use the single key 'a' or the combination 'd' + 's' *
* A **string** can represent:
* - A **single key**, e.g. `'a'`
* - A **key combination**, using `'+'` to separate keys, e.g. `'a+d'`
*
* An **array of strings** represents **multiple possible key inputs**. For example, `['a', 'd+s']`
* means the user can press either the single key `'a'` or the combination of `'d'` and `'s'`.
* @default null
*/
keyCode: KeyCode | null = null, keyCode: KeyCode | null = null,
options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true } options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true }
): boolean { ): boolean {
@@ -36,20 +69,24 @@ export function useKeyPress(
// we need to remember the pressed keys in order to support combinations // we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef<PressedKeys>(new Set([])); const pressedKeys = useRef<PressedKeys>(new Set([]));
// keyCodes = array with single keys [['a']] or key combinations [['a', 's']] /*
// keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft'] * keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
// used to check if we store event.code or event.key. When the code is in the list of keysToWatch * keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
// we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft" * used to check if we store event.code or event.key. When the code is in the list of keysToWatch
// and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when * we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
// we can't find it in the list of keysToWatch. * and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
* we can't find it in the list of keysToWatch.
*/
const [keyCodes, keysToWatch] = useMemo<[Array<Keys>, Keys]>(() => { const [keyCodes, keysToWatch] = useMemo<[Array<Keys>, Keys]>(() => {
if (keyCode !== null) { if (keyCode !== null) {
const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode]; const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode];
const keys = keyCodeArr const keys = keyCodeArr
.filter((kc) => typeof kc === 'string') .filter((kc) => typeof kc === 'string')
// we first replace all '+' with '\n' which we will use to split the keys on /*
// then we replace '\n\n' with '\n+', this way we can also support the combination 'key++' * we first replace all '+' with '\n' which we will use to split the keys on
// in the end we simply split on '\n' to get the key array * then we replace '\n\n' with '\n+', this way we can also support the combination 'key++'
* in the end we simply split on '\n' to get the key array
*/
.map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n')); .map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n'));
const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []); const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []);
@@ -64,7 +101,7 @@ export function useKeyPress(
if (keyCode !== null) { if (keyCode !== null) {
const downHandler = (event: KeyboardEvent) => { const downHandler = (event: KeyboardEvent) => {
modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey; modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
const preventAction = const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) && (!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event); isInputDOMNode(event);
@@ -76,19 +113,18 @@ export function useKeyPress(
pressedKeys.current.add(event[keyOrCode]); pressedKeys.current.add(event[keyOrCode]);
if (isMatchingKey(keyCodes, pressedKeys.current, false)) { if (isMatchingKey(keyCodes, pressedKeys.current, false)) {
event.preventDefault(); const target = (event.composedPath?.()?.[0] || event.target) as Element | null;
const isInteractiveElement = target?.nodeName === 'BUTTON' || target?.nodeName === 'A';
if (options.preventDefault !== false && (modifierPressed.current || !isInteractiveElement)) {
event.preventDefault();
}
setKeyPressed(true); setKeyPressed(true);
} }
}; };
const upHandler = (event: KeyboardEvent) => { const upHandler = (event: KeyboardEvent) => {
const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event);
if (preventAction) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch); const keyOrCode = useKeyOrCode(event.code, keysToWatch);
if (isMatchingKey(keyCodes, pressedKeys.current, true)) { if (isMatchingKey(keyCodes, pressedKeys.current, true)) {
@@ -133,12 +169,16 @@ export function useKeyPress(
function isMatchingKey(keyCodes: Array<Keys>, pressedKeys: PressedKeys, isUp: boolean): boolean { function isMatchingKey(keyCodes: Array<Keys>, pressedKeys: PressedKeys, isUp: boolean): boolean {
return ( return (
keyCodes keyCodes
// we only want to compare same sizes of keyCode definitions /*
// and pressed keys. When the user specified 'Meta' as a key somewhere * we only want to compare same sizes of keyCode definitions
// this would also be truthy without this filter when user presses 'Meta' + 'r' * and pressed keys. When the user specified 'Meta' as a key somewhere
* this would also be truthy without this filter when user presses 'Meta' + 'r'
*/
.filter((keys) => isUp || keys.length === pressedKeys.size) .filter((keys) => isUp || keys.length === pressedKeys.size)
// since we want to support multiple possibilities only one of the /*
// combinations need to be part of the pressed keys * since we want to support multiple possibilities only one of the
* combinations need to be part of the pressed keys
*/
.some((keys) => keys.every((k) => pressedKeys.has(k))) .some((keys) => keys.every((k) => pressedKeys.has(k)))
); );
} }
@@ -22,8 +22,10 @@ export function useMoveSelectedNodes() {
const nodeUpdates = new Map(); const nodeUpdates = new Map();
const isSelected = selectedAndDraggable(nodesDraggable); const isSelected = selectedAndDraggable(nodesDraggable);
// by default a node moves 5px on each key press /*
// if snap grid is enabled, we use that for the velocity * by default a node moves 5px on each key press
* if snap grid is enabled, we use that for the velocity
*/
const xVelo = snapToGrid ? snapGrid[0] : 5; const xVelo = snapToGrid ? snapGrid[0] : 5;
const yVelo = snapToGrid ? snapGrid[1] : 5; const yVelo = snapToGrid ? snapGrid[1] : 5;
+24 -8
View File
@@ -14,23 +14,39 @@ import { useNodeId } from '../contexts/NodeIdContext';
const error014 = errorMessages['error014'](); const error014 = errorMessages['error014']();
type UseNodeConnectionsParams = { type UseNodeConnectionsParams = {
/** ID of the node, filled in automatically if used inside custom node. */
id?: string; id?: string;
/** What type of handle connections do you want to observe? */
handleType?: HandleType; handleType?: HandleType;
/** Filter by handle id (this is only needed if the node has multiple handles of the same type). */
handleId?: string; handleId?: string;
/** Gets called when a connection is established. */
onConnect?: (connections: Connection[]) => void; onConnect?: (connections: Connection[]) => void;
/** Gets called when a connection is removed. */
onDisconnect?: (connections: Connection[]) => void; onDisconnect?: (connections: Connection[]) => void;
}; };
/** /**
* Hook to retrieve all edges connected to a node. Can be filtered by handle type and id. * This hook returns an array of connections on a specific node, handle type ('source', 'target') or handle ID.
* *
* @public * @public
* @param param.id - node id - optional if called inside a custom node * @returns An array with connections.
* @param param.handleType - filter by handle type 'source' or 'target' *
* @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type) * @example
* @param param.onConnect - gets called when a connection is established * ```jsx
* @param param.onDisconnect - gets called when a connection is removed *import { useNodeConnections } from '@xyflow/react';
* @returns an array with connections *
*export default function () {
* const connections = useNodeConnections({
* handleType: 'target',
* handleId: 'my-handle',
* });
*
* return (
* <div>There are currently {connections.length} incoming connections!</div>
* );
*}
*```
*/ */
export function useNodeConnections({ export function useNodeConnections({
id, id,
@@ -57,7 +73,7 @@ export function useNodeConnections({
); );
useEffect(() => { useEffect(() => {
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts // @todo discuss if onConnect/onDisconnect should be called when the component mounts/unmounts
if (prevConnections.current && prevConnections.current !== connections) { if (prevConnections.current && prevConnections.current !== connections) {
const _connections = connections ?? new Map(); const _connections = connections ?? new Map();
handleConnectionChange(prevConnections.current, _connections, onDisconnect); handleConnectionChange(prevConnections.current, _connections, onDisconnect);
+15 -2
View File
@@ -6,10 +6,23 @@ import type { Node, ReactFlowState } from '../types';
const nodesSelector = (state: ReactFlowState) => state.nodes; const nodesSelector = (state: ReactFlowState) => state.nodes;
/** /**
* Hook for getting the current nodes from the store. * This hook returns an array of the current nodes. Components that use this hook
* will re-render **whenever any node changes**, including when a node is selected
* or moved.
* *
* @public * @public
* @returns An array of nodes * @returns An array of all nodes currently in the flow.
*
* @example
* ```jsx
*import { useNodes } from '@xyflow/react';
*
*export default function() {
* const nodes = useNodes();
*
* return <div>There are currently {nodes.length} nodes!</div>;
*}
*```
*/ */
export function useNodes<NodeType extends Node = Node>(): NodeType[] { export function useNodes<NodeType extends Node = Node>(): NodeType[] {
const nodes = useStore(nodesSelector, shallow) as NodeType[]; const nodes = useStore(nodesSelector, shallow) as NodeType[];
+19 -5
View File
@@ -5,17 +5,31 @@ import { useStore } from '../hooks/useStore';
import type { Node } from '../types'; import type { Node } from '../types';
/** /**
* Hook for receiving data of one or multiple nodes * This hook lets you subscribe to changes of a specific nodes `data` object.
* *
* @public * @public
* @param nodeId - The id (or ids) of the node to get the data from * @returns An object (or array of object) with `id`, `type`, `data` representing each node.
* @param guard - Optional guard function to narrow down the node type *
* @returns An object (or array of object) with {id, type, data} representing each node * @example
*```jsx
*import { useNodesData } from '@xyflow/react';
*
*export default function() {
* const nodeData = useNodesData('nodeId-1');
* const nodesData = useNodesData(['nodeId-1', 'nodeId-2']);
*
* return null;
*}
*```
*/ */
export function useNodesData<NodeType extends Node = Node>( export function useNodesData<NodeType extends Node = Node>(
/** The id of the node to get the data from. */
nodeId: string nodeId: string
): Pick<NodeType, 'id' | 'type' | 'data'> | null; ): Pick<NodeType, 'id' | 'type' | 'data'> | null;
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): Pick<NodeType, 'id' | 'type' | 'data'>[]; export function useNodesData<NodeType extends Node = Node>(
/** The ids of the nodes to get the data from. */
nodeIds: string[]
): Pick<NodeType, 'id' | 'type' | 'data'>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any { export function useNodesData(nodeIds: any): any {
const nodesData = useStore( const nodesData = useStore(
+96 -8
View File
@@ -4,15 +4,58 @@ import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types'; import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types';
/** /**
* Hook for managing the state of nodes - should only be used for prototyping / simple use cases. * This hook makes it easy to prototype a controlled flow where you manage the
* state of nodes and edges outside the `ReactFlowInstance`. You can think of it
* like React's `useState` hook with an additional helper callback.
* *
* @public * @public
* @param initialNodes * @returns
* @returns an array [nodes, setNodes, onNodesChange] * - `nodes`: The current array of nodes. You might pass this directly to the `nodes` prop of your
* `<ReactFlow />` component, or you may want to manipulate it first to perform some layouting,
* for example.
* - `setNodes`: A function that you can use to update the nodes. You can pass it a new array of
* nodes or a callback that receives the current array of nodes and returns a new array of nodes.
* This is the same as the second element of the tuple returned by React's `useState` hook.
* - `onNodesChange`: A handy callback that can take an array of `NodeChanges` and update the nodes
* state accordingly. You'll typically pass this directly to the `onNodesChange` prop of your
* `<ReactFlow />` component.
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @remarks This hook was created to make prototyping easier and our documentation
* examples clearer. Although it is OK to use this hook in production, in
* practice you may want to use a more sophisticated state management solution
* like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/ */
export function useNodesState<NodeType extends Node>( export function useNodesState<NodeType extends Node>(
initialNodes: NodeType[] initialNodes: NodeType[]
): [NodeType[], Dispatch<SetStateAction<NodeType[]>>, OnNodesChange<NodeType>] { ): [
//
nodes: NodeType[],
setNodes: Dispatch<SetStateAction<NodeType[]>>,
onNodesChange: OnNodesChange<NodeType>
] {
const [nodes, setNodes] = useState(initialNodes); const [nodes, setNodes] = useState(initialNodes);
const onNodesChange: OnNodesChange<NodeType> = useCallback( const onNodesChange: OnNodesChange<NodeType> = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)), (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
@@ -23,15 +66,60 @@ export function useNodesState<NodeType extends Node>(
} }
/** /**
* Hook for managing the state of edges - should only be used for prototyping / simple use cases. * This hook makes it easy to prototype a controlled flow where you manage the
* state of nodes and edges outside the `ReactFlowInstance`. You can think of it
* like React's `useState` hook with an additional helper callback.
* *
* @public * @public
* @param initialEdges * @returns
* @returns an array [edges, setEdges, onEdgesChange] * - `edges`: The current array of edges. You might pass this directly to the `edges` prop of your
* `<ReactFlow />` component, or you may want to manipulate it first to perform some layouting,
* for example.
*
* - `setEdges`: A function that you can use to update the edges. You can pass it a new array of
* edges or a callback that receives the current array of edges and returns a new array of edges.
* This is the same as the second element of the tuple returned by React's `useState` hook.
*
* - `onEdgesChange`: A handy callback that can take an array of `EdgeChanges` and update the edges
* state accordingly. You'll typically pass this directly to the `onEdgesChange` prop of your
* `<ReactFlow />` component.
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @remarks This hook was created to make prototyping easier and our documentation
* examples clearer. Although it is OK to use this hook in production, in
* practice you may want to use a more sophisticated state management solution
* like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/ */
export function useEdgesState<EdgeType extends Edge = Edge>( export function useEdgesState<EdgeType extends Edge = Edge>(
initialEdges: EdgeType[] initialEdges: EdgeType[]
): [EdgeType[], Dispatch<SetStateAction<EdgeType[]>>, OnEdgesChange<EdgeType>] { ): [
//
edges: EdgeType[],
setEdges: Dispatch<SetStateAction<EdgeType[]>>,
onEdgesChange: OnEdgesChange<EdgeType>
] {
const [edges, setEdges] = useState(initialEdges); const [edges, setEdges] = useState(initialEdges);
const onEdgesChange: OnEdgesChange<EdgeType> = useCallback( const onEdgesChange: OnEdgesChange<EdgeType> = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
+38 -10
View File
@@ -1,8 +1,10 @@
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
import { nodeHasDimensions } from '@xyflow/system'; import { nodeHasDimensions } from '@xyflow/system';
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
export type UseNodesInitializedOptions = { export type UseNodesInitializedOptions = {
/** @default false */
includeHiddenNodes?: boolean; includeHiddenNodes?: boolean;
}; };
@@ -22,18 +24,44 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) =>
return true; return true;
}; };
const defaultOptions = {
includeHiddenNodes: false,
};
/** /**
* Hook which returns true when all nodes are initialized. * This hook tells you whether all the nodes in a flow have been measured and given
*a width and height. When you add a node to the flow, this hook will return
*`false` and then `true` again once the node has been measured.
* *
* @public * @public
* @param options.includeHiddenNodes - defaults to false * @returns Whether or not the nodes have been initialized by the `<ReactFlow />` component and
* @returns boolean indicating whether all nodes are initialized * given a width and height.
*
* @example
* ```jsx
*import { useReactFlow, useNodesInitialized } from '@xyflow/react';
*import { useEffect, useState } from 'react';
*
*const options = {
* includeHiddenNodes: false,
*};
*
*export default function useLayout() {
* const { getNodes } = useReactFlow();
* const nodesInitialized = useNodesInitialized(options);
* const [layoutedNodes, setLayoutedNodes] = useState(getNodes());
*
* useEffect(() => {
* if (nodesInitialized) {
* setLayoutedNodes(yourLayoutingFunction(getNodes()));
* }
* }, [nodesInitialized]);
*
* return layoutedNodes;
*}
*```
*/ */
export function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean { export function useNodesInitialized(
options: UseNodesInitializedOptions = {
includeHiddenNodes: false,
}
): boolean {
const initialized = useStore(selector(options)); const initialized = useStore(selector(options));
return initialized; return initialized;
@@ -1,20 +1,53 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useStoreApi } from './useStore'; import { useStoreApi } from './useStore';
import type { OnSelectionChangeFunc } from '../types'; import type { OnSelectionChangeFunc, Node, Edge } from '../types';
export type UseOnSelectionChangeOptions = { export type UseOnSelectionChangeOptions<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
onChange: OnSelectionChangeFunc; /** The handler to register. */
onChange: OnSelectionChangeFunc<NodeType, EdgeType>;
}; };
/** /**
* Hook for registering an onSelectionChange handler. * This hook lets you listen for changes to both node and edge selection. As the
*name implies, the callback you provide will be called whenever the selection of
*_either_ nodes or edges changes.
* *
* @public * @public
* @param params.onChange - The handler to register * @example
* ```jsx
*import { useState } from 'react';
*import { ReactFlow, useOnSelectionChange } from '@xyflow/react';
*
*function SelectionDisplay() {
* const [selectedNodes, setSelectedNodes] = useState([]);
* const [selectedEdges, setSelectedEdges] = useState([]);
*
* // the passed handler has to be memoized, otherwise the hook will not work correctly
* const onChange = useCallback(({ nodes, edges }) => {
* setSelectedNodes(nodes.map((node) => node.id));
* setSelectedEdges(edges.map((edge) => edge.id));
* }, []);
*
* useOnSelectionChange({
* onChange,
* });
*
* return (
* <div>
* <p>Selected nodes: {selectedNodes.join(', ')}</p>
* <p>Selected edges: {selectedEdges.join(', ')}</p>
* </div>
* );
*}
*```
*
* @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly.
*/ */
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { export function useOnSelectionChange<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
const store = useStoreApi(); onChange,
}: UseOnSelectionChangeOptions<NodeType, EdgeType>) {
const store = useStoreApi<NodeType, EdgeType>();
useEffect(() => { useEffect(() => {
const nextOnSelectionChangeHandlers = [...store.getState().onSelectionChangeHandlers, onChange]; const nextOnSelectionChangeHandlers = [...store.getState().onSelectionChangeHandlers, onChange];
@@ -4,18 +4,35 @@ import type { OnViewportChange } from '@xyflow/system';
import { useStoreApi } from './useStore'; import { useStoreApi } from './useStore';
export type UseOnViewportChangeOptions = { export type UseOnViewportChangeOptions = {
/** Gets called when the viewport starts changing. */
onStart?: OnViewportChange; onStart?: OnViewportChange;
/** Gets called when the viewport changes. */
onChange?: OnViewportChange; onChange?: OnViewportChange;
/** Gets called when the viewport stops changing. */
onEnd?: OnViewportChange; onEnd?: OnViewportChange;
}; };
/** /**
* Hook for registering an onViewportChange handler. * The `useOnViewportChange` hook lets you listen for changes to the viewport such
* as panning and zooming. You can provide a callback for each phase of a viewport
* change: `onStart`, `onChange`, and `onEnd`.
* *
* @public * @public
* @param params.onStart - gets called when the viewport starts changing * @example
* @param params.onChange - gets called when the viewport changes * ```jsx
* @param params.onEnd - gets called when the viewport stops changing *import { useCallback } from 'react';
*import { useOnViewportChange } from '@xyflow/react';
*
*function ViewportChangeLogger() {
* useOnViewportChange({
* onStart: (viewport: Viewport) => console.log('start', viewport),
* onChange: (viewport: Viewport) => console.log('change', viewport),
* onEnd: (viewport: Viewport) => console.log('end', viewport),
* });
*
* return null;
*}
*```
*/ */
export function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) { export function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) {
const store = useStoreApi(); const store = useStoreApi();
+46 -6
View File
@@ -15,15 +15,44 @@ import useViewportHelper from './useViewportHelper';
import { useStore, useStoreApi } from './useStore'; import { useStore, useStoreApi } from './useStore';
import { useBatchContext } from '../components/BatchProvider'; import { useBatchContext } from '../components/BatchProvider';
import { elementToRemoveChange, isEdge, isNode } from '../utils'; import { elementToRemoveChange, isEdge, isNode } from '../utils';
import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types'; import type {
ReactFlowInstance,
Node,
Edge,
InternalNode,
ReactFlowState,
GeneralHelpers,
FitViewOptions,
} from '../types';
const selector = (s: ReactFlowState) => !!s.panZoom; const selector = (s: ReactFlowState) => !!s.panZoom;
/** /**
* Hook for accessing the ReactFlow instance. * This hook returns a ReactFlowInstance that can be used to update nodes and edges, manipulate the viewport, or query the current state of the flow.
* *
* @public * @public
* @returns ReactFlowInstance * @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { useReactFlow } from '@xyflow/react';
*
*export function NodeCounter() {
* const reactFlow = useReactFlow();
* const [count, setCount] = useState(0);
* const countNodes = useCallback(() => {
* setCount(reactFlow.getNodes().length);
* // you need to pass it as a dependency if you are using it with useEffect or useCallback
* // because at the first render, it's not initialized yet and some functions might not work.
* }, [reactFlow]);
*
* return (
* <div>
* <button onClick={countNodes}>Update count</button>
* <p>There are {count} nodes in the flow.</p>
* </div>
* );
*}
*```
*/ */
export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance< export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
NodeType, NodeType,
@@ -72,7 +101,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
setNodes((prevNodes) => setNodes((prevNodes) =>
prevNodes.map((node) => { prevNodes.map((node) => {
if (node.id === id) { if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as NodeType) : nodeUpdate; const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node) : nodeUpdate;
return options.replace && isNode(nextNode) ? (nextNode as NodeType) : { ...node, ...nextNode }; return options.replace && isNode(nextNode) ? (nextNode as NodeType) : { ...node, ...nextNode };
} }
@@ -89,7 +118,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
setEdges((prevEdges) => setEdges((prevEdges) =>
prevEdges.map((edge) => { prevEdges.map((edge) => {
if (edge.id === id) { if (edge.id === id) {
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge as EdgeType) : edgeUpdate; const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge) : edgeUpdate;
return options.replace && isEdge(nextEdge) ? (nextEdge as EdgeType) : { ...edge, ...nextEdge }; return options.replace && isEdge(nextEdge) ? (nextEdge as EdgeType) : { ...edge, ...nextEdge };
} }
@@ -184,7 +213,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
return (nodes || store.getState().nodes).filter((n) => { return (nodes || store.getState().nodes).filter((n) => {
const internalNode = store.getState().nodeLookup.get(n.id); const internalNode = store.getState().nodeLookup.get(n.id);
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) { if (internalNode && !isRect && (n.id === nodeOrRect.id || !internalNode.internals.positionAbsolute)) {
return false; return false;
} }
@@ -248,6 +277,17 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
.connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`) .connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`)
?.values() ?? [] ?.values() ?? []
), ),
fitView: async (options: FitViewOptions<NodeType> | undefined) => {
// We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.getState().fitViewResolver ?? Promise.withResolvers<boolean>();
// We schedule a fitView by setting fitViewQueued and triggering a setNodes
store.setState({ fitViewQueued: true, fitViewOptions: options, fitViewResolver });
batchContext.nodeQueue.push((nodes) => [...nodes]);
return fitViewResolver.promise;
},
}; };
}, []); }, []);
+1 -1
View File
@@ -16,7 +16,7 @@ export function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null
if (!domNode.current) { if (!domNode.current) {
return false; return false;
} }
const size = getDimensions(domNode.current!); const size = getDimensions(domNode.current);
if (size.height === 0 || size.width === 0) { if (size.height === 0 || size.width === 0) {
store.getState().onError?.('004', errorMessages['error004']()); store.getState().onError?.('004', errorMessages['error004']());
+29 -5
View File
@@ -9,16 +9,27 @@ import type { Edge, Node, ReactFlowState } from '../types';
const zustandErrorMessage = errorMessages['error001'](); const zustandErrorMessage = errorMessages['error001']();
/** /**
* Hook for accessing the internal store. Should only be used in rare cases. * This hook can be used to subscribe to internal state changes of the React Flow
* component. The `useStore` hook is re-exported from the [Zustand](https://github.com/pmndrs/zustand)
* state management library, so you should check out their docs for more details.
* *
* @public * @public
* @param selector * @param selector - A selector function that returns a slice of the flow's internal state.
* @param equalityFn * Extracting or transforming just the state you need is a good practice to avoid unnecessary
* @returns The selected state slice * re-renders.
* @param equalityFn - A function to compare the previous and next value. This is incredibly useful
* for preventing unnecessary re-renders. Good sensible defaults are using `Object.is` or importing
* `zustand/shallow`, but you can be as granular as you like.
* @returns The selected state slice.
* *
* @example * @example
* const nodes = useStore((state: ReactFlowState<MyNodeType>) => state.nodes); * ```ts
* const nodes = useStore((state) => state.nodes);
* ```
* *
* @remarks This hook should only be used if there is no other way to access the internal
* state. For many of the common use cases, there are dedicated hooks available
* such as {@link useReactFlow}, {@link useViewport}, etc.
*/ */
function useStore<StateSlice = unknown>( function useStore<StateSlice = unknown>(
selector: (state: ReactFlowState) => StateSlice, selector: (state: ReactFlowState) => StateSlice,
@@ -33,6 +44,19 @@ function useStore<StateSlice = unknown>(
return useZustandStore(store, selector, equalityFn); return useZustandStore(store, selector, equalityFn);
} }
/**
* In some cases, you might need to access the store directly. This hook returns the store object which can be used on demand to access the state or dispatch actions.
*
* @returns The store object.
* @example
* ```ts
* const store = useStoreApi();
* ```
*
* @remarks This hook should only be used if there is no other way to access the internal
* state. For many of the common use cases, there are dedicated hooks available
* such as {@link useReactFlow}, {@link useViewport}, etc.
*/
function useStoreApi<NodeType extends Node = Node, EdgeType extends Edge = Edge>() { function useStoreApi<NodeType extends Node = Node, EdgeType extends Edge = Edge>() {
const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn< const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn<
StoreApi<ReactFlowState<NodeType, EdgeType>> StoreApi<ReactFlowState<NodeType, EdgeType>>
@@ -4,10 +4,49 @@ import type { UpdateNodeInternals, InternalNodeUpdate } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore'; import { useStoreApi } from '../hooks/useStore';
/** /**
* Hook for updating node internals. * When you programmatically add or remove handles to a node or update a node's
* handle position, you need to let React Flow know about it using this hook. This
* will update the internal dimensions of the node and properly reposition handles
* on the canvas if necessary.
* *
* @public * @public
* @returns function for updating node internals * @returns Use this function to tell React Flow to update the internal state of one or more nodes
* that you have changed programmatically.
*
* @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { Handle, useUpdateNodeInternals } from '@xyflow/react';
*
*export default function RandomHandleNode({ id }) {
* const updateNodeInternals = useUpdateNodeInternals();
* const [handleCount, setHandleCount] = useState(0);
* const randomizeHandleCount = useCallback(() => {
* setHandleCount(Math.floor(Math.random() * 10));
* updateNodeInternals(id);
* }, [id, updateNodeInternals]);
*
* return (
* <>
* {Array.from({ length: handleCount }).map((_, index) => (
* <Handle
* key={index}
* type="target"
* position="left"
* id={`handle-${index}`}
* />
* ))}
*
* <div>
* <button onClick={randomizeHandleCount}>Randomize handle count</button>
* <p>There are {handleCount} handles on this node.</p>
* </div>
* </>
* );
*}
*```
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/ */
export function useUpdateNodeInternals(): UpdateNodeInternals { export function useUpdateNodeInternals(): UpdateNodeInternals {
const store = useStoreApi(); const store = useStoreApi();
+25 -2
View File
@@ -11,10 +11,33 @@ const viewportSelector = (state: ReactFlowState) => ({
}); });
/** /**
* Hook for getting the current viewport from the store. * The `useViewport` hook is a convenient way to read the current state of the
* {@link Viewport} in a component. Components that use this hook
* will re-render **whenever the viewport changes**.
* *
* @public * @public
* @returns The current viewport * @returns The current viewport.
*
* @example
*
*```jsx
*import { useViewport } from '@xyflow/react';
*
*export default function ViewportDisplay() {
* const { x, y, zoom } = useViewport();
*
* return (
* <div>
* <p>
* The viewport is currently at ({x}, {y}) and zoomed to {zoom}.
* </p>
* </div>
* );
*}
*```
*
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/ */
export function useViewport(): Viewport { export function useViewport(): Viewport {
const viewport = useStore(viewportSelector, shallow); const viewport = useStore(viewportSelector, shallow);
+9 -29
View File
@@ -2,11 +2,9 @@ import { useMemo } from 'react';
import { import {
pointToRendererPoint, pointToRendererPoint,
getViewportForBounds, getViewportForBounds,
getFitViewNodes,
fitView,
type XYPosition, type XYPosition,
rendererPointToPoint, rendererPointToPoint,
getDimensions, SnapGrid,
} from '@xyflow/system'; } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore'; import { useStoreApi } from '../hooks/useStore';
@@ -64,28 +62,6 @@ const useViewportHelper = (): ViewportHelperFunctions => {
const [x, y, zoom] = store.getState().transform; const [x, y, zoom] = store.getState().transform;
return { x, y, zoom }; return { x, y, zoom };
}, },
fitView: (options) => {
const { nodeLookup, minZoom, maxZoom, panZoom, domNode } = store.getState();
if (!panZoom || !domNode) {
return Promise.resolve(false);
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
const { width, height } = getDimensions(domNode);
return fitView(
{
nodes: fitViewNodes,
width,
height,
minZoom,
maxZoom,
panZoom,
},
options
);
},
setCenter: async (x, y, options) => { setCenter: async (x, y, options) => {
const { width, height, maxZoom, panZoom } = store.getState(); const { width, height, maxZoom, panZoom } = store.getState();
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
@@ -119,21 +95,25 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return Promise.resolve(true); return Promise.resolve(true);
}, },
screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => { screenToFlowPosition: (
const { transform, snapGrid, domNode } = store.getState(); clientPosition: XYPosition,
options: { snapToGrid?: boolean; snapGrid?: SnapGrid } = {}
) => {
const { transform, snapGrid, snapToGrid, domNode } = store.getState();
if (!domNode) { if (!domNode) {
return clientPosition; return clientPosition;
} }
const { x: domX, y: domY } = domNode.getBoundingClientRect(); const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = { const correctedPosition = {
x: clientPosition.x - domX, x: clientPosition.x - domX,
y: clientPosition.y - domY, y: clientPosition.y - domY,
}; };
const _snapGrid = options.snapGrid ?? snapGrid;
const _snapToGrid = options.snapToGrid ?? snapToGrid;
return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid); return pointToRendererPoint(correctedPosition, transform, _snapToGrid, _snapGrid);
}, },
flowToScreenPosition: (flowPosition: XYPosition) => { flowToScreenPosition: (flowPosition: XYPosition) => {
const { transform, domNode } = store.getState(); const { transform, domNode } = store.getState();
@@ -8,8 +8,8 @@ import type { Node, ReactFlowState } from '../types';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => { const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible return onlyRenderVisible
? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map( ? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id (node) => node.id
) )
: Array.from(s.nodeLookup.keys()); : Array.from(s.nodeLookup.keys());
}; };
+2 -1
View File
@@ -9,7 +9,7 @@ export { SmoothStepEdge } from './components/Edges/SmoothStepEdge';
export { BaseEdge } from './components/Edges/BaseEdge'; export { BaseEdge } from './components/Edges/BaseEdge';
export { ReactFlowProvider } from './components/ReactFlowProvider'; export { ReactFlowProvider } from './components/ReactFlowProvider';
export { Panel, type PanelProps } from './components/Panel'; export { Panel, type PanelProps } from './components/Panel';
export { EdgeLabelRenderer } from './components/EdgeLabelRenderer'; export { EdgeLabelRenderer, type EdgeLabelRendererProps } from './components/EdgeLabelRenderer';
export { ViewportPortal } from './components/ViewportPortal'; export { ViewportPortal } from './components/ViewportPortal';
export { useReactFlow } from './hooks/useReactFlow'; export { useReactFlow } from './hooks/useReactFlow';
@@ -106,6 +106,7 @@ export {
type FinalConnectionState, type FinalConnectionState,
type ConnectionInProgress, type ConnectionInProgress,
type NoConnection, type NoConnection,
type NodeConnection,
} from '@xyflow/system'; } from '@xyflow/system';
// we need this workaround to prevent a duplicate identifier error // we need this workaround to prevent a duplicate identifier error
+81 -112
View File
@@ -1,7 +1,5 @@
import { createWithEqualityFn } from 'zustand/traditional'; import { createWithEqualityFn } from 'zustand/traditional';
import { import {
getFitViewNodes,
fitView as fitViewSystem,
adoptUserNodes, adoptUserNodes,
updateAbsolutePositions, updateAbsolutePositions,
panBy as panBySystem, panBy as panBySystem,
@@ -15,11 +13,12 @@ import {
initialConnection, initialConnection,
NodeOrigin, NodeOrigin,
CoordinateExtent, CoordinateExtent,
fitViewport,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState'; import getInitialState from './initialState';
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions } from '../types'; import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams } from '../types';
const createStore = ({ const createStore = ({
nodes, nodes,
@@ -42,25 +41,60 @@ const createStore = ({
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
}) => }) =>
createWithEqualityFn<ReactFlowState>( createWithEqualityFn<ReactFlowState>((set, get) => {
(set, get) => ({ async function resolveFitView() {
const { nodeLookup, panZoom, fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get();
if (!panZoom) {
return;
}
await fitViewport(
{
nodes: nodeLookup,
width,
height,
panZoom,
minZoom,
maxZoom,
},
fitViewOptions
);
fitViewResolver?.resolve(true);
/**
* wait for the fitViewport to resolve before deleting the resolver,
* we want to reuse the old resolver if the user calls fitView again in the mean time
*/
set({ fitViewResolver: null });
}
return {
...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => { setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get();
// setNodes() is called exclusively in response to user actions: /*
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup, * setNodes() is called exclusively in response to user actions:
// - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup. * - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
// * - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
// When this happens, we take the note objects passed by the user and extend them with fields *
// relevant for internal React Flow operations. * When this happens, we take the note objects passed by the user and extend them with fields
adoptUserNodes(nodes, nodeLookup, parentLookup, { * relevant for internal React Flow operations.
*/
const nodesInitialized = adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
elevateNodesOnSelect, elevateNodesOnSelect,
checkEquality: true, checkEquality: true,
}); });
set({ nodes }); if (fitViewQueued && nodesInitialized) {
resolveFitView();
set({ nodes, fitViewQueued: false, fitViewOptions: undefined });
} else {
set({ nodes });
}
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { connectionLookup, edgeLookup } = get(); const { connectionLookup, edgeLookup } = get();
@@ -81,23 +115,14 @@ const createStore = ({
set({ hasDefaultEdges: true }); set({ hasDefaultEdges: true });
} }
}, },
// Every node gets registerd at a ResizeObserver. Whenever a node /*
// changes its dimensions, this function is called to measure the * Every node gets registerd at a ResizeObserver. Whenever a node
// new dimensions and update the nodes. * changes its dimensions, this function is called to measure the
updateNodeInternals: (updates, params = { triggerFitView: true }) => { * new dimensions and update the nodes.
const { */
triggerNodeChanges, updateNodeInternals: (updates) => {
nodeLookup, const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued } =
parentLookup, get();
fitViewOnInit,
fitViewDone,
fitViewOnInitOptions,
domNode,
nodeOrigin,
nodeExtent,
debug,
fitViewSync,
} = get();
const { changes, updatedInternals } = updateNodeInternalsSystem( const { changes, updatedInternals } = updateNodeInternalsSystem(
updates, updates,
@@ -114,23 +139,9 @@ const createStore = ({
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent });
if (params.triggerFitView) { if (fitViewQueued) {
// we call fitView once initially after all dimensions are set resolveFitView();
let nextFitViewDone = fitViewDone; set({ fitViewQueued: false, fitViewOptions: undefined });
if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitViewSync({
...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes,
});
}
// here we are cirmumventing the onNodesChange handler
// in order to be able to display nodes even if the user
// has not provided an onNodesChange handler.
// Nodes are only rendered if they have a width and height
// attribute which they get from this handler.
set({ fitViewDone: nextFitViewDone });
} else { } else {
// we always want to trigger useStore calls whenever updateNodeInternals is called // we always want to trigger useStore calls whenever updateNodeInternals is called
set({}); set({});
@@ -146,9 +157,12 @@ const createStore = ({
updateNodePositions: (nodeDragItems, dragging = false) => { updateNodePositions: (nodeDragItems, dragging = false) => {
const parentExpandChildren: ParentExpandChild[] = []; const parentExpandChildren: ParentExpandChild[] = [];
const changes = []; const changes = [];
const { nodeLookup, triggerNodeChanges } = get();
for (const [id, dragItem] of nodeDragItems) { for (const [id, dragItem] of nodeDragItems) {
const expandParent = !!(dragItem?.expandParent && dragItem?.parentId && dragItem?.position); // we are using the nodelookup to be sure to use the current expandParent and parentId value
const node = nodeLookup.get(id);
const expandParent = !!(node?.expandParent && node?.parentId && dragItem?.position);
const change: NodeChange = { const change: NodeChange = {
id, id,
@@ -162,14 +176,14 @@ const createStore = ({
dragging, dragging,
}; };
if (expandParent) { if (expandParent && node.parentId) {
parentExpandChildren.push({ parentExpandChildren.push({
id, id,
parentId: dragItem.parentId!, parentId: node.parentId,
rect: { rect: {
...dragItem.internals.positionAbsolute, ...dragItem.internals.positionAbsolute,
width: dragItem.measured.width!, width: dragItem.measured.width ?? 0,
height: dragItem.measured.height!, height: dragItem.measured.height ?? 0,
}, },
}); });
} }
@@ -178,12 +192,12 @@ const createStore = ({
} }
if (parentExpandChildren.length > 0) { if (parentExpandChildren.length > 0) {
const { nodeLookup, parentLookup, nodeOrigin } = get(); const { parentLookup, nodeOrigin } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin); const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
} }
get().triggerNodeChanges(changes); triggerNodeChanges(changes);
}, },
triggerNodeChanges: (changes) => { triggerNodeChanges: (changes) => {
const { onNodesChange, setNodes, nodes, hasDefaultNodes, debug } = get(); const { onNodesChange, setNodes, nodes, hasDefaultNodes, debug } = get();
@@ -222,7 +236,7 @@ const createStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)); const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
triggerNodeChanges(nodeChanges as NodeSelectionChange[]); triggerNodeChanges(nodeChanges);
return; return;
} }
@@ -234,7 +248,7 @@ const createStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)); const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
triggerEdgeChanges(changedEdges as EdgeSelectionChange[]); triggerEdgeChanges(changedEdges);
return; return;
} }
@@ -248,8 +262,10 @@ const createStore = ({
const nodeChanges = nodesToUnselect.map((n) => { const nodeChanges = nodesToUnselect.map((n) => {
const internalNode = nodeLookup.get(n.id); const internalNode = nodeLookup.get(n.id);
if (internalNode) { if (internalNode) {
// we need to unselect the internal node that was selected previously before we /*
// send the change to the user to prevent it to be selected while dragging the new node * we need to unselect the internal node that was selected previously before we
* send the change to the user to prevent it to be selected while dragging the new node
*/
internalNode.selected = false; internalNode.selected = false;
} }
@@ -257,8 +273,8 @@ const createStore = ({
}); });
const edgeChanges = edgesToUnselect.map((edge) => createSelectionChange(edge.id, false)); const edgeChanges = edgesToUnselect.map((edge) => createSelectionChange(edge.id, false));
triggerNodeChanges(nodeChanges as NodeSelectionChange[]); triggerNodeChanges(nodeChanges);
triggerEdgeChanges(edgeChanges as EdgeSelectionChange[]); triggerEdgeChanges(edgeChanges);
}, },
setMinZoom: (minZoom) => { setMinZoom: (minZoom) => {
const { panZoom, maxZoom } = get(); const { panZoom, maxZoom } = get();
@@ -284,11 +300,11 @@ const createStore = ({
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
const nodeChanges = nodes.reduce<NodeSelectionChange[]>( const nodeChanges = nodes.reduce<NodeSelectionChange[]>(
(res, node) => (node.selected ? [...res, createSelectionChange(node.id, false) as NodeSelectionChange] : res), (res, node) => (node.selected ? [...res, createSelectionChange(node.id, false)] : res),
[] []
); );
const edgeChanges = edges.reduce<EdgeSelectionChange[]>( const edgeChanges = edges.reduce<EdgeSelectionChange[]>(
(res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false) as EdgeSelectionChange] : res), (res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false)] : res),
[] []
); );
@@ -321,52 +337,6 @@ const createStore = ({
return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
}, },
fitView: (options?: FitViewOptions): Promise<boolean> => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) {
return Promise.resolve(false);
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
return fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
panZoom,
minZoom,
maxZoom,
},
options
);
},
// we can't call an asnychronous function in updateNodeInternals
// for that we created this sync version of fitView
fitViewSync: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) {
return false;
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
panZoom,
minZoom,
maxZoom,
},
options
);
return fitViewNodes.size > 0;
},
cancelConnection: () => { cancelConnection: () => {
set({ set({
connection: { ...initialConnection }, connection: { ...initialConnection },
@@ -377,8 +347,7 @@ const createStore = ({
}, },
reset: () => set({ ...getInitialState() }), reset: () => set({ ...getInitialState() }),
}), };
Object.is }, Object.is);
);
export { createStore }; export { createStore };
+4 -3
View File
@@ -104,13 +104,14 @@ const getInitialState = ({
elementsSelectable: true, elementsSelectable: true,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
elevateEdgesOnSelect: false, elevateEdgesOnSelect: false,
fitViewOnInit: false,
fitViewDone: false,
fitViewOnInitOptions: undefined,
selectNodesOnDrag: true, selectNodesOnDrag: true,
multiSelectionActive: false, multiSelectionActive: false,
fitViewQueued: fitView ?? false,
fitViewOptions: undefined,
fitViewResolver: null,
connection: { ...initialConnection }, connection: { ...initialConnection },
connectionClickStartHandle: null, connectionClickStartHandle: null,
connectOnClick: true, connectOnClick: true,
+307 -134
View File
@@ -52,7 +52,9 @@ import type {
*/ */
export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
extends Omit<HTMLAttributes<HTMLDivElement>, 'onError'> { extends Omit<HTMLAttributes<HTMLDivElement>, 'onError'> {
/** An array of nodes to render in a controlled flow. /**
* An array of nodes to render in a controlled flow.
* @default []
* @example * @example
* const nodes = [ * const nodes = [
* { * {
@@ -64,7 +66,9 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* ]; * ];
*/ */
nodes?: NodeType[]; nodes?: NodeType[];
/** An array of edges to render in a controlled flow. /**
* An array of edges to render in a controlled flow.
* @default []
* @example * @example
* const edges = [ * const edges = [
* { * {
@@ -79,7 +83,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
defaultNodes?: NodeType[]; defaultNodes?: NodeType[];
/** The initial edges to render in an uncontrolled flow. */ /** The initial edges to render in an uncontrolled flow. */
defaultEdges?: EdgeType[]; defaultEdges?: EdgeType[];
/** Defaults to be applied to all new edges that are added to the flow. /**
* Defaults to be applied to all new edges that are added to the flow.
* *
* Properties on a new edge will override these defaults if they exist. * Properties on a new edge will override these defaults if they exist.
* @example * @example
@@ -99,57 +104,75 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* } * }
*/ */
defaultEdgeOptions?: DefaultEdgeOptions; defaultEdgeOptions?: DefaultEdgeOptions;
/** This event handler is called when a user clicks on a node */ /** This event handler is called when a user clicks on a node. */
onNodeClick?: NodeMouseHandler<NodeType>; onNodeClick?: NodeMouseHandler<NodeType>;
/** This event handler is called when a user double clicks on a node */ /** This event handler is called when a user double-clicks on a node. */
onNodeDoubleClick?: NodeMouseHandler<NodeType>; onNodeDoubleClick?: NodeMouseHandler<NodeType>;
/** This event handler is called when mouse of a user enters a node */ /** This event handler is called when mouse of a user enters a node. */
onNodeMouseEnter?: NodeMouseHandler<NodeType>; onNodeMouseEnter?: NodeMouseHandler<NodeType>;
/** This event handler is called when mouse of a user moves over a node */ /** This event handler is called when mouse of a user moves over a node. */
onNodeMouseMove?: NodeMouseHandler<NodeType>; onNodeMouseMove?: NodeMouseHandler<NodeType>;
/** This event handler is called when mouse of a user leaves a node */ /** This event handler is called when mouse of a user leaves a node. */
onNodeMouseLeave?: NodeMouseHandler<NodeType>; onNodeMouseLeave?: NodeMouseHandler<NodeType>;
/** This event handler is called when a user right clicks on a node */ /** This event handler is called when a user right-clicks on a node. */
onNodeContextMenu?: NodeMouseHandler<NodeType>; onNodeContextMenu?: NodeMouseHandler<NodeType>;
/** This event handler is called when a user starts to drag a node */ /** This event handler is called when a user starts to drag a node. */
onNodeDragStart?: OnNodeDrag<NodeType>; onNodeDragStart?: OnNodeDrag<NodeType>;
/** This event handler is called when a user drags a node */ /** This event handler is called when a user drags a node. */
onNodeDrag?: OnNodeDrag<NodeType>; onNodeDrag?: OnNodeDrag<NodeType>;
/** This event handler is called when a user stops dragging a node */ /** This event handler is called when a user stops dragging a node. */
onNodeDragStop?: OnNodeDrag<NodeType>; onNodeDragStop?: OnNodeDrag<NodeType>;
/** This event handler is called when a user clicks on an edge */ /** This event handler is called when a user clicks on an edge. */
onEdgeClick?: (event: ReactMouseEvent, edge: EdgeType) => void; onEdgeClick?: (event: ReactMouseEvent, edge: EdgeType) => void;
/** This event handler is called when a user right clicks on an edge */ /** This event handler is called when a user right-clicks on an edge. */
onEdgeContextMenu?: EdgeMouseHandler<EdgeType>; onEdgeContextMenu?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when mouse of a user enters an edge */ /** This event handler is called when mouse of a user enters an edge. */
onEdgeMouseEnter?: EdgeMouseHandler<EdgeType>; onEdgeMouseEnter?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when mouse of a user moves over an edge */ /** This event handler is called when mouse of a user moves over an edge. */
onEdgeMouseMove?: EdgeMouseHandler<EdgeType>; onEdgeMouseMove?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when mouse of a user leaves an edge */ /** This event handler is called when mouse of a user leaves an edge. */
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>;
/**
* This handler is called when the source or target of a reconnectable edge is dragged from the
* current node. It will fire even if the edge's source or target do not end up changing.
*
* You can use the `reconnectEdge` utility to convert the connection to a new edge.
*/
onReconnect?: OnReconnect<EdgeType>; onReconnect?: OnReconnect<EdgeType>;
/**
* This event fires when the user begins dragging the source or target of an editable edge.
*/
onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
/**
* This event fires when the user releases the source or target of an editable edge. It is called
* even if an edge update does not occur.
*
*/
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
/** This event handler is called when a Node is updated /**
* Use this event handler to add interactivity to a controlled flow.
* It is called on node drag, select, and move.
* @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';
* const [edges, setNodes, onNodesChange] = useNodesState(initialNodes); * const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* *
* return (<ReactFlow onNodeChange={onNodeChange} {...rest} />) * return (<ReactFlow onNodeChange={onNodeChange} {...rest} />)
* @example // Use helper function to update edge * @example // Use helper function to update node
* import ReactFlow, { applyNodeChanges } from '@xyflow/react'; * import ReactFlow, { applyNodeChanges } from '@xyflow/react';
* *
* const onNodeChange = useCallback( * const onNodeChange = useCallback(
* (changes) => setNode((eds) => applyNodeChanges(changes, eds)), * (changes) => setNode((nds) => applyNodeChanges(changes, nds)),
* [], * [],
* ); * );
* *
* return (<ReactFlow onNodeChange={onNodeChange} {...rest} />) * return (<ReactFlow onNodeChange={onNodeChange} {...rest} />)
*/ */
onNodesChange?: OnNodesChange<NodeType>; onNodesChange?: OnNodesChange<NodeType>;
/** This event handler is called when a Edge is updated /**
* Use this event handler to add interactivity to a controlled flow. It is called on edge select
* and remove.
* @example // Use EdgesState hook to create edges and get onEdgesChange handler * @example // Use EdgesState hook to create edges and get onEdgesChange handler
* import ReactFlow, { useEdgesState } from '@xyflow/react'; * import ReactFlow, { useEdgesState } from '@xyflow/react';
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -166,24 +189,28 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* return (<ReactFlow onEdgesChange={onEdgesChange} {...rest} />) * return (<ReactFlow onEdgesChange={onEdgesChange} {...rest} />)
*/ */
onEdgesChange?: OnEdgesChange<EdgeType>; onEdgesChange?: OnEdgesChange<EdgeType>;
/** This event handler gets called when a Node is deleted */ /** This event handler gets called when a node is deleted. */
onNodesDelete?: OnNodesDelete<NodeType>; onNodesDelete?: OnNodesDelete<NodeType>;
/** This event handler gets called when a Edge is deleted */ /** This event handler gets called when an edge is deleted. */
onEdgesDelete?: OnEdgesDelete<EdgeType>; onEdgesDelete?: OnEdgesDelete<EdgeType>;
/** This event handler gets called when a Node or Edge is deleted */ /** This event handler gets called when a node or edge is deleted. */
onDelete?: OnDelete<NodeType, EdgeType>; onDelete?: OnDelete<NodeType, EdgeType>;
/** This event handler gets called when a user starts to drag a selection box */ /** This event handler gets called when a user starts to drag a selection box. */
onSelectionDragStart?: SelectionDragHandler<NodeType>; onSelectionDragStart?: SelectionDragHandler<NodeType>;
/** This event handler gets called when a user drags a selection box */ /** This event handler gets called when a user drags a selection box. */
onSelectionDrag?: SelectionDragHandler<NodeType>; onSelectionDrag?: SelectionDragHandler<NodeType>;
/** This event handler gets called when a user stops dragging a selection box */ /** This event handler gets called when a user stops dragging a selection box. */
onSelectionDragStop?: SelectionDragHandler<NodeType>; onSelectionDragStop?: SelectionDragHandler<NodeType>;
onSelectionStart?: (event: ReactMouseEvent) => void; onSelectionStart?: (event: ReactMouseEvent) => void;
onSelectionEnd?: (event: ReactMouseEvent) => void; onSelectionEnd?: (event: ReactMouseEvent) => void;
/**
* This event handler is called when a user right-clicks on a node selection.
*/
onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void; onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void;
/** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection. /**
* When a connection line is completed and two nodes are connected by the user, this event fires with the new connection.
* *
* You can use the addEdge utility to convert the connection to a complete edge. * You can use the `addEdge` utility to convert the connection to a complete edge.
* @example // Use helper function to update edges onConnect * @example // Use helper function to update edges onConnect
* import ReactFlow, { addEdge } from '@xyflow/react'; * import ReactFlow, { addEdge } from '@xyflow/react';
* *
@@ -195,178 +222,260 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* return (<ReactFlow onConnect={onConnect} {...rest} />) * return (<ReactFlow onConnect={onConnect} {...rest} />)
*/ */
onConnect?: OnConnect; onConnect?: OnConnect;
/** This event handler gets called when a user starts to drag a connection line */ /** This event handler gets called when a user starts to drag a connection line. */
onConnectStart?: OnConnectStart; onConnectStart?: OnConnectStart;
/** This event handler gets called when a user stops dragging a connection line */ /**
* This callback will fire regardless of whether a valid connection could be made or not. You can
* use the second `connectionState` parameter to have different behavior when a connection was
* unsuccessful.
*/
onConnectEnd?: OnConnectEnd; onConnectEnd?: OnConnectEnd;
onClickConnectStart?: OnConnectStart; onClickConnectStart?: OnConnectStart;
onClickConnectEnd?: OnConnectEnd; onClickConnectEnd?: OnConnectEnd;
/** This event handler gets called when a flow has finished initializing */ /**
* The `onInit` callback is called when the viewport is initialized. At this point you can use the
* instance to call methods like `fitView` or `zoomTo`.
*/
onInit?: OnInit<NodeType, EdgeType>; onInit?: OnInit<NodeType, EdgeType>;
/** This event handler is called while the user is either panning or zooming the viewport. */ /** This event handler is called while the user is either panning or zooming the viewport. */
onMove?: OnMove; onMove?: OnMove;
/** This event handler gets called when a user starts to pan or zoom the viewport */ /** This event handler is called when the user begins to pan or zoom the viewport. */
onMoveStart?: OnMoveStart; onMoveStart?: OnMoveStart;
/** This event handler gets called when a user stops panning or zooming the viewport */ /**
* This event handler is called when panning or zooming viewport movement stops.
* If the movement is not user-initiated, the event parameter will be `null`.
*/
onMoveEnd?: OnMoveEnd; onMoveEnd?: OnMoveEnd;
/** This event handler gets called when a user changes group of selected elements in the flow */ /** This event handler gets called when a user changes group of selected elements in the flow. */
onSelectionChange?: OnSelectionChangeFunc; onSelectionChange?: OnSelectionChangeFunc<NodeType, EdgeType>;
/** This event handler gets called when user scroll inside the pane */ /** This event handler gets called when user scroll inside the pane. */
onPaneScroll?: (event?: WheelEvent) => void; onPaneScroll?: (event?: WheelEvent) => void;
/** This event handler gets called when user clicks inside the pane */ /** This event handler gets called when user clicks inside the pane. */
onPaneClick?: (event: ReactMouseEvent) => void; onPaneClick?: (event: ReactMouseEvent) => void;
/** This event handler gets called when user right clicks inside the pane */ /** This event handler gets called when user right clicks inside the pane. */
onPaneContextMenu?: (event: ReactMouseEvent | MouseEvent) => void; onPaneContextMenu?: (event: ReactMouseEvent | MouseEvent) => void;
/** This event handler gets called when mouse enters the pane */ /** This event handler gets called when mouse enters the pane. */
onPaneMouseEnter?: (event: ReactMouseEvent) => void; onPaneMouseEnter?: (event: ReactMouseEvent) => void;
/** This event handler gets called when mouse moves over the pane */ /** This event handler gets called when mouse moves over the pane. */
onPaneMouseMove?: (event: ReactMouseEvent) => void; onPaneMouseMove?: (event: ReactMouseEvent) => void;
/** This event handler gets called when mouse leaves the pane */ /** This event handler gets called when mouse leaves the pane. */
onPaneMouseLeave?: (event: ReactMouseEvent) => void; onPaneMouseLeave?: (event: ReactMouseEvent) => void;
/** Distance that the mouse can move between mousedown/up that will trigger a click /**
* Distance that the mouse can move between mousedown/up that will trigger a click.
* @default 0 * @default 0
*/ */
paneClickDistance?: number; paneClickDistance?: number;
/** Distance that the mouse can move between mousedown/up that will trigger a click /**
* Distance that the mouse can move between mousedown/up that will trigger a click.
* @default 0 * @default 0
*/ */
nodeClickDistance?: number; nodeClickDistance?: number;
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */ /**
* This handler is called before nodes or edges are deleted, allowing the deletion to be aborted
* by returning `false` or modified by returning updated nodes and edges.
*/
onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>; onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
/** Custom node types to be available in a flow. /**
* Custom node types to be available in a flow.
* *
* React Flow matches a node's type to a component in the nodeTypes object. * React Flow matches a node's type to a component in the `nodeTypes` object.
* @TODO check if @default is correct
* @default {
* input: InputNode,
* default: DefaultNode,
* output: OutputNode,
* group: GroupNode
* }
* @example * @example
* import CustomNode from './CustomNode'; * import CustomNode from './CustomNode';
* *
* const nodeTypes = { nameOfNodeType: CustomNode }; * const nodeTypes = { nameOfNodeType: CustomNode };
*/ */
nodeTypes?: NodeTypes; nodeTypes?: NodeTypes;
/** Custom edge types to be available in a flow. /**
* Custom edge types to be available in a flow.
* *
* React Flow matches an edge's type to a component in the edgeTypes object. * React Flow matches an edge's type to a component in the `edgeTypes` object.
* @TODO check if @default is correct
* @default {
* default: BezierEdge,
* straight: StraightEdge,
* step: StepEdge,
* smoothstep: SmoothStepEdge,
* simplebezier: SimpleBezier
* }
* @example * @example
* import CustomEdge from './CustomEdge'; * import CustomEdge from './CustomEdge';
* *
* const edgeTypes = { nameOfEdgeType: CustomEdge }; * const edgeTypes = { nameOfEdgeType: CustomEdge };
*/ */
edgeTypes?: EdgeTypes; edgeTypes?: EdgeTypes;
/** The type of edge path to use for connection lines. /**
* The type of edge path to use for connection lines.
* *
* Although created edges can be of any type, React Flow needs to know what type of path to render for the connection line before the edge is created! * Although created edges can be of any type, React Flow needs to know what type of path to render for the connection line before the edge is created!
* @default ConnectionLineType.Bezier
*/ */
connectionLineType?: ConnectionLineType; connectionLineType?: ConnectionLineType;
/** Styles to be applied to the connection line */ /** Styles to be applied to the connection line. */
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
/** React Component to be used as a connection line */ /** React Component to be used as a connection line. */
connectionLineComponent?: ConnectionLineComponent; connectionLineComponent?: ConnectionLineComponent<NodeType>;
/** Styles to be applied to the container of the connection line */ /** Styles to be applied to the container of the connection line. */
connectionLineContainerStyle?: CSSProperties; connectionLineContainerStyle?: CSSProperties;
/** 'strict' connection mode will only allow you to connect source handles to target handles. /**
* * A loose connection mode will allow you to connect handles with differing types, including
* 'loose' connection mode will allow you to connect handles of any type to one another. * source-to-source connections. However, it does not support target-to-target connections. Strict
* mode allows only connections between source handles and target handles.
* @default 'strict' * @default 'strict'
*/ */
connectionMode?: ConnectionMode; connectionMode?: ConnectionMode;
/** Pressing down this key deletes all selected nodes & edges. /**
* If set, pressing the key or chord will delete any selected nodes and edges. Passing an array
* represents multiple keys that can be pressed.
*
* For example, `["Delete", "Backspace"]` will delete selected elements when either key is pressed.
* @default 'Backspace' * @default 'Backspace'
*/ */
deleteKeyCode?: KeyCode | null; deleteKeyCode?: KeyCode | null;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false. /**
* If set, holding this key will let you click and drag to draw a selection box around multiple
* nodes and edges. Passing an array represents multiple keys that can be pressed.
* *
* By setting this prop to null you can disable this functionality. * For example, `["Shift", "Meta"]` will allow you to draw a selection box when either key is
* @default 'Space' * pressed.
* @default 'Shift'
*/ */
selectionKeyCode?: KeyCode | null; selectionKeyCode?: KeyCode | null;
/** Select multiple elements with a selection box, without pressing down selectionKey */ /**
* Select multiple elements with a selection box, without pressing down `selectionKey`.
* @default false
*/
selectionOnDrag?: boolean; selectionOnDrag?: boolean;
/** When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected. /**
* When set to `"partial"`, when the user creates a selection box by click and dragging nodes that
* are only partially in the box are still selected.
* @default 'full' * @default 'full'
*/ */
selectionMode?: SelectionMode; selectionMode?: SelectionMode;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false. /**
* If a key is set, you can pan the viewport while that key is held down even if `panOnScroll`
* is set to `false`.
* *
* By setting this prop to null you can disable this functionality. * By setting this prop to `null` you can disable this functionality.
* @default 'Space' * @default 'Space'
*/ */
panActivationKeyCode?: KeyCode | null; panActivationKeyCode?: KeyCode | null;
/** Pressing down this key you can select multiple elements by clicking. /**
* @default 'Meta' for macOS, "Ctrl" for other systems * Pressing down this key you can select multiple elements by clicking.
* @default "Meta" for macOS, "Control" for other systems
*/ */
multiSelectionKeyCode?: KeyCode | null; multiSelectionKeyCode?: KeyCode | null;
/** If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false. /**
* If a key is set, you can zoom the viewport while that key is held down even if `panOnScroll`
* is set to `false`.
* *
* By setting this prop to null you can disable this functionality. * By setting this prop to `null` you can disable this functionality.
* @default 'Meta' for macOS, "Ctrl" for other systems * @default "Meta" for macOS, "Control" for other systems
* */ *
*/
zoomActivationKeyCode?: KeyCode | null; zoomActivationKeyCode?: KeyCode | null;
/** Set this prop to make the flow snap to the grid */ /** When enabled, nodes will snap to the grid when dragged. */
snapToGrid?: boolean; snapToGrid?: boolean;
/** Grid all nodes will snap to /**
* If `snapToGrid` is enabled, this prop configures the grid that nodes will snap to.
* @example [20, 20] * @example [20, 20]
*/ */
snapGrid?: SnapGrid; snapGrid?: SnapGrid;
/** You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport. /**
* You can enable this optimisation to instruct React Flow to only render nodes and edges that would be visible in the viewport.
* *
* This might improve performance when you have a large number of nodes and edges but also adds an overhead. * This might improve performance when you have a large number of nodes and edges but also adds an overhead.
* @default false * @default false
*/ */
onlyRenderVisibleElements?: boolean; onlyRenderVisibleElements?: boolean;
/** Controls if all nodes should be draggable /**
* Controls whether all nodes should be draggable or not. Individual nodes can override this
* setting by setting their `draggable` prop. If you want to use the mouse handlers on
* non-draggable nodes, you need to add the `"nopan"` class to those nodes.
* @default true * @default true
*/ */
nodesDraggable?: boolean; nodesDraggable?: boolean;
/** Controls if all nodes should be connectable to each other /**
* Controls whether all nodes should be connectable or not. Individual nodes can override this
* setting by setting their `connectable` prop.
* @default true * @default true
*/ */
nodesConnectable?: boolean; nodesConnectable?: boolean;
/** Controls if all nodes should be focusable /**
* When `true`, focus between nodes can be cycled with the `Tab` key and selected with the `Enter`
* key. This option can be overridden by individual nodes by setting their `focusable` prop.
* @default true * @default true
*/ */
nodesFocusable?: boolean; nodesFocusable?: boolean;
/** Defines nodes relative position to its coordinates /**
* The origin of the node to use when placing it in the flow or looking up its `x` and `y`
* position. An origin of `[0, 0]` means that a node's top left corner will be placed at the `x`
* and `y` position.
* @default [0, 0]
* @example * @example
* [0, 0] // default, top left * [0, 0] // default, top left
* [0.5, 0.5] // center * [0.5, 0.5] // center
* [1, 1] // bottom right * [1, 1] // bottom right
*/ */
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
/** Controls if all edges should be focusable /**
* When `true`, focus between edges can be cycled with the `Tab` key and selected with the `Enter`
* key. This option can be overridden by individual edges by setting their `focusable` prop.
* @default true * @default true
*/ */
edgesFocusable?: boolean; edgesFocusable?: boolean;
/** Controls if all edges should be updateable /**
* Whether edges can be updated once they are created. When both this prop is `true` and an
* `onReconnect` handler is provided, the user can drag an existing edge to a new source or
* target. Individual edges can override this value with their reconnectable property.
* @default true * @default true
*/ */
edgesReconnectable?: boolean; edgesReconnectable?: boolean;
/** Controls if all elements should (nodes & edges) be selectable /**
* When `true`, elements (nodes and edges) can be selected by clicking on them. This option can be
* overridden by individual elements by setting their `selectable` prop.
* @default true * @default true
*/ */
elementsSelectable?: boolean; elementsSelectable?: boolean;
/** If true, nodes get selected on drag /**
* If `true`, nodes get selected on drag.
* @default true * @default true
*/ */
selectNodesOnDrag?: boolean; selectNodesOnDrag?: boolean;
/** Enableing this prop allows users to pan the viewport by clicking and dragging. /**
* Enabling this prop allows users to pan the viewport by clicking and dragging.
* *
* You can also set this prop to an array of numbers to limit which mouse buttons can activate panning. * You can also set this prop to an array of numbers to limit which mouse buttons can activate panning.
* @default true
* @example [0, 2] // allows panning with the left and right mouse buttons * @example [0, 2] // allows panning with the left and right mouse buttons
* [0, 1, 2, 3, 4] // allows panning with all mouse buttons * [0, 1, 2, 3, 4] // allows panning with all mouse buttons
*/ */
panOnDrag?: boolean | number[]; panOnDrag?: boolean | number[];
/** Minimum zoom level /**
* Minimum zoom level.
* @default 0.5 * @default 0.5
*/ */
minZoom?: number; minZoom?: number;
/** Maximum zoom level /**
* Maximum zoom level.
* @default 2 * @default 2
*/ */
maxZoom?: number; maxZoom?: number;
/** Controlled viewport to be used instead of internal one */ /**
* When you pass a `viewport` prop, it's controlled, and you also need to pass `onViewportChange`
* to handle internal changes.
*/
viewport?: Viewport; viewport?: Viewport;
/** Sets the initial position and zoom of the viewport. /**
* * Sets the initial position and zoom of the viewport. If a default viewport is provided but
* If a default viewport is provided but fitView is enabled, the default viewport will be ignored. * `fitView` is enabled, the default viewport will be ignored.
* @default { x: 0, y: 0, zoom: 1 }
* @example * @example
* const initialViewport = { * const initialViewport = {
* zoom: 0.5, * zoom: 0.5,
@@ -375,59 +484,103 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
*/ */
defaultViewport?: Viewport; defaultViewport?: Viewport;
/** /**
* Gets called when the viewport changes. * Used when working with a controlled viewport for updating the user viewport state.
*/ */
onViewportChange?: (viewport: Viewport) => void; onViewportChange?: (viewport: Viewport) => void;
/** By default the viewport extends infinitely. You can use this prop to set a boundary. /**
* By default, the viewport extends infinitely. You can use this prop to set a boundary.
* *
* The first pair of coordinates is the top left boundary and the second pair is the bottom right. * The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @default [[-, -], [+, +]]
* @example [[-1000, -10000], [1000, 1000]] * @example [[-1000, -10000], [1000, 1000]]
*/ */
translateExtent?: CoordinateExtent; translateExtent?: CoordinateExtent;
/** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow. /**
* Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
* @default true * @default true
*/ */
preventScrolling?: boolean; preventScrolling?: boolean;
/** By default nodes can be placed on an infinite flow. You can use this prop to set a boundary. /**
* By default, nodes can be placed on an infinite flow. You can use this prop to set a boundary.
* *
* The first pair of coordinates is the top left boundary and the second pair is the bottom right. * The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @example [[-1000, -10000], [1000, 1000]] * @example [[-1000, -10000], [1000, 1000]]
*/ */
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
/** Color of edge markers /**
* @example "#b1b1b7" * Color of edge markers.
* @default '#b1b1b7'
*/ */
defaultMarkerColor?: string; defaultMarkerColor?: string;
/** Controls if the viewport should zoom by scrolling inside the container */ /**
* Controls if the viewport should zoom by scrolling inside the container.
* @default true
*/
zoomOnScroll?: boolean; zoomOnScroll?: boolean;
/** Controls if the viewport should zoom by pinching on a touch screen */ /**
* Controls if the viewport should zoom by pinching on a touch screen.
* @default true
*/
zoomOnPinch?: boolean; zoomOnPinch?: boolean;
/** Controls if the viewport should pan by scrolling inside the container /**
* Controls if the viewport should pan by scrolling inside the container.
* *
* Can be limited to a specific direction with panOnScrollMode * Can be limited to a specific direction with `panOnScrollMode`.
* @default false
*/ */
panOnScroll?: boolean; panOnScroll?: boolean;
/** Controls how fast viewport should be panned on scroll. /**
* Controls how fast viewport should be panned on scroll.
* *
* Use togther with panOnScroll prop. * Use together with `panOnScroll` prop.
* @default 0.5
*/ */
panOnScrollSpeed?: number; panOnScrollSpeed?: number;
/** This prop is used to limit the direction of panning when panOnScroll is enabled. /**
* This prop is used to limit the direction of panning when `panOnScroll` is enabled.
* *
* The "free" option allows panning in any direction. * The `"free"` option allows panning in any direction.
* @default "free" * @default "free"
* @example "horizontal" | "vertical" * @example "horizontal" | "vertical"
*/ */
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.
* @default true
*/
zoomOnDoubleClick?: boolean; zoomOnDoubleClick?: boolean;
/**
* The radius around an edge connection that can trigger an edge reconnection.
* @default 10
*/
reconnectRadius?: number; reconnectRadius?: number;
/**
* If a node is draggable, clicking and dragging that node will move it around the canvas. Adding
* the `"nodrag"` class prevents this behavior and this prop allows you to change the name of that
* class.
* @default "nodrag"
*/
noDragClassName?: string; noDragClassName?: string;
/**
* Typically, scrolling the mouse wheel when the mouse is over the canvas will zoom the viewport.
* Adding the `"nowheel"` class to an element n the canvas will prevent this behavior and this prop
* allows you to change the name of that class.
* @default "nowheel"
*/
noWheelClassName?: string; noWheelClassName?: string;
/**
* If an element in the canvas does not stop mouse events from propagating, clicking and dragging
* that element will pan the viewport. Adding the `"nopan"` class prevents this behavior and this
* prop allows you to change the name of that class.
* @default "nopan"
*/
noPanClassName?: string; noPanClassName?: string;
/** If set, initial viewport will show all nodes & edges */ /** When `true`, the flow will be zoomed and panned to fit all the nodes initially provided. */
fitView?: boolean; fitView?: boolean;
/** Options to be used in combination with fitView /**
* When you typically call `fitView` on a `ReactFlowInstance`, you can provide an object of
* options to customize its behavior. This prop lets you do the same for the initial `fitView`
* call.
* @example * @example
* const fitViewOptions = { * const fitViewOptions = {
* padding: 0.1, * padding: 0.1,
@@ -439,85 +592,105 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* }; * };
*/ */
fitViewOptions?: FitViewOptions; fitViewOptions?: FitViewOptions;
/**The connectOnClick option lets you click or tap on a source handle to start a connection /**
* The `connectOnClick` option lets you click or tap on a source handle to start a connection
* and then click on a target handle to complete the connection. * and then click on a target handle to complete the connection.
* *
* If you set this option to false, users will need to drag the connection line to the target * If you set this option to `false`, users will need to drag the connection line to the target
* handle to create a connection. * handle to create a connection.
* @default true
*/ */
connectOnClick?: boolean; connectOnClick?: boolean;
/** Set position of the attribution /**
* By default, React Flow will render a small attribution in the bottom right corner of the flow.
*
* You can use this prop to change its position in case you want to place something else there.
* @default 'bottom-right' * @default 'bottom-right'
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' * @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
*/ */
attributionPosition?: PanelPosition; attributionPosition?: PanelPosition;
/** By default, we render a small attribution in the corner of your flows that links back to the project. /**
* By default, we render a small attribution in the corner of your flows that links back to the project.
* *
* Anyone is free to remove this attribution whether they're a Pro subscriber or not * Anyone is free to remove this attribution whether they're a Pro subscriber or not
* but we ask that you take a quick look at our {@link https://reactflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide} * but we ask that you take a quick look at our {@link https://reactflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide}
* before doing so. * before doing so.
*/ */
proOptions?: ProOptions; proOptions?: ProOptions;
/** Enabling this option will raise the z-index of nodes when they are selected. /**
* Enabling this option will raise the z-index of nodes when they are selected.
* @default true * @default true
*/ */
elevateNodesOnSelect?: boolean; elevateNodesOnSelect?: boolean;
/** Enabling this option will raise the z-index of edges when they are selected. /**
* @default true * Enabling this option will raise the z-index of edges when they are selected.
*/ */
elevateEdgesOnSelect?: boolean; elevateEdgesOnSelect?: boolean;
/** /**
* Can be set true if built-in keyboard controls should be disabled. * You can use this prop to disable keyboard accessibility features such as selecting nodes or
* moving selected nodes with the arrow keys.
* @default false * @default false
*/ */
disableKeyboardA11y?: boolean; disableKeyboardA11y?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node. /**
* When `true`, the viewport will pan automatically when the cursor moves to the edge of the
* viewport while dragging a node.
* @default true * @default true
*/ */
autoPanOnNodeDrag?: boolean; autoPanOnNodeDrag?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node. /**
* When `true`, the viewport will pan automatically when the cursor moves to the edge of the
* viewport while creating a connection.
* @default true * @default true
*/ */
autoPanOnConnect?: boolean; autoPanOnConnect?: boolean;
/** The speed at which the viewport pans while dragging a node or a selection box. /**
* The speed at which the viewport pans while dragging a node or a selection box.
* @default 15 * @default 15
*/ */
autoPanSpeed?: number; autoPanSpeed?: number;
/** You can enable this prop to automatically pan the viewport while making a new connection. /**
* @default true * The radius around a handle where you drop a connection line to create a new edge.
* @default 20
*/ */
connectionRadius?: number; connectionRadius?: number;
/** Ocassionally something may happen that causes Svelte Flow to throw an error. /**
* Occasionally something may happen that causes React Flow to throw an error.
* *
* Instead of exploding your application, we log a message to the console and then call this event handler. * Instead of exploding your application, we log a message to the console and then call this event handler.
* You might use it for additional logging or to show a message to the user. * You might use it for additional logging or to show a message to the user.
*/ */
onError?: OnError; onError?: OnError;
/** This callback can be used to validate a new connection /**
* This callback can be used to validate a new connection
* *
* If you return false, the edge will not be added to your flow. * If you return `false`, the edge will not be added to your flow.
* If you have custom connection logic its preferred to use this callback over the isValidConnection prop on the handle component for performance reasons. * If you have custom connection logic its preferred to use this callback over the
* @default (connection: Connection) => true * `isValidConnection` prop on the handle component for performance reasons.
*/ */
isValidConnection?: IsValidConnection<EdgeType>; isValidConnection?: IsValidConnection<EdgeType>;
/** With a threshold greater than zero you can control the distinction between node drag and click events. /**
* With a threshold greater than zero you can delay node drag events.
* *
* If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired. * If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired.
*
* 1 is the default value, so clicks don't trigger drag events.
* @default 1 * @default 1
*/ */
nodeDragThreshold?: number; nodeDragThreshold?: number;
/** Sets a fixed width for the flow */ /** Sets a fixed width for the flow. */
width?: number; width?: number;
/** Sets a fixed height for the flow */ /** Sets a fixed height for the flow. */
height?: number; height?: number;
/** Controls color scheme used for styling the flow /**
* @default 'system' * Controls color scheme used for styling the flow.
* @default 'light'
* @example 'system' | 'light' | 'dark' * @example 'system' | 'light' | 'dark'
*/ */
colorMode?: ColorMode; colorMode?: ColorMode;
/** If set true, some debug information will be logged to the console like which events are fired. /**
* * If set `true`, some debug information will be logged to the console like which events are fired.
* @default undefined * @default false
*/ */
debug?: boolean; debug?: boolean;
} }
+70 -7
View File
@@ -19,8 +19,18 @@ import type {
import { EdgeTypes, InternalNode, Node } from '.'; import { EdgeTypes, InternalNode, Node } from '.';
/**
* @inline
*/
export type EdgeLabelOptions = { export type EdgeLabelOptions = {
label?: string | ReactNode; /**
* The label or custom element to render along the edge. This is commonly a text label or some
* custom controls.
*/
label?: ReactNode;
/**
* Custom styles to apply to the label.
*/
labelStyle?: CSSProperties; labelStyle?: CSSProperties;
labelShowBg?: boolean; labelShowBg?: boolean;
labelBgStyle?: CSSProperties; labelBgStyle?: CSSProperties;
@@ -29,7 +39,8 @@ export type EdgeLabelOptions = {
}; };
/** /**
* The Edge type is mainly used for the `edges` that get passed to the ReactFlow component * An `Edge` is the complete description with everything React Flow needs
* to know in order to render it.
* @public * @public
*/ */
export type Edge< export type Edge<
@@ -39,6 +50,11 @@ export type Edge<
EdgeLabelOptions & { EdgeLabelOptions & {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
/**
* Determines whether the edge can be updated by dragging the source or target to a new node.
* This property will override the default set by the `edgesReconnectable` prop on the
* `<ReactFlow />` component.
*/
reconnectable?: boolean | HandleType; reconnectable?: boolean | HandleType;
focusable?: boolean; focusable?: boolean;
}; };
@@ -91,17 +107,26 @@ export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
disableKeyboardA11y?: boolean; disableKeyboardA11y?: boolean;
}; };
/**
* Many properties on an [`Edge`](/api-reference/types/edge) are optional. When a new edge is created,
* the properties that are not provided will be filled in with the default values
* passed to the `defaultEdgeOptions` prop of the [`<ReactFlow />`](/api-reference/react-flow#defaultedgeoptions) component.
*/
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>; export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
export type EdgeTextProps = SVGAttributes<SVGElement> & export type EdgeTextProps = Omit<SVGAttributes<SVGElement>, 'x' | 'y'> &
EdgeLabelOptions & { EdgeLabelOptions & {
/** The x position where the label should be rendered. */
x: number; x: number;
/** The y position where the label should be rendered. */
y: number; y: number;
}; };
/** /**
* Custom edge component props * When you implement a custom edge it is wrapped in a component that enables some
* basic functionality. The `EdgeProps` type is the props that are passed to this.
* @public * @public
* @expand
*/ */
export type EdgeProps<EdgeType extends Edge = Edge> = Pick< export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
EdgeType, EdgeType,
@@ -121,22 +146,42 @@ export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
/** /**
* BaseEdge component props * BaseEdge component props
* @public * @public
* @expand
*/ */
export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd'> & export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd' | 'path' | 'markerStart' | 'markerEnd'> &
EdgeLabelOptions & { EdgeLabelOptions & {
/** Additional padding where interacting with an edge is still possible */ /**
* The width of the invisible area around the edge that the user can interact with. This is
* useful for making the edge easier to click or hover over.
* @default 20
*/
interactionWidth?: number; interactionWidth?: number;
/** The x position of edge label */ /** The x position of edge label */
labelX?: number; labelX?: number;
/** The y position of edge label */ /** The y position of edge label */
labelY?: number; labelY?: number;
/** SVG path of the edge */ /**
* The SVG path string that defines the edge. This should look something like
* `'M 0 0 L 100 100'` for a simple line. The utility functions like `getSimpleBezierEdge` can
* be used to generate this string for you.
*/
path: string; path: string;
/**
* The id of the SVG marker to use at the start of the edge. This should be defined in a
* `<defs>` element in a separate SVG document or element.
*/
markerStart?: string;
/**
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
* element in a separate SVG document or element.
*/
markerEnd?: string;
}; };
/** /**
* Helper type for edge components that get exported by the library * Helper type for edge components that get exported by the library
* @public * @public
* @expand
*/ */
export type EdgeComponentProps = EdgePosition & export type EdgeComponentProps = EdgePosition &
EdgeLabelOptions & { EdgeLabelOptions & {
@@ -156,39 +201,53 @@ export type EdgeComponentWithPathOptions<PathOptions> = EdgeComponentProps & {
/** /**
* BezierEdge component props * BezierEdge component props
* @public * @public
* @expand
*/ */
export type BezierEdgeProps = EdgeComponentWithPathOptions<BezierPathOptions>; export type BezierEdgeProps = EdgeComponentWithPathOptions<BezierPathOptions>;
/** /**
* SmoothStepEdge component props * SmoothStepEdge component props
* @public * @public
* @expand
*/ */
export type SmoothStepEdgeProps = EdgeComponentWithPathOptions<SmoothStepPathOptions>; export type SmoothStepEdgeProps = EdgeComponentWithPathOptions<SmoothStepPathOptions>;
/** /**
* StepEdge component props * StepEdge component props
* @public * @public
* @expand
*/ */
export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>; export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
/** /**
* StraightEdge component props * StraightEdge component props
* @public * @public
* @expand
*/ */
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>; export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
/** /**
* SimpleBezier component props * SimpleBezier component props
* @public * @public
* @expand
*/ */
export type SimpleBezierEdgeProps = EdgeComponentProps; export type SimpleBezierEdgeProps = EdgeComponentProps;
export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void; export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void;
/**
* If you want to render a custom component for connection lines, you can set the
* `connectionLineComponent` prop on the [`<ReactFlow />`](/api-reference/react-flow#connection-connectionLineComponent)
* component. The `ConnectionLineComponentProps` are passed to your custom component.
*
* @public
*/
export type ConnectionLineComponentProps<NodeType extends Node = Node> = { export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
/** The node the connection line originates from. */
fromNode: InternalNode<NodeType>; fromNode: InternalNode<NodeType>;
/** The handle on the `fromNode` that the connection line originates from. */
fromHandle: Handle; fromHandle: Handle;
fromX: number; fromX: number;
fromY: number; fromY: number;
@@ -196,6 +255,10 @@ export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
toY: number; toY: number;
fromPosition: Position; fromPosition: Position;
toPosition: Position; toPosition: Position;
/**
* If there is an `isValidConnection` callback, this prop will be set to `"valid"` or `"invalid"`
* based on the return value of that callback. Otherwise, it will be `null`.
*/
connectionStatus: 'valid' | 'invalid' | null; connectionStatus: 'valid' | 'invalid' | null;
toNode: InternalNode<NodeType> | null; toNode: InternalNode<NodeType> | null;
toHandle: Handle | null; toHandle: Handle | null;
+66 -25
View File
@@ -18,11 +18,44 @@ import {
import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.'; import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
/**
* This type can be used to type the `onNodesChange` function with a custom node type.
*
* @public
*
* @example
*
* ```ts
* const onNodesChange: OnNodesChange<MyNodeType> = useCallback((changes) => {
* setNodes((nodes) => applyNodeChanges(nodes, changes));
* },[]);
* ```
*/
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void; export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
/**
* This type can be used to type the `onEdgesChange` function with a custom edge type.
*
* @public
*
* @example
*
* ```ts
* const onEdgesChange: OnEdgesChange<MyEdgeType> = useCallback((changes) => {
* setEdges((edges) => applyEdgeChanges(edges, changes));
* },[]);
* ```
*/
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void; export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
export type OnNodesDelete<NodeType extends Node = Node> = (nodes: NodeType[]) => void; export type OnNodesDelete<NodeType extends Node = Node> = (nodes: NodeType[]) => void;
export type OnEdgesDelete<EdgeType extends Edge = Edge> = (edges: EdgeType[]) => void; export type OnEdgesDelete<EdgeType extends Edge = Edge> = (edges: EdgeType[]) => void;
/**
* This type can be used to type the `onDelete` function with a custom node and edge type.
*
* @public
*/
export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: { export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: {
nodes: NodeType[]; nodes: NodeType[];
edges: EdgeType[]; edges: EdgeType[];
@@ -39,6 +72,7 @@ export type NodeTypes = Record<
} }
> >
>; >;
export type EdgeTypes = Record< export type EdgeTypes = Record<
string, string,
ComponentType< ComponentType<
@@ -51,25 +85,38 @@ export type EdgeTypes = Record<
> >
>; >;
export type UnselectNodesAndEdgesParams = { export type UnselectNodesAndEdgesParams<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes?: Node[]; nodes?: NodeType[];
edges?: Edge[]; edges?: EdgeType[];
}; };
export type OnSelectionChangeParams = { export type OnSelectionChangeParams<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes: Node[]; nodes: NodeType[];
edges: Edge[]; edges: EdgeType[];
}; };
export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void; export type OnSelectionChangeFunc<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
params: OnSelectionChangeParams<NodeType, EdgeType>
) => void;
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>; export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
/**
* When calling [`fitView`](/api-reference/types/react-flow-instance#fitview) these options
* can be used to customize the behaviour. For example, the `duration` option can be used to
* transform the viewport smoothly over a given amount of time.
*
* @public
*/
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>; export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
export type FitView = (fitViewOptions?: FitViewOptions) => Promise<boolean>; export type FitView<NodeType extends Node = Node> = (fitViewOptions?: FitViewOptions<NodeType>) => Promise<boolean>;
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ( export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType> reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
) => void; ) => void;
/**
* @inline
*/
export type ViewportHelperFunctions = { export type ViewportHelperFunctions = {
/** /**
* Zooms viewport in by 1.2. * Zooms viewport in by 1.2.
@@ -84,14 +131,15 @@ export type ViewportHelperFunctions = {
*/ */
zoomOut: ZoomInOut; zoomOut: ZoomInOut;
/** /**
* Sets the current zoom level. * Zoom the viewport to a given zoom level. Passing in a `duration` will animate the viewport to
* the new zoom level.
* *
* @param zoomLevel - the zoom level to set * @param zoomLevel - the zoom level to set
* @param options.duration - optional duration. If set, a transition will be applied * @param options.duration - optional duration. If set, a transition will be applied
*/ */
zoomTo: ZoomTo; zoomTo: ZoomTo;
/** /**
* Returns the current zoom level. * Get the current zoom level of the viewport.
* *
* @returns current zoom as a number * @returns current zoom as a number
*/ */
@@ -110,18 +158,8 @@ export type ViewportHelperFunctions = {
*/ */
getViewport: GetViewport; getViewport: GetViewport;
/** /**
* Fits the view. * Center the viewport on a given position. Passing in a `duration` will animate the viewport to
* * the new position.
* @param options.padding - optional padding
* @param options.includeHiddenNodes - optional includeHiddenNodes
* @param options.minZoom - optional minZoom
* @param options.maxZoom - optional maxZoom
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.nodes - optional nodes to fit the view to
*/
fitView: FitView;
/**
* Sets the center of the view to the given position.
* *
* @param x - x position * @param x - x position
* @param y - y position * @param y - y position
@@ -129,14 +167,17 @@ export type ViewportHelperFunctions = {
*/ */
setCenter: SetCenter; setCenter: SetCenter;
/** /**
* Fits the view to the given bounds . * A low-level utility function to fit the viewport to a given rectangle. By passing in a
* `duration`, the viewport will animate from its current position to the new position. The
* `padding` option can be used to add space around the bounds.
* *
* @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to * @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
* @param options.padding - optional padding * @param options.padding - optional padding
*/ */
fitBounds: FitBounds; fitBounds: FitBounds;
/** /**
* Converts a screen / client position to a flow position. * With this function you can translate a screen pixel position to a flow position. It is useful
* for implementing drag and drop from a sidebar for example.
* *
* @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY * @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @param options.snapToGrid - if true, the converted position will be snapped to the grid * @param options.snapToGrid - if true, the converted position will be snapped to the grid
@@ -147,7 +188,7 @@ export type ViewportHelperFunctions = {
*/ */
screenToFlowPosition: (clientPosition: XYPosition, options?: { snapToGrid: boolean }) => XYPosition; screenToFlowPosition: (clientPosition: XYPosition, options?: { snapToGrid: boolean }) => XYPosition;
/** /**
* Converts a flow position to a screen / client position. * Translate a position inside the flow's canvas to a screen pixel position.
* *
* @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY * @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @returns position as { x: number, y: number } * @returns position as { x: number, y: number }
+46 -11
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system'; import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.'; import type { Node, Edge, ViewportHelperFunctions, InternalNode, FitView } from '.';
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = { export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes: NodeType[]; nodes: NodeType[];
@@ -13,6 +13,9 @@ export type DeleteElementsOptions = {
edges?: (Edge | { id: Edge['id'] })[]; edges?: (Edge | { id: Edge['id'] })[];
}; };
/**
* @inline
*/
export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge = Edge> = { export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
/** /**
* Returns nodes. * Returns nodes.
@@ -21,13 +24,17 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
*/ */
getNodes: () => NodeType[]; getNodes: () => NodeType[];
/** /**
* Sets nodes. * Set your nodes array to something else by either overwriting it with a new array or by passing
* in a function to update the existing array. If using a function, it is important to make sure a
* new array is returned instead of mutating the existing array. Calling this function will
* trigger the `onNodesChange` handler in a controlled flow.
* *
* @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes * @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes
*/ */
setNodes: (payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])) => void; setNodes: (payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])) => void;
/** /**
* Adds nodes. * Add one or many nodes to your existing nodes array. Calling this function will trigger the
* `onNodesChange` handler in a controlled flow.
* *
* @param payload - the nodes to add * @param payload - the nodes to add
*/ */
@@ -53,13 +60,17 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
*/ */
getEdges: () => EdgeType[]; getEdges: () => EdgeType[];
/** /**
* Sets edges. * Set your edges array to something else by either overwriting it with a new array or by passing
* in a function to update the existing array. If using a function, it is important to make sure a
* new array is returned instead of mutating the existing array. Calling this function will
* trigger the `onEdgesChange` handler in a controlled flow.
* *
* @param payload - the edges to set or a function that receives the current edges and returns the new edges * @param payload - the edges to set or a function that receives the current edges and returns the new edges
*/ */
setEdges: (payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])) => void; setEdges: (payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])) => void;
/** /**
* Adds edges. * Add one or many edges to your existing edges array. Calling this function will trigger the
* `onEdgesChange` handler in a controlled flow.
* *
* @param payload - the edges to add * @param payload - the edges to add
*/ */
@@ -90,7 +101,8 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
deletedEdges: Edge[]; deletedEdges: Edge[];
}>; }>;
/** /**
* Returns all nodes that intersect with the given node or rect. * Find all the nodes currently intersecting with a given node or rectangle. The `partially`
* parameter can be set to `true` to include nodes that are only partially intersecting.
* *
* @param node - the node or rect to check for intersections * @param node - the node or rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
@@ -104,7 +116,8 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
nodes?: NodeType[] nodes?: NodeType[]
) => NodeType[]; ) => NodeType[];
/** /**
* Checks if the given node or rect intersects with the passed rect. * Determine if a given node or rectangle is intersecting with another rectangle. The `partially`
* parameter can be set to true return `true` even if the node is only partially intersecting.
* *
* @param node - the node or rect to check for intersections * @param node - the node or rect to check for intersections
* @param area - the rect to check for intersections * @param area - the rect to check for intersections
@@ -182,7 +195,8 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
*/ */
getNodesBounds: (nodes: (NodeType | InternalNode | string)[]) => Rect; getNodesBounds: (nodes: (NodeType | InternalNode | string)[]) => Rect;
/** /**
* Gets all connections for a given handle belonging to a specific node. * Get all the connections of a handle belonging to a specific node. The type parameter be either
* `'source'` or `'target'`.
* @deprecated * @deprecated
* @param type - handle type 'source' or 'target' * @param type - handle type 'source' or 'target'
* @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle) * @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle)
@@ -200,7 +214,6 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
}) => HandleConnection[]; }) => HandleConnection[];
/** /**
* Gets all connections to a node. Can be filtered by handle type and id. * Gets all connections to a node. Can be filtered by handle type and id.
* @deprecated use `getNodeConnections` instead
* @param type - handle type 'source' or 'target' * @param type - handle type 'source' or 'target'
* @param handleId - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle) * @param handleId - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle)
* @param nodeId - the node id the handle belongs to * @param nodeId - the node id the handle belongs to
@@ -215,12 +228,34 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
nodeId: string; nodeId: string;
handleId?: string | null; handleId?: string | null;
}) => NodeConnection[]; }) => NodeConnection[];
// /**
// * Fits the view.
// *
// * @param options.padding - optional padding
// * @param options.includeHiddenNodes - optional includeHiddenNodes
// * @param options.minZoom - optional minZoom
// * @param options.maxZoom - optional maxZoom
// * @param options.duration - optional duration. If set, a transition will be applied
// * @param options.nodes - optional nodes to fit the view to
// */
fitView: FitView<NodeType>;
}; };
/**
* The `ReactFlowInstance` provides a collection of methods to query and manipulate
* the internal state of your flow. You can get an instance by using the
* [`useReactFlow`](/api-reference/hooks/use-react-flow) hook or attaching a listener
* to the [`onInit`](/api-reference/react-flow#event-oninit) event.
*
* @public
*/
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers< export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<
NodeType, NodeType,
EdgeType EdgeType
> & > &
Omit<ViewportHelperFunctions, 'initialized'> & { ViewportHelperFunctions & {
/**
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
* This property tells you when viewport is initialized.
*/
viewportInitialized: boolean; viewportInitialized: boolean;
}; };
+46 -4
View File
@@ -4,7 +4,10 @@ import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, I
import { NodeTypes } from './general'; import { NodeTypes } from './general';
/** /**
* The node data structure that gets used for the nodes prop. * The `Node` type represents everything React Flow needs to know about a given node.
* Whenever you want to update a certain attribute of a node, you need to create a new
* node object.
*
* @public * @public
*/ */
export type Node< export type Node<
@@ -18,9 +21,10 @@ export type Node<
}; };
/** /**
* The node data structure that gets used for internal nodes. * The `InternalNode` type is identical to the base [`Node`](/api-references/types/node)
* There are some data structures added under node.internal * type but is extended with some additional properties used internally.
* that are needed for tracking some properties * Some functions and callbacks that return nodes may return an `InternalNode`.
*
* @public * @public
*/ */
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>; export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
@@ -56,8 +60,46 @@ export type NodeWrapperProps<NodeType extends Node> = {
nodeClickDistance?: number; nodeClickDistance?: number;
}; };
/**
* The `BuiltInNode` type represents the built-in node types that are available in React Flow.
* You can use this type to extend your custom node type if you still want ot use the built-in ones.
*
* @public
* @example
* ```ts
* type CustomNode = Node<{ value: number }, 'custom'>;
* type MyAppNode = CustomNode | BuiltInNode;
* ```
*/
export type BuiltInNode = export type BuiltInNode =
| Node<{ label: string }, 'input' | 'output' | 'default'> | Node<{ label: string }, 'input' | 'output' | 'default'>
| Node<Record<string, never>, 'group'>; | Node<Record<string, never>, 'group'>;
/**
* When you implement a [custom node](/learn/customization/custom-nodes) it is
* wrapped in a component that enables basic functionality like selection and
* dragging. Your custom node receives `NodeProps` as props.
*
* @public
* @example
* ```tsx
*import { useState } from 'react';
*import { NodeProps, Node } from '@xyflow/react';
*
*export type CounterNode = Node<{ initialCount?: number }, 'counter'>;
*
*export default function CounterNode(props: NodeProps<CounterNode>) {
* const [count, setCount] = useState(props.data?.initialCount ?? 0);
*
* return (
* <div>
* <p>Count: {count}</p>
* <button className="nodrag" onClick={() => setCount(count + 1)}>
* Increment
* </button>
* </div>
* );
*}
*```
*/
export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType>; export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType>;
+5 -7
View File
@@ -119,9 +119,9 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
connectOnClick: boolean; connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions; defaultEdgeOptions?: DefaultEdgeOptions;
fitViewOnInit: boolean; fitViewQueued: boolean;
fitViewDone: boolean; fitViewOptions: FitViewOptions | undefined;
fitViewOnInitOptions: FitViewOptions | undefined; fitViewResolver: PromiseWithResolvers<boolean> | null;
onNodesDelete?: OnNodesDelete<NodeType>; onNodesDelete?: OnNodesDelete<NodeType>;
onEdgesDelete?: OnEdgesDelete<EdgeType>; onEdgesDelete?: OnEdgesDelete<EdgeType>;
@@ -134,7 +134,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
onViewportChangeEnd?: OnViewportChange; onViewportChangeEnd?: OnViewportChange;
onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>; onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
onSelectionChangeHandlers: OnSelectionChangeFunc[]; onSelectionChangeHandlers: OnSelectionChangeFunc<NodeType, EdgeType>[];
ariaLiveMessage: string; ariaLiveMessage: string;
autoPanOnConnect: boolean; autoPanOnConnect: boolean;
@@ -155,7 +155,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>, params?: { triggerFitView: boolean }) => void; updateNodeInternals: (updates: Map<string, InternalNodeUpdate>, params?: { triggerFitView: boolean }) => void;
updateNodePositions: UpdateNodePositions; updateNodePositions: UpdateNodePositions;
resetSelectedElements: () => void; resetSelectedElements: () => void;
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void; unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams<NodeType, EdgeType>) => void;
addSelectedNodes: (nodeIds: string[]) => void; addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void; addSelectedEdges: (edgeIds: string[]) => void;
setMinZoom: (minZoom: number) => void; setMinZoom: (minZoom: number) => void;
@@ -168,8 +168,6 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void; triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void; triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy; panBy: PanBy;
fitView: (options?: FitViewOptions) => Promise<boolean>;
fitViewSync: (options?: FitViewOptions) => boolean;
setPaneClickDistance: (distance: number) => void; setPaneClickDistance: (distance: number) => void;
}; };
+86 -48
View File
@@ -11,13 +11,17 @@ import {
} from '@xyflow/system'; } from '@xyflow/system';
import type { Node, Edge, InternalNode } from '../types'; import type { Node, Edge, InternalNode } from '../types';
// This function applies changes to nodes or edges that are triggered by React Flow internally. /*
// When you drag a node for example, React Flow will send a position change update. * This function applies changes to nodes or edges that are triggered by React Flow internally.
// This function then applies the changes and returns the updated elements. * When you drag a node for example, React Flow will send a position change update.
* This function then applies the changes and returns the updated elements.
*/
function applyChanges(changes: any[], elements: any[]): any[] { function applyChanges(changes: any[], elements: any[]): any[] {
const updatedElements: any[] = []; const updatedElements: any[] = [];
// By storing a map of changes for each element, we can a quick lookup as we /*
// iterate over the elements array! * By storing a map of changes for each element, we can a quick lookup as we
* iterate over the elements array!
*/
const changesMap = new Map<any, any[]>(); const changesMap = new Map<any, any[]>();
const addItemChanges: any[] = []; const addItemChanges: any[] = [];
@@ -26,15 +30,19 @@ function applyChanges(changes: any[], elements: any[]): any[] {
addItemChanges.push(change); addItemChanges.push(change);
continue; continue;
} else if (change.type === 'remove' || change.type === 'replace') { } else if (change.type === 'remove' || change.type === 'replace') {
// For a 'remove' change we can safely ignore any other changes queued for /*
// the same element, it's going to be removed anyway! * For a 'remove' change we can safely ignore any other changes queued for
* the same element, it's going to be removed anyway!
*/
changesMap.set(change.id, [change]); changesMap.set(change.id, [change]);
} else { } else {
const elementChanges = changesMap.get(change.id); const elementChanges = changesMap.get(change.id);
if (elementChanges) { if (elementChanges) {
// If we have some changes queued already, we can do a mutable update of /*
// that array and save ourselves some copying. * If we have some changes queued already, we can do a mutable update of
* that array and save ourselves some copying.
*/
elementChanges.push(change); elementChanges.push(change);
} else { } else {
changesMap.set(change.id, [change]); changesMap.set(change.id, [change]);
@@ -45,8 +53,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
for (const element of elements) { for (const element of elements) {
const changes = changesMap.get(element.id); const changes = changesMap.get(element.id);
// When there are no changes for an element we can just push it unmodified, /*
// no need to copy it. * When there are no changes for an element we can just push it unmodified,
* no need to copy it.
*/
if (!changes) { if (!changes) {
updatedElements.push(element); updatedElements.push(element);
continue; continue;
@@ -62,9 +72,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
continue; continue;
} }
// For other types of changes, we want to start with a shallow copy of the /**
// object so React knows this element has changed. Sequential changes will * For other types of changes, we want to start with a shallow copy of the
/// each _mutate_ this object, so there's only ever one copy. * object so React knows this element has changed. Sequential changes will
* each _mutate_ this object, so there's only ever one copy.
*/
const updatedElement = { ...element }; const updatedElement = { ...element };
for (const change of changes) { for (const change of changes) {
@@ -74,8 +86,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updatedElements.push(updatedElement); updatedElements.push(updatedElement);
} }
// we need to wait for all changes to be applied before adding new items /*
// to be able to add them at the correct index * we need to wait for all changes to be applied before adding new items
* to be able to add them at the correct index
*/
if (addItemChanges.length) { if (addItemChanges.length) {
addItemChanges.forEach((change) => { addItemChanges.forEach((change) => {
if (change.index !== undefined) { if (change.index !== undefined) {
@@ -133,22 +147,33 @@ function applyChange(change: any, element: any): any {
/** /**
* Drop in function that applies node changes to an array of nodes. * Drop in function that applies node changes to an array of nodes.
* @public * @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way. * @param changes - Array of changes to apply.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param nodes - Array of nodes to apply the changes to.
* @param changes - Array of changes to apply * @returns Array of updated nodes.
* @param nodes - Array of nodes to apply the changes to
* @returns Array of updated nodes
* @example * @example
* const onNodesChange = useCallback( *```tsx
(changes) => { *import { useState, useCallback } from 'react';
setNodes((oldNodes) => applyNodeChanges(changes, oldNodes)); *import { ReactFlow, applyNodeChanges, type Node, type Edge, type OnNodesChange } from '@xyflow/react';
}, *
[setNodes], *export default function Flow() {
); * const [nodes, setNodes] = useState<Node[]>([]);
* const [edges, setEdges] = useState<Edge[]>([]);
return ( * const onNodesChange: OnNodesChange = useCallback(
<ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} /> * (changes) => {
); * setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
* },
* [setNodes],
* );
*
* return (
* <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
* );
*}
*```
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange}
* that describes how to update the edges of your flow in some way.
* If you don't need any custom behaviour, this util can be used to take an array
* of these changes and apply them to your edges.
*/ */
export function applyNodeChanges<NodeType extends Node = Node>( export function applyNodeChanges<NodeType extends Node = Node>(
changes: NodeChange<NodeType>[], changes: NodeChange<NodeType>[],
@@ -160,22 +185,33 @@ export function applyNodeChanges<NodeType extends Node = Node>(
/** /**
* Drop in function that applies edge changes to an array of edges. * Drop in function that applies edge changes to an array of edges.
* @public * @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way. * @param changes - Array of changes to apply.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param edges - Array of edge to apply the changes to.
* @param changes - Array of changes to apply * @returns Array of updated edges.
* @param edges - Array of edge to apply the changes to
* @returns Array of updated edges
* @example * @example
* ```tsx
*import { useState, useCallback } from 'react';
*import { ReactFlow, applyEdgeChanges } from '@xyflow/react';
*
*export default function Flow() {
* const [nodes, setNodes] = useState([]);
* const [edges, setEdges] = useState([]);
* const onEdgesChange = useCallback( * const onEdgesChange = useCallback(
(changes) => { * (changes) => {
setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges)); * setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
}, * },
[setEdges], * [setEdges],
); * );
*
return ( * return (
<ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} /> * <ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
); * );
*}
*```
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange}
* that describes how to update the edges of your flow in some way.
* If you don't need any custom behaviour, this util can be used to take an array
* of these changes and apply them to your edges.
*/ */
export function applyEdgeChanges<EdgeType extends Edge = Edge>( export function applyEdgeChanges<EdgeType extends Edge = Edge>(
changes: EdgeChange<EdgeType>[], changes: EdgeChange<EdgeType>[],
@@ -205,9 +241,11 @@ export function getSelectionChanges(
// we don't want to set all items to selected=false on the first selection // we don't want to set all items to selected=false on the first selection
if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) { if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
if (mutateItem) { if (mutateItem) {
// this hack is needed for nodes. When the user dragged a node, it's selected. /*
// When another node gets dragged, we need to deselect the previous one, * this hack is needed for nodes. When the user dragged a node, it's selected.
// in order to have only one selected node at a time - the onNodesChange callback comes too late here :/ * When another node gets dragged, we need to deselect the previous one,
* in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
*/
item.selected = willBeSelected; item.selected = willBeSelected;
} }
changes.push(createSelectionChange(item.id, willBeSelected)); changes.push(createSelectionChange(item.id, willBeSelected));
+35 -7
View File
@@ -1,29 +1,57 @@
import { type Ref, type RefAttributes, forwardRef } from 'react'; import { type Ref, type RefAttributes, forwardRef, JSX } from 'react';
import { isNodeBase, isEdgeBase } from '@xyflow/system'; import { isNodeBase, isEdgeBase } from '@xyflow/system';
import type { Edge, Node } from '../types'; import type { Edge, Node } from '../types';
/** /**
* Test whether an object is useable as a Node * Test whether an object is usable as an [`Node`](/api-reference/types/node).
* In TypeScript this is a type guard that will narrow the type of whatever you pass in to
* [`Node`](/api-reference/types/node) if it returns `true`.
*
* @public * @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test * @param element - The element to test.
* @returns A boolean indicating whether the element is an Node * @returns Tests whether the provided value can be used as a `Node`. If you're using TypeScript,
* this function acts as a type guard and will narrow the type of the value to `Node` if it returns
* `true`.
*
* @example
* ```js
*import { isNode } from '@xyflow/react';
*
*if (isNode(node)) {
* // ...
*}
*```
*/ */
export const isNode = <NodeType extends Node = Node>(element: unknown): element is NodeType => export const isNode = <NodeType extends Node = Node>(element: unknown): element is NodeType =>
isNodeBase<NodeType>(element); isNodeBase<NodeType>(element);
/** /**
* Test whether an object is useable as an Edge * Test whether an object is usable as an [`Edge`](/api-reference/types/edge).
* In TypeScript this is a type guard that will narrow the type of whatever you pass in to
* [`Edge`](/api-reference/types/edge) if it returns `true`.
*
* @public * @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test * @param element - The element to test
* @returns A boolean indicating whether the element is an Edge * @returns Tests whether the provided value can be used as an `Edge`. If you're using TypeScript,
* this function acts as a type guard and will narrow the type of the value to `Edge` if it returns
* `true`.
*
* @example
* ```js
*import { isEdge } from '@xyflow/react';
*
*if (isEdge(edge)) {
* // ...
*}
*```
*/ */
export const isEdge = <EdgeType extends Edge = Edge>(element: unknown): element is EdgeType => export const isEdge = <EdgeType extends Edge = Edge>(element: unknown): element is EdgeType =>
isEdgeBase<EdgeType>(element); isEdgeBase<EdgeType>(element);
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/no-empty-object-type
export function fixedForwardRef<T, P = {}>( export function fixedForwardRef<T, P = {}>(
render: (props: P, ref: Ref<T>) => JSX.Element render: (props: P, ref: Ref<T>) => JSX.Element
): (props: P & RefAttributes<T>) => JSX.Element { ): (props: P & RefAttributes<T>) => JSX.Element {
+55
View File
@@ -1,5 +1,60 @@
# @xyflow/svelte # @xyflow/svelte
## 0.1.35
### Patch Changes
- [#5158](https://github.com/xyflow/xyflow/pull/5158) [`06696060`](https://github.com/xyflow/xyflow/commit/0669606050bb2138a44a1591176ac8e16afeb0f1) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Fix typo in TSDoc comments `React Flow` -> `Svelte Flow`
- Updated dependencies [[`02a3b746`](https://github.com/xyflow/xyflow/commit/02a3b74645799a3f0ce670b69365fa86ecb0616e), [`cbe305e1`](https://github.com/xyflow/xyflow/commit/cbe305e15a5c5d3b92583e0ec12364b2509f49bd), [`1f671bd4`](https://github.com/xyflow/xyflow/commit/1f671bd48f06230da841fdd1d7a312413ef16d03), [`aaebc462`](https://github.com/xyflow/xyflow/commit/aaebc462951ded8e91374c3e084d77af5ed7380a), [`6ec942fc`](https://github.com/xyflow/xyflow/commit/6ec942fc6501f81009c278cc995764bef3e8d03b)]:
- @xyflow/system@0.0.55
## 0.1.34
### Patch Changes
- [#5139](https://github.com/xyflow/xyflow/pull/5139) [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Use `rgba` for colors with opacity instead of `rgb` for `MiniMap` mask color
- Updated dependencies [[`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a), [`24a1bc89`](https://github.com/xyflow/xyflow/commit/24a1bc89348817ed9b5c87f74bf2519c705143be), [`36657cd6`](https://github.com/xyflow/xyflow/commit/36657cd66322c911e87eb37275c584a80025adfe), [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654), [`2ac6e155`](https://github.com/xyflow/xyflow/commit/2ac6e155e35256ca436281df16344366e7d05761), [`f0f378e5`](https://github.com/xyflow/xyflow/commit/f0f378e5b6918c2c30d9dc1e32587063cb942d4e)]:
- @xyflow/system@0.0.54
## 0.1.33
### Patch Changes
- [#5124](https://github.com/xyflow/xyflow/pull/5124) [`b76f7f9e`](https://github.com/xyflow/xyflow/commit/b76f7f9eb4841f139b1468b8eda0430ddd19a1ae) Thanks [@bjornosal](https://github.com/bjornosal)! - Export NodeConnection type
## 0.1.32
### Patch Changes
- [#5059](https://github.com/xyflow/xyflow/pull/5059) [`065ff89d`](https://github.com/xyflow/xyflow/commit/065ff89d10488f9c76c56870511e45eaed299778) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Prevent onPaneClick when connection is in progress. Closes [#5057](https://github.com/xyflow/xyflow/issues/5057)
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working immediately after adding new nodes
- [#5093](https://github.com/xyflow/xyflow/pull/5093) [`65825e89`](https://github.com/xyflow/xyflow/commit/65825e89a6e2e7591087eb41ac89da4da7095f8f) Thanks [@moklick](https://github.com/moklick)! - Hidden nodes are not displayed in the mini map anymore
- Updated dependencies [[`5d15b01b`](https://github.com/xyflow/xyflow/commit/5d15b01ba8cb349d6397a6ed8162848b4dfec293), [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe), [`a79f30b3`](https://github.com/xyflow/xyflow/commit/a79f30b3dd7c8ff6400c8d22214b2c2282e5bac1)]:
- @xyflow/system@0.0.53
## 0.1.31
### Patch Changes
- [#5019](https://github.com/xyflow/xyflow/pull/5019) [`3e80317c`](https://github.com/xyflow/xyflow/commit/3e80317cf6da0e9fdc111c3ade88f2a88a10dbd6) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Add `"./package.json" to the `exports` field so that users can import it
- Updated dependencies [[`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90)]:
- @xyflow/system@0.0.52
## 0.1.30
### Patch Changes
- [#5010](https://github.com/xyflow/xyflow/pull/5010) [`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919) Thanks [@moklick](https://github.com/moklick)! - Add more TSDocs to components, hooks, utils funcs and types
- Updated dependencies [[`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919), [`4947029c`](https://github.com/xyflow/xyflow/commit/4947029cd6cda0f695e1fb4815e4030adb232234), [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7)]:
- @xyflow/system@0.0.51
## 0.1.29 ## 0.1.29
### Patch Changes ### Patch Changes
+17 -16
View File
@@ -34,6 +34,7 @@
"type": "module", "type": "module",
"module": "./dist/lib/index.js", "module": "./dist/lib/index.js",
"exports": { "exports": {
"./package.json": "./package.json",
".": { ".": {
"types": "./dist/lib/index.d.ts", "types": "./dist/lib/index.d.ts",
"svelte": "./dist/lib/index.js", "svelte": "./dist/lib/index.js",
@@ -54,32 +55,32 @@
"@xyflow/system": "workspace:*" "@xyflow/system": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.1", "@sveltejs/kit": "^2.20.4",
"@sveltejs/package": "^2.3.9", "@sveltejs/package": "^2.3.10",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.22.0", "@typescript-eslint/parser": "^8.29.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.21",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"eslint": "^8.57.0", "eslint": "^9.24.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^3.5.1",
"postcss": "^8.5.1", "postcss": "^8.5.3",
"postcss-cli": "^11.0.0", "postcss-cli": "^11.0.1",
"postcss-combine-duplicated-selectors": "^10.0.3", "postcss-combine-duplicated-selectors": "^10.0.3",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2", "postcss-nested": "^7.0.2",
"postcss-rename": "^0.6.1", "postcss-rename": "^0.6.1",
"prettier": "^3.4.2", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.19.5", "svelte": "^5.25.8",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.5",
"svelte-eslint-parser": "^0.43.0", "svelte-eslint-parser": "^1.1.2",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.7.3" "typescript": "^5.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^5.16.0" "svelte": "^5.16.0"
@@ -75,7 +75,8 @@
function onClick(event: MouseEvent) { function onClick(event: MouseEvent) {
// We prevent click events when the user let go of the selectionKey during a selection // We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress) { // We also prevent click events when a connection is in progress
if (selectionInProgress || store.connection.inProgress) {
selectionInProgress = false; selectionInProgress = false;
return; return;
} }
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system'; import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
import zoom from '$lib/actions/zoom'; import zoom from '$lib/actions/zoom';
@@ -24,15 +23,8 @@
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag); let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll); let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
const onTransformChange = (transform: Transform) =>
(store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] });
// We extract the initial value by destructuring // We extract the initial value by destructuring
const { viewport: initialViewport } = store; const { viewport: initialViewport } = store;
onMount(() => {
store.viewportInitialized = true;
});
</script> </script>
<div <div
@@ -66,7 +58,9 @@
translateExtent: store.translateExtent, translateExtent: store.translateExtent,
lib: 'svelte', lib: 'svelte',
paneClickDistance, paneClickDistance,
onTransformChange onTransformChange: (transform: Transform) => {
store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
}
}} }}
> >
{@render children()} {@render children()}
+2 -1
View File
@@ -107,7 +107,8 @@ export {
type ResizeParams, type ResizeParams,
type ResizeParamsWithDirection, type ResizeParamsWithDirection,
type ResizeDragEvent, type ResizeDragEvent,
type IsValidConnection type IsValidConnection,
type NodeConnection
} from '@xyflow/system'; } from '@xyflow/system';
// system utils // system utils
@@ -63,7 +63,10 @@
}); });
let boundingRect = $derived( let boundingRect = $derived(
store.nodeLookup.size > 0 store.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(store.nodeLookup), viewBB) ? getBoundsOfRects(
getInternalNodesBounds(store.nodeLookup, { filter: (n) => !n.hidden }),
viewBB
)
: viewBB : viewBB
); );
let scaledWidth = $derived(boundingRect.width / width); let scaledWidth = $derived(boundingRect.width / width);
+34 -53
View File
@@ -1,5 +1,4 @@
import { import {
fitView as fitViewSystem,
panBy as panBySystem, panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem, updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil, addEdge as addEdgeUtil,
@@ -13,9 +12,7 @@ import {
type CoordinateExtent, type CoordinateExtent,
type UpdateConnection, type UpdateConnection,
type ConnectionState, type ConnectionState,
getFitViewNodes, updateAbsolutePositions
updateAbsolutePositions,
getDimensions
} from '@xyflow/system'; } from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -72,14 +69,8 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
nodeExtent: store.nodeExtent nodeExtent: store.nodeExtent
}); });
if (!store.fitViewOnInitDone && store.fitViewOnInit) { if (store.fitViewQueued) {
const fitViewOnInitDone = fitViewSync({ store.resolveFitView();
...store.fitViewOptions,
nodes: store.fitViewOptions?.nodes
});
if (fitViewOnInitDone) {
store.fitViewOnInitDone = fitViewOnInitDone;
}
} }
const newNodes = new Map<string, Node>(); const newNodes = new Map<string, Node>();
@@ -120,52 +111,41 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
} }
function fitView(options?: FitViewOptions) { function fitView(options?: FitViewOptions) {
const panZoom = store.panZoom; // const panZoom = store.panZoom;
const domNode = store.domNode; // const domNode = store.domNode;
if (!panZoom || !domNode) { // if (!panZoom || !domNode) {
return Promise.resolve(false); // return Promise.resolve(false);
} // }
const { width, height } = getDimensions(domNode); // const { width, height } = getDimensions(domNode);
const fitViewNodes = getFitViewNodes(store.nodeLookup, options); // const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
return fitViewSystem( // return fitViewSystem(
{ // {
nodes: fitViewNodes, // nodes: fitViewNodes,
width, // width,
height, // height,
minZoom: store.minZoom, // minZoom: store.minZoom,
maxZoom: store.maxZoom, // maxZoom: store.maxZoom,
panZoom // panZoom
}, // },
options // options
); // );e3
} // We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.fitViewResolver ?? Promise.withResolvers<boolean>();
function fitViewSync(options?: FitViewOptions) { // We schedule a fitView by setting fitViewQueued and triggering a setNodes
const panZoom = store.panZoom; store.fitViewQueued = true;
store.fitViewOptions = options;
store.fitViewResolver = fitViewResolver;
if (!panZoom) { // We need to update the nodes so that adoptUserNodes is triggered
return false; store.nodes = [...store.nodes];
}
const fitViewNodes = getFitViewNodes(store.nodeLookup, options); return fitViewResolver.promise;
fitViewSystem(
{
nodes: fitViewNodes,
width: store.width,
height: store.height,
minZoom: store.minZoom,
maxZoom: store.maxZoom,
panZoom
},
options
);
return fitViewNodes.size > 0;
} }
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) { function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
@@ -350,10 +330,11 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
} }
function reset() { function reset() {
store.fitViewOnInitDone = false;
store.selectionRect = null; store.selectionRect = null;
store.selectionRectMode = null; store.selectionRectMode = null;
store.resetStoreValues();
unselectNodesAndEdges(); unselectNodesAndEdges();
cancelConnection(); cancelConnection();
} }
@@ -366,7 +347,7 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
updateNodeInternals, updateNodeInternals,
zoomIn, zoomIn,
zoomOut, zoomOut,
fitView: (options?: FitViewOptions) => fitView(options), fitView,
setMinZoom, setMinZoom,
setMaxZoom, setMaxZoom,
setTranslateExtent, setTranslateExtent,
@@ -27,7 +27,8 @@ import {
type ParentLookup, type ParentLookup,
pointToRendererPoint, pointToRendererPoint,
type ColorModeClass, type ColorModeClass,
type Transform type Transform,
fitViewport
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -51,7 +52,6 @@ import type {
OnBeforeDelete, OnBeforeDelete,
IsValidConnection, IsValidConnection,
Edge, Edge,
Node,
EdgeLayouted, EdgeLayouted,
InternalNode InternalNode
} from '$lib/types'; } from '$lib/types';
@@ -78,14 +78,54 @@ export const getInitialStore = (signals: StoreSignals) => {
// We use a class here, because Svelte adds getters & setter for us. // We use a class here, because Svelte adds getters & setter for us.
// Inline classes have some performance implications but we just call it once (max twice). // Inline classes have some performance implications but we just call it once (max twice).
class SvelteFlowStore { class SvelteFlowStore {
_nodes: Node[] = $derived.by(() => { flowId: string = $derived(signals.props.id ?? '1');
adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, { domNode = $state<HTMLDivElement | null>(null);
panZoom: PanZoomInstance | null = $state(null);
width = $state<number>(signals.width ?? 0);
height = $state<number>(signals.height ?? 0);
nodesInitialized: boolean = $derived.by(() => {
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
nodeExtent: this.nodeExtent, nodeExtent: this.nodeExtent,
nodeOrigin: this.nodeOrigin, nodeOrigin: this.nodeOrigin,
elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true, elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true,
checkEquality: true checkEquality: true
}); });
return signals.nodes;
if (this.fitViewQueued && nodesInitialized) {
if (this.fitViewOptions?.duration) {
this.resolveFitView();
} else {
/**
* When no duration is set, viewport is set immediately which prevents an update
* I do not understand why, however we are setting state in a derived which is a no-go
*/
queueMicrotask(() => {
this.resolveFitView();
});
}
}
return nodesInitialized;
});
edgesInitialized: boolean = $state(false);
viewportInitialized: boolean = $derived(this.panZoom !== null);
// TODO: Figure out initialized
_initialNodesLength: number = signals.nodes?.length ?? 0;
_initialEdgesLength: number = signals.edges?.length ?? 0;
initialized: boolean = $derived.by(() => {
let initialized = false;
// if it hasn't been initialised check if it's now
if (this._initialNodesLength === 0) {
initialized = this.viewportInitialized;
} else if (this._initialEdgesLength === 0) {
initialized = this.viewportInitialized && this.nodesInitialized;
} else {
initialized = this.viewportInitialized && this.nodesInitialized && this.edgesInitialized;
}
return initialized;
}); });
_edges: Edge[] = $derived.by(() => { _edges: Edge[] = $derived.by(() => {
@@ -94,7 +134,9 @@ export const getInitialStore = (signals: StoreSignals) => {
}); });
get nodes() { get nodes() {
return this._nodes; // eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.nodesInitialized;
return signals.nodes;
} }
set nodes(nodes) { set nodes(nodes) {
signals.nodes = nodes; signals.nodes = nodes;
@@ -116,7 +158,7 @@ export const getInitialStore = (signals: StoreSignals) => {
const { const {
// We need to access this._nodes to trigger on changes // We need to access this._nodes to trigger on changes
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
_nodes, nodes,
_edges: edges, _edges: edges,
_prevVisibleEdges: previousEdges, _prevVisibleEdges: previousEdges,
nodeLookup, nodeLookup,
@@ -174,11 +216,6 @@ export const getInitialStore = (signals: StoreSignals) => {
signals.elementsSelectable = value; signals.elementsSelectable = value;
} }
domNode = $state<HTMLDivElement | null>(null);
width = $state<number>(signals.width ?? 0);
height = $state<number>(signals.height ?? 0);
flowId: string = $derived(signals.props.id ?? '1');
minZoom: number = $derived(signals.props.minZoom ?? 0.5); minZoom: number = $derived(signals.props.minZoom ?? 0.5);
maxZoom: number = $derived(signals.props.maxZoom ?? 2); maxZoom: number = $derived(signals.props.maxZoom ?? 2);
@@ -192,11 +229,10 @@ export const getInitialStore = (signals: StoreSignals) => {
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true); autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true); autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
fitViewOnInitDone: boolean = $state(false); fitViewQueued: boolean = signals.props.fitView ?? false;
fitViewOnInit: boolean = $derived(signals.props.fitView ?? false); fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
fitViewOptions: FitViewOptions | undefined = $derived(signals.props.fitViewOptions); fitViewResolver: PromiseWithResolvers<boolean> | null = null;
panZoom: PanZoomInstance | null = $state(null);
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null); snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
dragging: boolean = $state(false); dragging: boolean = $state(false);
@@ -215,15 +251,16 @@ export const getInitialStore = (signals: StoreSignals) => {
// _viewport is the internal viewport. // _viewport is the internal viewport.
// when binding to viewport, we operate on signals.viewport instead // when binding to viewport, we operate on signals.viewport instead
initial: boolean = true;
_viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 }); _viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 });
get viewport() { get viewport() {
return signals.viewport ?? this._viewport; return signals.viewport ?? this._viewport;
} }
set viewport(viewport: Viewport) { set viewport(newViewport: Viewport) {
if (signals.viewport) { if (signals.viewport) {
signals.viewport = viewport; signals.viewport = newViewport;
} }
this._viewport = viewport; this._viewport = newViewport;
} }
// _connection is viewport independent and originating from XYHandle // _connection is viewport independent and originating from XYHandle
@@ -271,25 +308,32 @@ export const getInitialStore = (signals: StoreSignals) => {
onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend); onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend);
onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete); onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete);
nodesInitialized: boolean = $state(false); resolveFitView = async () => {
edgesInitialized: boolean = $state(false); if (!this.panZoom) {
viewportInitialized: boolean = $state(false); return;
_initialNodesLength: number = signals.nodes?.length ?? 0;
_initialEdgesLength: number = signals.edges?.length ?? 0;
initialized: boolean = $derived.by(() => {
let initialized = false;
// if it hasn't been initialised check if it's now
if (this._initialNodesLength === 0) {
initialized = this.viewportInitialized;
} else if (this._initialEdgesLength === 0) {
initialized = this.viewportInitialized && this.nodesInitialized;
} else {
initialized = this.viewportInitialized && this.nodesInitialized && this.edgesInitialized;
} }
return initialized; await fitViewport(
}); {
nodes: this.nodeLookup,
width: this.width,
height: this.height,
panZoom: this.panZoom,
minZoom: this.minZoom,
maxZoom: this.maxZoom
},
this.fitViewOptions
);
this.fitViewResolver?.resolve(true);
/**
* wait for the fitViewport to resolve before deleting the resolver,
* we want to reuse the old resolver if the user calls fitView again in the mean time
*/
this.fitViewQueued = false;
this.fitViewOptions = undefined;
this.fitViewResolver = null;
};
_prefersDark = new MediaQuery( _prefersDark = new MediaQuery(
'(prefers-color-scheme: dark)', '(prefers-color-scheme: dark)',
@@ -328,7 +372,6 @@ export const getInitialStore = (signals: StoreSignals) => {
// Only way to check if an object is a proxy // Only way to check if an object is a proxy
// is to see if is failes to perform a structured clone // is to see if is failes to perform a structured clone
// TODO: is $state.raw really nessessary?
function warnIfDeeplyReactive(array: unknown[] | undefined, name: string) { function warnIfDeeplyReactive(array: unknown[] | undefined, name: string) {
try { try {
if (array && array.length > 0) { if (array && array.length > 0) {
+3 -1
View File
@@ -12,7 +12,9 @@ import type { Node } from '$lib/types';
import type { ClassValue } from 'svelte/elements'; import type { ClassValue } from 'svelte/elements';
/** /**
* The Edge type is mainly used for the `edges` that get passed to the SvelteFlow component. * An `Edge` is the complete description with everything Svelte Flow needs to know in order to
* render it.
* @public
*/ */
export type Edge< export type Edge<
EdgeData extends Record<string, unknown> = Record<string, unknown>, EdgeData extends Record<string, unknown> = Record<string, unknown>,
+2 -2
View File
@@ -3,7 +3,7 @@ import { isNodeBase, isEdgeBase } from '@xyflow/system';
import type { Edge, Node } from '$lib/types'; import type { Edge, Node } from '$lib/types';
/** /**
* Test whether an object is useable as a Node * Test whether an object is usable as a Node
* @public * @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test * @param element - The element to test
@@ -13,7 +13,7 @@ export const isNode = <NodeType extends Node = Node>(element: unknown): element
isNodeBase<NodeType>(element); isNodeBase<NodeType>(element);
/** /**
* Test whether an object is useable as an Edge * Test whether an object is usable as an Edge
* @public * @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test * @param element - The element to test
+56
View File
@@ -1,5 +1,61 @@
# @xyflow/system # @xyflow/system
## 0.0.55
### Patch Changes
- [#5156](https://github.com/xyflow/xyflow/pull/5156) [`02a3b746`](https://github.com/xyflow/xyflow/commit/02a3b74645799a3f0ce670b69365fa86ecb0616e) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type GetSmoothStepPathParams` and `getSmoothStepPath` function
- [#5168](https://github.com/xyflow/xyflow/pull/5168) [`cbe305e1`](https://github.com/xyflow/xyflow/commit/cbe305e15a5c5d3b92583e0ec12364b2509f49bd) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `getOutgoers`, `getIncomers` and `type GetNodesBoundsParams`
- [#5169](https://github.com/xyflow/xyflow/pull/5169) [`1f671bd4`](https://github.com/xyflow/xyflow/commit/1f671bd48f06230da841fdd1d7a312413ef16d03) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type ReconnectEdgeOptions` and `getViewportForBounds`
- [#5155](https://github.com/xyflow/xyflow/pull/5155) [`aaebc462`](https://github.com/xyflow/xyflow/commit/aaebc462951ded8e91374c3e084d77af5ed7380a) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type GetStraightPathParams` and `getStraightPath` function
- [#5157](https://github.com/xyflow/xyflow/pull/5157) [`6ec942fc`](https://github.com/xyflow/xyflow/commit/6ec942fc6501f81009c278cc995764bef3e8d03b) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type GetBezierPathParams` and `getBezierPath` function
## 0.0.54
### Patch Changes
- [#5147](https://github.com/xyflow/xyflow/pull/5147) [`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a) Thanks [@moklick](https://github.com/moklick)! - Pass dimensions to final resize change event
- [#5142](https://github.com/xyflow/xyflow/pull/5142) [`24a1bc89`](https://github.com/xyflow/xyflow/commit/24a1bc89348817ed9b5c87f74bf2519c705143be) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `HandleProps`, `NodeBase` and `InternalNodeBase`
- [#5136](https://github.com/xyflow/xyflow/pull/5136) [`36657cd6`](https://github.com/xyflow/xyflow/commit/36657cd66322c911e87eb37275c584a80025adfe) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `EdgeBase`
- [#5139](https://github.com/xyflow/xyflow/pull/5139) [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Use `rgba` for colors with opacity instead of `rgb` for `MiniMap` mask color
- [#5148](https://github.com/xyflow/xyflow/pull/5148) [`2ac6e155`](https://github.com/xyflow/xyflow/commit/2ac6e155e35256ca436281df16344366e7d05761) Thanks [@moklick](https://github.com/moklick)! - Prevent browser zoom for pinch zoom gestures on nowheel elements
- [#5137](https://github.com/xyflow/xyflow/pull/5137) [`f0f378e5`](https://github.com/xyflow/xyflow/commit/f0f378e5b6918c2c30d9dc1e32587063cb942d4e) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `Connection` and `ConnectionInProgress`
## 0.0.53
### Patch Changes
- [#5118](https://github.com/xyflow/xyflow/pull/5118) [`5d15b01b`](https://github.com/xyflow/xyflow/commit/5d15b01ba8cb349d6397a6ed8162848b4dfec293) Thanks [@moklick](https://github.com/moklick)! - Do not swallow key events when a button is focused
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working immediately after adding new nodes
- [#5091](https://github.com/xyflow/xyflow/pull/5091) [`a79f30b3`](https://github.com/xyflow/xyflow/commit/a79f30b3dd7c8ff6400c8d22214b2c2282e5bac1) Thanks [@moklick](https://github.com/moklick)! - Add center-left and center-right as a panel position
## 0.0.52
### Patch Changes
- [#5052](https://github.com/xyflow/xyflow/pull/5052) [`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90) Thanks [@moklick](https://github.com/moklick)! - Show an error if user drags uninitialized node
## 0.0.51
### Patch Changes
- [#5010](https://github.com/xyflow/xyflow/pull/5010) [`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919) Thanks [@moklick](https://github.com/moklick)! - Add more TSDocs to components, hooks, utils funcs and types
- [#4990](https://github.com/xyflow/xyflow/pull/4990) [`4947029c`](https://github.com/xyflow/xyflow/commit/4947029cd6cda0f695e1fb4815e4030adb232234) Thanks [@damianstasik](https://github.com/damianstasik)! - Make it possible to stop autoPanOnDrag by setting it to false
- [#5003](https://github.com/xyflow/xyflow/pull/5003) [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - repair lint command
## 0.0.50 ## 0.0.50
### Patch Changes ### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.50", "version": "0.0.55",
"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",
@@ -63,7 +63,7 @@
"@xyflow/eslint-config": "workspace:*", "@xyflow/eslint-config": "workspace:*",
"@xyflow/rollup-config": "workspace:*", "@xyflow/rollup-config": "workspace:*",
"@xyflow/tsconfig": "workspace:*", "@xyflow/tsconfig": "workspace:*",
"typescript": "5.1.3" "typescript": "5.4.5"
}, },
"rollup": { "rollup": {
"globals": { "globals": {
+2
View File
@@ -26,6 +26,8 @@ export const errorMessages = {
`It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`, `It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`,
error014: () => error014: () =>
'useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.', 'useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.',
error015: () =>
'It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs.',
}; };
export const infiniteExtent: CoordinateExtent = [ export const infiniteExtent: CoordinateExtent = [

Some files were not shown because too many files have changed in this diff Show More