```
├── .changeset/
├── README.md (100 tokens)
├── config.json (100 tokens)
├── slick-kings-yell.md
├── .codespellrc (100 tokens)
├── .gitattributes (omitted)
├── .github/
├── FUNDING.yml
├── ISSUE_TEMPLATE/
├── bug_report.yml (700 tokens)
├── config.yml
├── feature_request.yml (100 tokens)
├── actions/
├── ci-checks/
├── action.yml
├── ci-setup/
├── action.yml (100 tokens)
├── workflows/
├── codespell.yml (100 tokens)
├── dispatchWebsiteUpdate.yaml (200 tokens)
├── playwright.yml (100 tokens)
├── release.yml (200 tokens)
├── .gitignore
├── .npmrc
├── .prettierignore (omitted)
├── .prettierrc.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md (600 tokens)
├── CONTRIBUTING.md (1000 tokens)
├── LICENSE (omitted)
├── README.md (1100 tokens)
├── SECURITY.md (400 tokens)
├── dependabot.yml
├── examples/
├── README.md (100 tokens)
├── astro-xyflow/
├── .gitignore
├── README.md
├── astro.config.mjs
├── package.json (100 tokens)
├── public/
├── .gitkeep
├── src/
├── components/
├── ReactFlowExample/
├── CustomNode.tsx (200 tokens)
├── index.tsx (500 tokens)
├── ReactFlowInitialExample/
├── CustomNode.tsx (100 tokens)
├── index.tsx (300 tokens)
├── SvelteFlowExample/
├── index.svelte (500 tokens)
├── SvelteFlowInitialExample/
├── CustomNode.svelte (100 tokens)
├── index.svelte (200 tokens)
├── env.d.ts (omitted)
├── pages/
├── index.astro (200 tokens)
├── tsconfig.json
├── react/
├── .gitignore (100 tokens)
├── README.md (200 tokens)
├── cypress.config.ts (100 tokens)
├── cypress/
├── components/
├── hooks/
├── useEdges.cy.tsx (200 tokens)
├── useNodes.cy.tsx (300 tokens)
├── useNodesInitialized.cy.tsx (400 tokens)
├── useOnViewportChange.cy.tsx (600 tokens)
├── useViewport.cy.tsx (400 tokens)
├── reactflow/
├── basic-props.cy.tsx (1300 tokens)
├── event-handlers.cy.tsx (1400 tokens)
├── multiple-instance.cy.tsx (400 tokens)
├── on-nodes-change.cy.tsx (800 tokens)
├── storeAvailabilityOnRemount.cy.tsx (800 tokens)
├── uncontrolled.cy.tsx (300 tokens)
├── view-props.cy.tsx (2000 tokens)
├── utils/
├── adopt-user-nodes.cy.ts (1400 tokens)
├── apply-changes.cy.ts (1300 tokens)
├── calculate-node-position.cy.ts (300 tokens)
├── get-nodes-bounds.cy.ts (1000 tokens)
├── graph-utils.cy.ts (500 tokens)
├── update-node-internals.cy.ts (400 tokens)
├── e2e/
├── basic.cy.ts (1100 tokens)
├── controls.cy.ts (500 tokens)
├── draghandle.cy.ts (200 tokens)
├── empty.cy.ts (200 tokens)
├── figma.cy.ts (300 tokens)
├── hidden.cy.ts (200 tokens)
├── interaction.cy.ts (1000 tokens)
├── minimap.cy.ts (600 tokens)
├── fixtures/
├── simpleflow.ts (100 tokens)
├── support/
├── ControlledFlow.tsx (300 tokens)
├── commands.ts (400 tokens)
├── component-index.html (100 tokens)
├── component.ts (200 tokens)
├── e2e.ts (200 tokens)
├── index.html (100 tokens)
├── package.json (300 tokens)
├── public/
├── favicon.ico
├── src/
├── App/
├── header.tsx (200 tokens)
├── index.tsx (100 tokens)
├── routes.ts (1900 tokens)
├── app.d.ts (omitted)
├── examples/
├── A11y/
├── index.tsx (600 tokens)
├── AddNodeOnEdgeDrop/
├── index.tsx (500 tokens)
├── Backgrounds/
├── index.tsx (300 tokens)
├── style.module.css
├── Basic/
├── index.tsx (1100 tokens)
├── BrokenNodes/
├── index.tsx (400 tokens)
├── CancelConnection/
├── Timer.module.css (100 tokens)
├── Timer.tsx (100 tokens)
├── data.ts (100 tokens)
├── hooks/
├── useCountdown.ts (100 tokens)
├── index.tsx (300 tokens)
├── ClickDistance/
├── index.tsx (400 tokens)
├── ColorMode/
├── index.tsx (400 tokens)
├── ControlledUncontrolled/
├── index.tsx (600 tokens)
├── ControlledViewport/
├── index.tsx (500 tokens)
├── CustomConnectionLine/
├── ConnectionLine.tsx (100 tokens)
├── index.tsx (200 tokens)
├── CustomMiniMapNode/
├── index.tsx (500 tokens)
├── CustomNode/
├── ColorSelectorNode.tsx (300 tokens)
├── index.tsx (900 tokens)
├── DefaultEdgeOverwrite/
├── index.tsx (300 tokens)
├── DefaultNodeOverwrite/
├── index.tsx (200 tokens)
├── DefaultNodes/
├── index.tsx (400 tokens)
├── DetachedHandle/
├── index.tsx (200 tokens)
├── style.css
├── DevTools/
├── DevTools/
├── ChangeLogger.tsx (400 tokens)
├── NodeInspector.tsx (300 tokens)
├── index.tsx (300 tokens)
├── style.css (300 tokens)
├── index.tsx (200 tokens)
├── DragHandle/
├── DragHandleNode.tsx (200 tokens)
├── index.tsx (200 tokens)
├── DragNDrop/
├── Sidebar.tsx (200 tokens)
├── dnd.module.css (100 tokens)
├── index.tsx (400 tokens)
├── EasyConnect/
├── CustomConnectionLine.tsx (100 tokens)
├── CustomNode.tsx (300 tokens)
├── FloatingEdge.tsx (100 tokens)
├── index.tsx (400 tokens)
├── style.css (100 tokens)
├── utils.tsx (700 tokens)
├── EdgeRenderer/
├── CustomEdge.tsx (300 tokens)
├── CustomEdge2.tsx (200 tokens)
├── index.tsx (1000 tokens)
├── EdgeRouting/
├── index.tsx (1300 tokens)
├── EdgeToolbar/
├── CustomEdge.tsx (200 tokens)
├── index.tsx (300 tokens)
├── EdgeTypes/
├── index.tsx (300 tokens)
├── utils.ts (600 tokens)
├── Edges/
├── CustomEdge.tsx (100 tokens)
├── CustomEdge2.tsx (200 tokens)
├── CustomEdge3.css (100 tokens)
├── CustomEdge3.tsx (100 tokens)
├── index.tsx (1500 tokens)
├── Empty/
├── index.tsx (400 tokens)
├── Figma/
├── index.tsx (400 tokens)
├── FloatingEdges/
├── FloatingConnectionLine.tsx (200 tokens)
├── FloatingEdge.tsx (200 tokens)
├── index.tsx (300 tokens)
├── style.module.css
├── utils.ts (700 tokens)
├── Hidden/
├── index.tsx (500 tokens)
├── Interaction/
├── index.tsx (1700 tokens)
├── InteractiveMinimap/
├── index.tsx (800 tokens)
├── Intersection/
├── index.tsx (500 tokens)
├── style.css
├── Layouting/
├── index.tsx (600 tokens)
├── initial-elements.ts (400 tokens)
├── layouting.module.css
├── Middlewares/
├── RestrictExtent.tsx (300 tokens)
├── index.tsx (600 tokens)
├── MovingHandles/
├── MovingHandleNode.tsx (300 tokens)
├── index.tsx (500 tokens)
├── MultiFlows/
├── index.tsx (300 tokens)
├── multiflows.module.css
├── MultiSetNodes/
├── index.tsx (400 tokens)
├── style.css
├── NodeResizer/
├── BottomRightResizer.tsx (100 tokens)
├── CustomResizer.tsx (200 tokens)
├── DefaultResizer.tsx (200 tokens)
├── FixedExtentNode.tsx (200 tokens)
├── HorizontalResizer.tsx (300 tokens)
├── ResizeIcon.tsx (100 tokens)
├── VerticalResizer.tsx (300 tokens)
├── index.tsx (1000 tokens)
├── NodeSelectionBug/
├── index.tsx (200 tokens)
├── NodeToolbar/
├── CustomNode.tsx (100 tokens)
├── SelectedNodesToolbar.tsx (100 tokens)
├── index.tsx (300 tokens)
├── NodeTypeChange/
├── index.tsx (300 tokens)
├── NodeTypesObjectChange/
├── index.tsx (400 tokens)
├── Overview/
├── index.tsx (1500 tokens)
├── Provider/
├── Sidebar.tsx (200 tokens)
├── index.tsx (400 tokens)
├── provider.module.css (100 tokens)
├── ReconnectEdge/
├── index.tsx (600 tokens)
├── Redux/
├── index.tsx (200 tokens)
├── initial-elements.tsx (300 tokens)
├── state.ts (300 tokens)
├── SaveRestore/
├── Controls.tsx (400 tokens)
├── index.tsx (200 tokens)
├── save.module.css
├── SetNodesBatching/
├── index.tsx (400 tokens)
├── Stress/
├── index.tsx (1700 tokens)
├── performanceUtils.ts (900 tokens)
├── utils.ts (200 tokens)
├── Subflow/
├── DebugNode.tsx (200 tokens)
├── index.tsx (1200 tokens)
├── Switch/
├── index.tsx (700 tokens)
├── TouchDevice/
├── index.tsx (300 tokens)
├── touch-device.css (100 tokens)
├── Undirectional/
├── CustomNode.tsx (100 tokens)
├── index.tsx (900 tokens)
├── UpdateNode/
├── index.tsx (600 tokens)
├── updatenode.module.css (100 tokens)
├── UseConnection/
├── index.tsx (300 tokens)
├── UseKeyPress/
├── index.tsx (100 tokens)
├── UseNodeConnections/
├── MultiHandleNode.tsx (300 tokens)
├── SingleHandleNode.tsx (300 tokens)
├── index.tsx (400 tokens)
├── UseNodesData/
├── ResultNode.tsx (100 tokens)
├── TextNode.tsx (200 tokens)
├── UppercaseNode.tsx (200 tokens)
├── index.tsx (500 tokens)
├── UseNodesInit/
├── index.tsx (400 tokens)
├── UseOnSelectionChange/
├── CustomNode.tsx (200 tokens)
├── index.tsx (400 tokens)
├── UseReactFlow/
├── index.tsx (1100 tokens)
├── UseUpdateNodeInternals/
├── CustomNode.tsx (200 tokens)
├── index.tsx (300 tokens)
├── Validation/
├── ConnectionStatus.tsx (300 tokens)
├── index.tsx (600 tokens)
├── validation.module.css (200 tokens)
├── ZIndexMode/
├── index.tsx (500 tokens)
├── generic-tests/
├── Flow.tsx (300 tokens)
├── edges/
├── general.ts (800 tokens)
├── index.tsx (100 tokens)
├── node-toolbar/
├── components/
├── ToolbarNode.tsx (100 tokens)
├── general.ts (200 tokens)
├── nodes/
├── components/
├── DragHandleNode.tsx (100 tokens)
├── general.ts (400 tokens)
├── pane/
├── activation-keys.ts (100 tokens)
├── general.ts (100 tokens)
├── non-defaults.ts (200 tokens)
├── index.css (200 tokens)
├── main.tsx (100 tokens)
├── vite-env.d.ts (omitted)
├── tsconfig.json (100 tokens)
├── tsconfig.node.json
├── vite.config.ts (100 tokens)
├── svelte/
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── README.md (200 tokens)
├── eslint.config.js (300 tokens)
├── package.json (200 tokens)
├── src/
├── app.d.ts (omitted)
├── app.html (100 tokens)
├── components/
├── Header/
├── Header.svelte (200 tokens)
├── index.ts
├── generic-tests/
├── edges/
├── general.ts (700 tokens)
├── node-toolbar/
├── components/
├── ToolbarNode.svelte (100 tokens)
├── general.ts (200 tokens)
├── nodes/
├── components/
├── DragHandleNode.svelte (100 tokens)
├── general.ts (400 tokens)
├── pane/
├── activation-keys.ts (100 tokens)
├── general.ts (100 tokens)
├── non-defaults.ts (100 tokens)
├── routes/
├── +layout.server.ts
├── +layout.ts
├── +page.svelte
├── examples/
├── +layout.svelte (100 tokens)
├── +page.svelte
├── a11y/
├── +page.svelte (400 tokens)
├── add-node-on-drop/
├── +page.svelte
├── Flow.svelte (600 tokens)
├── color-mode/
├── +page.svelte (300 tokens)
├── custom-connection-line/
├── +page.svelte (200 tokens)
├── ConnectionLine.svelte (100 tokens)
├── CustomNode.svelte (100 tokens)
├── custom-minimap/
├── +page.svelte (300 tokens)
├── CustomMiniMapNode.svelte (100 tokens)
├── customnode/
├── +page.svelte (300 tokens)
├── CustomNode.svelte (200 tokens)
├── dagre/
├── +page.svelte (400 tokens)
├── nodes-and-edges.ts (300 tokens)
├── detached-handle/
├── +page.svelte (100 tokens)
├── CustomNode.svelte (100 tokens)
├── drag-n-drop/
├── +page.svelte
├── Flow.svelte (400 tokens)
├── Sidebar.svelte (200 tokens)
├── edge-toolbar/
├── +page.svelte (200 tokens)
├── CustomEdge.svelte (100 tokens)
├── edges/
├── +page.svelte (1000 tokens)
├── ButtonEdge.svelte (300 tokens)
├── CustomBezierEdge.svelte
├── figma/
├── +page.svelte (300 tokens)
├── handle-connect/
├── +page.svelte (300 tokens)
├── MultiHandleNode.svelte (400 tokens)
├── SingleHandleNode.svelte (200 tokens)
├── interaction/
├── +page.svelte (1000 tokens)
├── intersections/
├── +page.svelte
├── +server.ts (200 tokens)
├── Flow.svelte (300 tokens)
├── nodes-and-edges.ts (100 tokens)
├── node-resizer/
├── +page.svelte (800 tokens)
├── BottomRightResizer.svelte (100 tokens)
├── CustomResizer.svelte (300 tokens)
├── DefaultResizer.svelte (200 tokens)
├── FixedExtentNode.svelte (100 tokens)
├── HorizontalResizer.svelte (300 tokens)
├── VerticalResizer.svelte (300 tokens)
├── store.svelte.ts
├── types.ts (100 tokens)
├── node-toolbar/
├── +page.svelte (300 tokens)
├── CustomNode.svelte (200 tokens)
├── SelectedNodesToolbar.svelte (100 tokens)
├── overview/
├── +page.svelte
├── CustomEdge.svelte (200 tokens)
├── CustomNode.svelte (200 tokens)
├── CustomNodeDragHandle.svelte (100 tokens)
├── Flow.svelte (1300 tokens)
├── InitTracker.svelte (100 tokens)
├── reset/
├── +page.svelte (100 tokens)
├── stress/
├── +page.svelte (200 tokens)
├── subflows/
├── +page.svelte (500 tokens)
├── DebugNode.svelte (100 tokens)
├── two-way-viewport/
├── +page.svelte
├── Flow.svelte (200 tokens)
├── usenodesdata/
├── +page.svelte (300 tokens)
├── ResultNode.svelte (200 tokens)
├── TextNode.svelte (100 tokens)
├── UppercaseNode.svelte (200 tokens)
├── usesvelteflow/
├── +page.svelte
├── Flow.svelte (200 tokens)
├── Sidebar.svelte (500 tokens)
├── useupdatenodeinternals/
├── +page.svelte
├── CustomNode.svelte (100 tokens)
├── Flow.svelte (300 tokens)
├── validation/
├── +page.svelte (200 tokens)
├── style.css
├── tests/
├── generic/
├── [topic]/
├── [example]/
├── +page.svelte (100 tokens)
├── +page.ts (100 tokens)
├── Flow.svelte (200 tokens)
├── static/
├── favicon.ico
├── svelte.config.js (200 tokens)
├── tsconfig.json (100 tokens)
├── vite.config.ts (100 tokens)
├── package.json (300 tokens)
├── packages/
├── react/
├── .env
├── .eslintrc.js
├── CHANGELOG.md (18.2k tokens)
├── README.md (1300 tokens)
├── package.json (600 tokens)
├── src/
├── additional-components/
├── Background/
├── Background.tsx (900 tokens)
├── Patterns.tsx (200 tokens)
├── index.tsx
├── types.ts (300 tokens)
├── Controls/
├── ControlButton.tsx (200 tokens)
├── Controls.tsx (900 tokens)
├── Icons/
├── FitView.tsx (100 tokens)
├── Lock.tsx (100 tokens)
├── Minus.tsx
├── Plus.tsx
├── Unlock.tsx (100 tokens)
├── index.tsx
├── types.ts (400 tokens)
├── EdgeToolbar/
├── EdgeToolbar.tsx (400 tokens)
├── index.tsx
├── types.ts (100 tokens)
├── MiniMap/
├── MiniMap.tsx (1500 tokens)
├── MiniMapNode.tsx (200 tokens)
├── MiniMapNodes.tsx (800 tokens)
├── index.tsx
├── types.ts (800 tokens)
├── NodeResizer/
├── NodeResizeControl.tsx (1400 tokens)
├── NodeResizer.tsx (500 tokens)
├── index.tsx
├── types.ts (500 tokens)
├── NodeToolbar/
├── NodeToolbar.tsx (900 tokens)
├── NodeToolbarPortal.tsx (100 tokens)
├── index.tsx
├── types.ts (200 tokens)
├── index.ts
├── components/
├── A11yDescriptions/
├── index.tsx (300 tokens)
├── Attribution/
├── index.tsx (200 tokens)
├── BatchProvider/
├── index.tsx (700 tokens)
├── types.ts
├── useQueue.ts (400 tokens)
├── ConnectionLine/
├── index.tsx (700 tokens)
├── EdgeLabelRenderer/
├── index.tsx (400 tokens)
├── EdgeWrapper/
├── EdgeUpdateAnchors.tsx (900 tokens)
├── index.tsx (1700 tokens)
├── utils.ts (100 tokens)
├── Edges/
├── BaseEdge.tsx (400 tokens)
├── BezierEdge.tsx (500 tokens)
├── EdgeAnchor.tsx (300 tokens)
├── EdgeText.tsx (500 tokens)
├── SimpleBezierEdge.tsx (800 tokens)
├── SmoothStepEdge.tsx (500 tokens)
├── StepEdge.tsx (300 tokens)
├── StraightEdge.tsx (400 tokens)
├── index.ts (100 tokens)
├── Handle/
├── index.tsx (1900 tokens)
├── NodeWrapper/
├── index.tsx (1600 tokens)
├── useNodeObserver.ts (500 tokens)
├── utils.tsx (200 tokens)
├── Nodes/
├── DefaultNode.tsx (100 tokens)
├── GroupNode.tsx
├── InputNode.tsx (100 tokens)
├── OutputNode.tsx (100 tokens)
├── utils.ts (200 tokens)
├── NodesSelection/
├── index.tsx (600 tokens)
├── Panel/
├── index.tsx (300 tokens)
├── ReactFlowProvider/
├── index.tsx (800 tokens)
├── SelectionListener/
├── index.tsx (500 tokens)
├── StoreUpdater/
├── index.tsx (1100 tokens)
├── UserSelection/
├── index.tsx (200 tokens)
├── ViewportPortal/
├── index.tsx (200 tokens)
├── container/
├── EdgeRenderer/
├── MarkerDefinitions.tsx (500 tokens)
├── MarkerSymbols.tsx (300 tokens)
├── index.tsx (600 tokens)
├── FlowRenderer/
├── index.tsx (900 tokens)
├── GraphView/
├── index.tsx (1300 tokens)
├── useNodeOrEdgeTypesWarning.ts (200 tokens)
├── useStylesLoadedWarning.ts (100 tokens)
├── NodeRenderer/
├── index.tsx (900 tokens)
├── useResizeObserver.ts (200 tokens)
├── Pane/
├── index.tsx (2.5k tokens)
├── ReactFlow/
├── Wrapper.tsx (300 tokens)
├── index.tsx (2.2k tokens)
├── init-values.ts
├── Viewport/
├── index.tsx (400 tokens)
├── ZoomPane/
├── index.tsx (900 tokens)
├── contexts/
├── HandleConfigContext.tsx (200 tokens)
├── NodeIdContext.ts (200 tokens)
├── StoreContext.ts
├── custom.d.ts (omitted)
├── hooks/
├── useColorModeClass.ts (200 tokens)
├── useConnection.ts (500 tokens)
├── useDrag.ts (400 tokens)
├── useEdges.ts (200 tokens)
├── useGlobalKeyHandler.ts (300 tokens)
├── useHandleConnections.ts (500 tokens)
├── useInternalNode.ts (200 tokens)
├── useIsomorphicLayoutEffect.ts
├── useKeyPress.ts (1400 tokens)
├── useMoveSelectedNodes.ts (400 tokens)
├── useNodeConnections.ts (400 tokens)
├── useNodes.ts (200 tokens)
├── useNodesData.ts (300 tokens)
├── useNodesEdgesState.ts (1000 tokens)
├── useNodesInitialized.ts (400 tokens)
├── useOnEdgesChangeMiddleware.ts (200 tokens)
├── useOnInitHandler.ts (100 tokens)
├── useOnNodesChangeMiddleware.ts (200 tokens)
├── useOnSelectionChange.ts (400 tokens)
├── useOnViewportChange.ts (300 tokens)
├── useReactFlow.ts (2.2k tokens)
├── useResizeHandler.ts (300 tokens)
├── useStore.ts (600 tokens)
├── useUpdateNodeInternals.ts (500 tokens)
├── useViewport.ts (200 tokens)
├── useViewportHelper.ts (700 tokens)
├── useViewportSync.ts (100 tokens)
├── useVisibleEdgeIds.ts (300 tokens)
├── useVisibleNodeIds.ts (200 tokens)
├── index.ts (900 tokens)
├── store/
├── index.ts (2.8k tokens)
├── initialState.ts (800 tokens)
├── styles/
├── base.css
├── style.css (100 tokens)
├── utils.ts
├── types/
├── component-props.ts (5.3k tokens)
├── edges.ts (1800 tokens)
├── general.ts (1400 tokens)
├── index.ts
├── instance.ts (2000 tokens)
├── nodes.ts (800 tokens)
├── store.ts (1100 tokens)
├── utils/
├── changes.ts (1900 tokens)
├── edges.ts (200 tokens)
├── general.ts (500 tokens)
├── index.ts
├── tsconfig.json
├── svelte/
├── .env
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md (13.3k tokens)
├── README.md (1100 tokens)
├── eslint.config.js (300 tokens)
├── package.json (600 tokens)
├── src/
├── lib/
├── actions/
├── drag/
├── index.ts (500 tokens)
├── portal/
├── index.ts
├── portal.svelte.ts (200 tokens)
├── utils.svelte.ts (100 tokens)
├── zoom/
├── index.ts (400 tokens)
├── components/
├── A11yDescriptions/
├── A11yDescriptions.svelte (200 tokens)
├── index.ts (100 tokens)
├── Attribution/
├── Attribution.svelte (100 tokens)
├── index.ts
├── types.ts
├── ConnectionLine/
├── ConnectionLine.svelte (400 tokens)
├── index.ts
├── EdgeLabel/
├── EdgeLabel.svelte (300 tokens)
├── index.ts
├── types.ts (100 tokens)
├── EdgeReconnectAnchor/
├── EdgeReconnectAnchor.svelte (700 tokens)
├── index.ts
├── types.ts (100 tokens)
├── EdgeWrapper/
├── EdgeWrapper.svelte (900 tokens)
├── index.ts
├── Handle/
├── Handle.svelte (1400 tokens)
├── index.ts
├── types.ts (100 tokens)
├── KeyHandler/
├── KeyHandler.svelte (800 tokens)
├── index.ts
├── types.ts (100 tokens)
├── NodeSelection/
├── NodeSelection.svelte (600 tokens)
├── index.ts
├── types.ts (100 tokens)
├── NodeWrapper/
├── NodeWrapper.svelte (1900 tokens)
├── index.ts
├── types.ts (100 tokens)
├── Selection/
├── Selection.svelte (100 tokens)
├── index.ts
├── SvelteFlowProvider/
├── SvelteFlowProvider.svelte (200 tokens)
├── index.ts
├── types.ts
├── ViewportPortal/
├── ViewportPortal.svelte (100 tokens)
├── index.ts
├── types.ts
├── edges/
├── BaseEdge.svelte (200 tokens)
├── BezierEdge.svelte (200 tokens)
├── BezierEdgeInternal.svelte (100 tokens)
├── SmoothStepEdge.svelte (200 tokens)
├── SmoothStepEdgeInternal.svelte (100 tokens)
├── StepEdge.svelte (200 tokens)
├── StepEdgeInternal.svelte (100 tokens)
├── StraightEdge.svelte (100 tokens)
├── StraightEdgeInternal.svelte (100 tokens)
├── index.ts (200 tokens)
├── nodes/
├── DefaultNode.svelte (100 tokens)
├── GroupNode.svelte
├── InputNode.svelte (100 tokens)
├── OutputNode.svelte (100 tokens)
├── container/
├── EdgeRenderer/
├── EdgeRenderer.svelte (200 tokens)
├── MarkerDefinition/
├── Marker.svelte (200 tokens)
├── MarkerDefinition.svelte (100 tokens)
├── index.ts
├── index.ts
├── NodeRenderer/
├── NodeRenderer.svelte (400 tokens)
├── index.ts
├── Pane/
├── Pane.svelte (2.2k tokens)
├── index.ts
├── types.ts (100 tokens)
├── Panel/
├── Panel.svelte (100 tokens)
├── index.ts
├── types.ts (100 tokens)
├── SvelteFlow/
├── SvelteFlow.svelte (1300 tokens)
├── Wrapper.svelte (700 tokens)
├── index.ts
├── types.ts (3.9k tokens)
├── Viewport/
├── Viewport.svelte (100 tokens)
├── index.ts
├── Zoom/
├── Zoom.svelte (500 tokens)
├── index.ts
├── types.ts (200 tokens)
├── hooks/
├── useColorMode.svelte.ts (100 tokens)
├── useConnection.svelte.ts (100 tokens)
├── useInitialized.svelte.ts (100 tokens)
├── useInternalNode.svelte.ts (100 tokens)
├── useNodeConnections.svelte.ts (600 tokens)
├── useNodesData.svelte.ts (300 tokens)
├── useNodesEdgesViewport.svelte.ts (300 tokens)
├── useOnSelectionChange.svelte.ts (100 tokens)
├── useStore.ts (100 tokens)
├── useSvelteFlow.svelte.ts (3.7k tokens)
├── useUpdateNodeInternals.svelte.ts (300 tokens)
├── index.ts (700 tokens)
├── plugins/
├── Background/
├── Background.svelte (400 tokens)
├── DotPattern.svelte (100 tokens)
├── LinePattern.svelte (100 tokens)
├── index.ts
├── types.ts (300 tokens)
├── Controls/
├── ControlButton.svelte (100 tokens)
├── Controls.svelte (700 tokens)
├── Icons/
├── Fit.svelte (100 tokens)
├── Lock.svelte (100 tokens)
├── Minus.svelte
├── Plus.svelte
├── Unlock.svelte (100 tokens)
├── index.ts
├── types.ts (300 tokens)
├── EdgeToolbar/
├── EdgeToolbar.svelte (200 tokens)
├── index.ts
├── types.ts (100 tokens)
├── Minimap/
├── Minimap.svelte (900 tokens)
├── MinimapNode.svelte (300 tokens)
├── index.ts
├── interactive.ts (300 tokens)
├── types.ts (500 tokens)
├── NodeResizer/
├── NodeResizer.svelte (200 tokens)
├── ResizeControl.svelte (800 tokens)
├── index.ts
├── types.ts (500 tokens)
├── NodeToolbar/
├── NodeToolbar.svelte (500 tokens)
├── index.ts
├── types.ts (200 tokens)
├── store/
├── context.ts (200 tokens)
├── index.ts (2.4k tokens)
├── initial-store.svelte.ts (3.5k tokens)
├── types.ts (800 tokens)
├── visibleElements.ts (800 tokens)
├── types/
├── edges.ts (1000 tokens)
├── events.ts (700 tokens)
├── general.ts (400 tokens)
├── index.ts
├── nodes.ts (400 tokens)
├── utils/
├── edges.ts (100 tokens)
├── index.ts (300 tokens)
├── routes/
├── +page.svelte
├── +page.svelte.d.ts (omitted)
├── styles/
├── base.css (100 tokens)
├── style.css (100 tokens)
├── svelte.config.js (100 tokens)
├── tsconfig.json (100 tokens)
├── system/
├── .eslintrc.js
├── CHANGELOG.md (6.7k tokens)
├── README.md (500 tokens)
├── package.json (400 tokens)
├── src/
├── constants.ts (700 tokens)
├── index.ts
├── styles/
├── base.css (200 tokens)
├── init.css (1800 tokens)
├── node-resizer.css (400 tokens)
├── style.css (1000 tokens)
├── types/
├── changes.ts (500 tokens)
├── edges.ts (800 tokens)
├── general.ts (2.5k tokens)
├── handles.ts (300 tokens)
├── index.ts
├── nodes.ts (1000 tokens)
├── panzoom.ts (400 tokens)
├── utils.ts (400 tokens)
├── utils/
├── connections.ts (200 tokens)
├── dom.ts (700 tokens)
├── edge-toolbar.ts (100 tokens)
├── edges/
├── bezier-edge.ts (900 tokens)
├── general.ts (1500 tokens)
├── index.ts
├── positions.ts (800 tokens)
├── smoothstep-edge.ts (2.2k tokens)
├── straight-edge.ts (400 tokens)
├── general.ts (2.7k tokens)
├── graph.ts (3.4k tokens)
├── index.ts (100 tokens)
├── marker.ts (300 tokens)
├── node-toolbar.ts (300 tokens)
├── shallow-node-data.ts (100 tokens)
├── store.ts (3.9k tokens)
├── types.ts
├── xydrag/
├── XYDrag.ts (2.6k tokens)
├── index.ts
├── utils.ts (800 tokens)
├── xyhandle/
├── XYHandle.ts (1900 tokens)
├── index.ts
├── types.ts (400 tokens)
├── utils.ts (800 tokens)
├── xyminimap/
├── index.ts (700 tokens)
├── xypanzoom/
├── XYPanZoom.ts (1900 tokens)
├── eventhandler.ts (1500 tokens)
├── filter.ts (700 tokens)
├── index.ts
├── utils.ts (300 tokens)
├── xyresizer/
├── XYResizer.ts (2.1k tokens)
├── index.ts
├── types.ts (300 tokens)
├── utils.ts (2.2k tokens)
├── tsconfig.json (100 tokens)
├── pnpm-lock.yaml (omitted)
├── pnpm-workspace.yaml
├── tests/
├── playwright/
├── .gitignore
├── README.md (500 tokens)
├── e2e/
├── background.spec.ts (200 tokens)
├── constants.ts
├── edges.spec.ts (1200 tokens)
├── node-toolbar.spec.ts (700 tokens)
├── nodes.spec.ts (2.5k tokens)
├── pane.spec.ts (2.2k tokens)
├── props.spec.ts (200 tokens)
├── utils.ts (100 tokens)
├── package-lock.json (15.5k tokens)
├── package.json (200 tokens)
├── playwright.react.config.ts
├── playwright.shared.config.ts (400 tokens)
├── playwright.svelte.config.ts
├── tooling/
├── eslint-config/
├── CHANGELOG.md (100 tokens)
├── package.json (100 tokens)
├── src/
├── index.js (200 tokens)
├── postcss-config/
├── postcss.config.js (100 tokens)
├── rollup-config/
├── package.json (100 tokens)
├── src/
├── index.js (400 tokens)
├── tsconfig/
├── base.json (100 tokens)
├── package.json
├── react.json
├── turbo.json (100 tokens)
```
## /.changeset/README.md
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
## /.changeset/config.json
```json path="/.changeset/config.json"
{
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "xyflow/xyflow", "disableThanks": true }],
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["react-examples", "svelte-examples", "astro-examples", "playwright"],
"prettier": false
}
```
## /.changeset/slick-kings-yell.md
---
"@xyflow/svelte": patch
---
Fix unnecessary edge rerenders
## /.codespellrc
```codespellrc path="/.codespellrc"
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git,.gitattributes,.gitignore,package-lock.json,*-lock.yaml,*.css,.codespellrc,.npm,.cache
check-hidden = true
# ned - person name "Ned Batchelder" in .coderabbit.yaml
# mistic - GitHub username @mistic in CHANGELOGs
# inout - legitimate "inout field" (input/output) term
ignore-words-list = ned,mistic,inout
```
## /.github/FUNDING.yml
```yml path="/.github/FUNDING.yml"
# These are supported funding model platforms
github: [xyflow]
open_collective: xyflow
```
## /.github/ISSUE_TEMPLATE/bug_report.yml
```yml path="/.github/ISSUE_TEMPLATE/bug_report.yml"
name: '🐛 Bug Report'
description: Create a report to help us improve React Flow
body:
- type: markdown
attributes:
value: |
Hello! :sparkles: You found the place to submit a bug or issue that you discovered in the xyflow library, thank you for helping us out!
Having trouble making something work in xyflow, but it’s not a bug with the library itself? You’ll get the fastest response by asking in [github discussions](https://github.com/xyflow/xyflow/discussions) or our [discord server](https://discord.gg/RVmnytFmGW).
**Search our [discussions](https://github.com/xyflow/xyflow/discussions?discussions_q=) and [issues](https://github.com/xyflow/xyflow/issues?q=)** to see if your problem has already been reported and you can add more context, or see if someone else has already solved your issue. And of course, check our [docs](https://reactflow.dev/learn) if you haven’t already.
**Issue with the xyflow Docs?** Submit an issue in our [web repo](https://github.com/xyflow/web/issues) instead.
With that out of the way, let’s get started :bug:
- type: textarea
id: platform
attributes:
label: What platform were you using when you found the bug?
description: We only address issues for the most recent xyflow major release or upcoming releases.
value: |
- React Flow / Svelte Flow version:
- Browser and version:
- OS and version:
validations:
required: true
- type: input
id: link
attributes:
label: Live code example
description: |
This is the most important part of the bug report!
React Flow codesandbox starters
- js: https://new.reactflow.dev/js
- ts: https://new.reactflow.dev/ts
Svelte Flow stackblitz starters
- js: https://new.svelteflow.dev/js
- ts: https://new.svelteflow.dev/ts
Avoid dependencies in your example for easier debugging.
placeholder: https://stackblitz.com/edit/... or Github Repo
validations:
required: false
- type: textarea
id: description
attributes:
label: Describe the Bug
description: What exactly is happening when you encounter the bug?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce the bug or issue
description: How can we see the reported behavior in the code example you provided? The more detail, the better!
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What would you expect to happen if there was no bug or issue?
placeholder: As a user, I expected ___ behavior, but instead I am seeing ___
validations:
required: true
- type: textarea
id: screenshots_or_videos
attributes:
label: Screenshots or Videos
description: |
If applicable, add screenshots or a video to help explain your problem.
Supported image/file types and file size limits- https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
placeholder: Drag your video or image files inside of this editor
- type: textarea
id: additional
attributes:
label: Additional context
```
## /.github/ISSUE_TEMPLATE/config.yml
```yml path="/.github/ISSUE_TEMPLATE/config.yml"
blank_issues_enabled: false
contact_links:
- name: 🤔 Feature Requests & Questions
url: https://github.com/wbkd/react-flow/discussions
about: Please ask and answer questions in the discussions section
```
## /.github/ISSUE_TEMPLATE/feature_request.yml
```yml path="/.github/ISSUE_TEMPLATE/feature_request.yml"
name: 🛠 Feature Request
description: Suggest a new feature or idea for React Flow
labels: feature request
body:
- type: textarea
id: description
attributes:
label: Please describe the feature that you want to propose
description: A clear and concise description of what you want to happen.
validations:
required: true
```
## /.github/actions/ci-checks/action.yml
```yml path="/.github/actions/ci-checks/action.yml"
name: 'CI checks'
description: 'Checks if the code builds and typechecks'
runs:
using: 'composite'
steps:
- name: Build
run: pnpm build
shell: bash
- name: Typecheck
run: pnpm typecheck
shell: bash
```
## /.github/actions/ci-setup/action.yml
```yml path="/.github/actions/ci-setup/action.yml"
name: 'CI setup'
description: 'Setup pnpm and node'
inputs:
package-manager-cache:
description: 'Enable pnpm cache in setup-node'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup node.js
uses: actions/setup-node@v6
with:
# https://github.com/changesets/changesets/issues/2115
node-version: '24.16.0'
registry-url: 'https://registry.npmjs.org'
package-manager-cache: ${{ inputs.package-manager-cache }}
- name: Install dependencies
run: pnpm install
shell: bash
```
## /.github/workflows/codespell.yml
```yml path="/.github/workflows/codespell.yml"
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Codespell
uses: codespell-project/actions-codespell@v2
```
## /.github/workflows/dispatchWebsiteUpdate.yaml
```yaml path="/.github/workflows/dispatchWebsiteUpdate.yaml"
name: Dispatch Website Update
run-name: Dispatch Website Update for ${{ github.event.release.tag_name }}
on:
release:
types: [published, edited]
jobs:
dispatch_website_update:
if: ${{ contains(github.event.release.tag_name, 'svelte') || contains(github.event.release.tag_name, 'react') }}
runs-on: ubuntu-latest
steps:
# Github Actions is not able to escape JSON strings with certain characters in them
# which release bodies have plenty of. This step uses jq to extract the body and tag_name
# and escape them so they can be used in the next step to create a JSON for the client-payload
- name: Strip and escape JSON
run: |
{
echo 'ESCAPED_PAYLOAD<<EOF'
echo '${{ toJson(github.event.release) }}' | jq '{"body", "tag_name"}'
echo EOF
} >> "$GITHUB_ENV"
- name: Dispatch Website Update
uses: peter-evans/repository-dispatch@v4
with:
repository: xyflow/web
token: ${{ secrets.PAT }}
event-type: library-release
client-payload: ${{ env.ESCAPED_PAYLOAD }}
```
## /.github/workflows/playwright.yml
```yml path="/.github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.60.0-jammy
steps:
- name: Checkout repo
uses: actions/checkout@v6
- uses: ./.github/actions/ci-setup
- uses: ./.github/actions/ci-checks
- name: Run your tests
run: CI=true pnpm test:react
env:
HOME: /root
```
## /.github/workflows/release.yml
```yml path="/.github/workflows/release.yml"
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
id-token: write
contents: write
pull-requests: write
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- uses: ./.github/actions/ci-setup
with:
package-manager-cache: 'false'
- uses: ./.github/actions/ci-checks
- name: Create release PR or publish to npm
id: changesets
uses: xyflow/changeset-action@v1
with:
publish: pnpm release
title: Release packages
commit: 'chore(packages): bump'
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
```
## /.gitignore
```gitignore path="/.gitignore"
.DS_Store
.cache
.npm
node_modules
examples/build
cypress/videos
cypress/screenshots
dist
stats.html
.eslintcache
.idea
.log
.turbo
.rollup.cache
```
## /.npmrc
```npmrc path="/.npmrc"
legacy-peer-deps=true
strict-peer-dependencies=false
```
## /.prettierrc.json
```json path="/.prettierrc.json"
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120
}
```
## /CHANGELOG.md
# Changelog
Please check the [releases](https://github.com/wbkd/react-flow/releases) page for information about each release.
## /CODE_OF_CONDUCT.md
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective and/or restorative action in response to any instances of unacceptable behaviour.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
Where it is appropriate and parties consent, project maintainers will endeavour to facilitate restorative justice over punitive action.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
Additionally, we ask that all community members uphold the standards laid out in this Code of Conduct to create a safe and welcome space for all.
## Attribution
This Code of Conduct is a direct descendant of the [Gleam Code of Conduct](https://github.com/gleam-lang/gleam/blob/f793b5d28a3102276a8b861c7e16a19c5231426e/CODE_OF_CONDUCT.md), which is itself a descendant of the [Contributor Covenant (v1.4)](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html).
## /CONTRIBUTING.md
# Contributing to React Flow and Svelte Flow
Hello there! So glad you want to help out with React Flow and Svelte Flow 🤗 You’re the best. Here’s a guide for how you can contribute to the project.
# The xyflow organization
This is the current structure:
* React Flow `@xyflow/react` can be found under [packages/react](./packages/react)
* Svelte Flow `@xyflow/svelte` can be found under [packages/svelte](./packages/svelte)
* `@xyflow/system` can be found under [packages/system](./packages/system)
* The legacy React Flow package `reactflow` (v11) is on branch "v11"
# Our Contributing Philosophy
The direction of React Flow and Svelte Flow and which new features are added, and which are left out, is decided by the core team (sometimes referred to as a “cathedral” style of development). The core team is paid to do this work ([see how here](https://xyflow.com/blog/asking-for-money-for-open-source/)). With this model we ensure that the people doing the most work on the library are paid for their time and effort, and that we prevent the library from bloating.
That being said, our libraries are only interesting because of the people who make things with it, share their work, and discuss it. Some of the most important and undervalued work in open source is from non-code contributions, and that is where we can use the most help from you.
# How can I help?
The things we need the most help for the library and its community are:
**🐛 Bug reports:** We simply can’t catch them all. Check [existing issues](https://github.com/xyflow/xyflow/issues/) and discussion first, then [create a new issue](https://github.com/xyflow/xyflow/issues/new/choose) to tell us what’s up.
**💬 Answering questions** in our [Discord Server](https://discord.gg/Bqt6xrs) and [Github discussions](https://github.com/xyflow/xyflow/discussions).
🎬 **Create tutorials**. Send them to us and we’ll happily share them!
**✏️ Edit our [Docs](https://reactflow.dev/learn/concepts/introduction/)**: Make changes in our [web repo](https://github.com/xyflow/web), where our docs live.
All interactions should be done with care following our [Code of Conduct](https://github.com/xyflow/xyflow/blob/main/CODE_OF_CONDUCT.md).
## Enhancements
If you have an idea or suggestion for an enhancement to the React Flow or Svelte Flow library, please use the [New Features](https://github.com/xyflow/xyflow/discussions/categories/new-features) discussion section. If you want to build it yourself, **please reach out to us before you dive into a new pull request.** The direction of React Flow/ Svelte Flow and which new features are added are discussed in our Discord Server and in [this Github discussions section](https://github.com/xyflow/xyflow/discussions/categories/new-features), and in the end they are decided by the core team.
Talking to us first about the enhancement you want to build will be the most likely way to get your pull request into the library (see Our Contributing Philosophy above). We would hate to see you write code you’re proud of, just to learn that we’ve already been working on the same thing, or that we feel doesn’t fit into the core library.
### Contact us
To ask about a possible enhancement, email us at info@xyflow.com
## 💫 Pull Requests
If you want to contribute improvements or new features we are happy to review your PR :)
Please use a meaningful commit message and add a little description of your changes.
1. Install dependencies `pnpm install`
2. Start dev server `pnpm dev`
3. Test your changes with the existing examples or add a new one if it's needed for your changes
4. Run tests `pnpm test` and add new new tests if you are introducing a new feature
## Changeset Style Guide
*Inspired and taken from [Common Changelogs](https://github.com/vweevers/common-changelog?tab=readme-ov-file) and [Warp by Broad Institute](https://broadinstitute.github.io/warp/docs/contribution/contribute_to_warp/changelog_style/)*
If you are writing a changeset for a PR, here are some helpful tips:
## TLDR
- Changelogs are for humans
- Communicate the impact of changes
- Use active voice *and* presence tense *(”Fix …” instead of “… was fixed”)*
- Omit redundant verbs
- *“Document” instead of “Add documentation”*
- Omit personal pronouns
- Use backticks for function or component names (`getNodesBounds`, `<ReactFlow />`, etc) for a better syntax highlighting
## Examples
**🛑 Bad:**
“minimap: use latest node dimensions”
**✅ Good:**
“Display minimap nodes even if onNodesChange is not implemented”
**🛑 Bad:**
“fix(handles): reconnect for connectionMode=loose”
**✅ Good:**
“Fix reconnections when connectionMode is set to loose”
**🛑 Bad:**
“use correct index when using setNodes for inserting”
**✅ Good:**
“Fix incorrect order of nodes added with setNodes from useReactFlow”
## /README.md


<div align="center">



Powerful open source libraries for building node-based UIs with React or Svelte. Ready out-of-the-box and infinitely customizable.
[React Flow](https://reactflow.dev/) · [Svelte Flow](https://svelteflow.dev/) · [React Flow Pro](https://reactflow.dev/pro) · [Discord](https://discord.gg/Bqt6xrs)
</div>
---
## The xyflow mono repo
The xyflow repository is the home of four packages:
* React Flow 12 `@xyflow/react` [packages/react](./packages/react)
* React Flow 11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
* Svelte Flow `@xyflow/svelte` [packages/svelte](./packages/svelte)
* Shared helper library `@xyflow/system` [packages/system](./packages/system)
## Commercial usage
**Are you using React Flow or Svelte Flow for a personal project?** Great! No sponsorship needed, you can support us by reporting any bugs you find, sending us screenshots of your projects, and starring us on Github 🌟
**Are you using React Flow or Svelte Flow at your organization and making money from it?** Awesome! We rely on your support to keep our libraries developed and maintained under an MIT License, just how we like it. For React Flow you can do that on the [React Flow Pro website](https://reactflow.dev/pro) and for both of our libraries you can do it through [Github Sponsors](https://github.com/sponsors/xyflow).
## Getting started
The best way to get started is to check out the [React Flow](https://reactflow.dev/learn) or [Svelte Flow](https://svelteflow.dev/learn) learn section. However if you want to get a sneak peek of how to install and use the libraries you can see it here:
<details>
<summary><strong>React Flow</strong> basic usage</summary>
### Installation
```sh
npm install @xyflow/react
```
### Basic usage
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' } },
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
}
export default Flow;
```
</details>
<details>
<summary><strong>Svelte Flow</strong> basic usage</summary>
### Installation
```sh
npm install @xyflow/svelte
```
### Basic usage
```svelte
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
Background,
BackgroundVariant,
MiniMap,
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css'
const nodes = writable([
{
id: '1',
type: 'input',
data: { label: 'Input Node' },
position: { x: 0, y: 0 }
},
{
id: '2',
type: 'custom',
data: { label: 'Node' },
position: { x: 0, y: 150 }
}
]);
const edges = writable([
{
id: '1-2',
type: 'default',
source: '1',
target: '2',
label: 'Edge Text'
}
]);
</script>
<SvelteFlow
{nodes}
{edges}
fitView
on:nodeclick={(event) => console.log('on node click', event)}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
</SvelteFlow>
```
</details>
## Releases
For releasing packages we are using [changesets](https://github.com/changesets/changesets) in combination with the [changeset Github action](https://github.com/changesets/action). The rough idea is:
1. create PRs for new features, updates and fixes (with a changeset if relevant for changelog)
2. merge into main
3. changeset creates a PR that bumps all packages based on the changesets
4. merge changeset PR if you want to release to Github and npm
## Built by [xyflow](https://xyflow.com)
React Flow and Svelte Flow are maintained by the [xyflow team](https://xyflow.com/about). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs).
## License
React Flow and Svelte Flow are [MIT licensed](./LICENSE).
## /SECURITY.md
# Security Policy
## Reporting a Vulnerability
To report a vulnerability, please privately report it via the Security tab on the correct GitHub repository ([see documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)). Do not open a public issue. Provide:
- A clear description of the issue
- Steps to reproduce
- Expected vs actual behavior
- Potential impact
- A proof of concept if possible
- Affected commit / version (if known)
## Acknowledgment Timeline
We aim to acknowledge receipt of a valid report within 1 week.
## Resolution Timeline
We aim to provide a remediation plan or decision within 4 weeks. Actual fix time may be shorter or longer depending on severity, complexity, and scope.
## Scope & Threat Model
In scope:
- Vulnerabilities introduced by code in this repository
- Supply-chain risks caused by how this repository consumes or distributes its own code (e.g. insecure GitHub Actions)
Out of scope:
- Issues only present in third-party dependencies (please report those upstream)
- Issues that can only be exploited when the underlying platform (browser, server runtime) is compromised
- Denial of service via excessive legitimate use
### Supported Versions
The following versions are currently supported with security updates:
| Package | Version | Supported |
| -------------- | ------- | --------------------- |
| @xyflow/react | 12.x | ✅ Fully supported |
| @xyflow/svelte | 1.x | ✅ Fully supported |
| @xyflow/system | 0.x | ✅ Fully supported |
| reactflow | > 11.x | ⚠ Depends on severity |
| reactflow | < 11.x | ❌ No longer |
## Disclosure
Please keep reports private until a fix is released and a Security Advisory is public.
## /dependabot.yml
```yml path="/dependabot.yml"
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/packages/'
schedule:
interval: 'weekly'
ignore:
- dependency-type: 'development'
```
## /examples/README.md
# Examples for Testing & Development
These examples are used for feature development and E2E testing.
## Overview
SvelteKit App with Svelte Flow examples at [`examples/svelte`](./svelte)
Vite App with React Flow examples at [`examples/react`](./react)
Astro App used for SSR testing at [`examples/astro-xyflow`](./astro-xyflow)
## E2E
For further documentation of E2E tests have a look at [`/tests/playwright`](/tests/playwright).
## /examples/astro-xyflow/.gitignore
```gitignore path="/examples/astro-xyflow/.gitignore"
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
```
## /examples/astro-xyflow/README.md
# Astro examples
Tiny app for testing React Flow and Svelte Flow with Astro.
## Start local dev server
```sh
pnpm dev
```
## /examples/astro-xyflow/astro.config.mjs
```mjs path="/examples/astro-xyflow/astro.config.mjs"
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
// https://astro.build/config
export default defineConfig({
integrations: [react(), svelte()],
});
```
## /examples/astro-xyflow/package.json
```json path="/examples/astro-xyflow/package.json"
{
"name": "astro-examples",
"type": "module",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^5.0.5",
"@astrojs/svelte": "^8.1.2",
"@types/react": "^18.2.24",
"@types/react-dom": "^18.2.8",
"@xyflow/react": "workspace:^",
"@xyflow/svelte": "workspace:^",
"astro": "^6.3.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"svelte": "^5.55.9"
}
}
```
## /examples/astro-xyflow/public/.gitkeep
```gitkeep path="/examples/astro-xyflow/public/.gitkeep"
```
## /examples/astro-xyflow/src/components/ReactFlowExample/CustomNode.tsx
```tsx path="/examples/astro-xyflow/src/components/ReactFlowExample/CustomNode.tsx"
import { memo, type FC, type CSSProperties } from 'react';
import { Handle, Position, type NodeProps } from '@xyflow/react';
const sourceHandleStyleA: CSSProperties = { left: 50 };
const sourceHandleStyleB: CSSProperties = {
right: 50,
left: 'auto',
};
const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
return (
<>
<Handle type="target" position={Position.Top} />
<div>
<div>
Label: <strong>{data.label}</strong>
</div>
<div>
Position:{' '}
<strong>
{positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
</strong>
</div>
</div>
<Handle type="source" position={Position.Bottom} id="a" style={sourceHandleStyleA} />
<Handle type="source" position={Position.Bottom} id="b" style={sourceHandleStyleB} />
</>
);
};
export default memo(CustomNode);
```
## /examples/astro-xyflow/src/components/ReactFlowExample/index.tsx
```tsx path="/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx"
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
useEdgesState,
useNodesState,
Background,
Controls,
type Connection,
type Edge,
type Node,
Position,
} from '@xyflow/react';
import CustomNode from './CustomNode';
import '@xyflow/react/dist/style.css';
const nodeSize = {
width: 100,
height: 40,
};
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
...nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
},
],
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
...nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
},
{
type: 'target',
position: Position.Top,
x: nodeSize.width * 0.5,
y: 0,
},
],
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 900, y: 100 },
...nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
},
{
type: 'target',
position: Position.Top,
x: nodeSize.width * 0.5,
y: 0,
},
],
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
width: 200,
height: 50,
type: 'custom',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3', animated: true },
];
const nodeTypes = {
custom: CustomNode,
};
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<div style={{ width: 700, height: 400 }}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
// fitView
width={700}
height={400}
onlyRenderVisibleElements
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default Flow;
```
## /examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx
```tsx path="/examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx"
import { memo, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
function CustomNode() {
const [text, setText] = useState('this is a pretty long text');
return (
<>
<Handle type="target" position={Position.Top} />
<div style={{ background: '#eee', padding: 10 }}>
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<div>text: {text}</div>
</div>
</div>
<Handle type="source" position={Position.Bottom} />
</>
);
}
export default memo(CustomNode);
```
## /examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx
```tsx path="/examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx"
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
useEdgesState,
useNodesState,
Background,
Controls,
type Connection,
type Edge,
type Node,
} from '@xyflow/react';
import CustomNode from './CustomNode';
import '@xyflow/react/dist/style.css';
const initialNodes: Node[] = [
{
id: '1',
data: {},
position: { x: 0, y: 0 },
initialWidth: 200,
initialHeight: 50,
type: 'custom',
},
{
id: '2',
data: {},
position: { x: 0, y: 200 },
width: 200,
initialHeight: 50,
type: 'custom',
},
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const nodeTypes = {
custom: CustomNode,
};
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<div style={{ width: 700, height: 400 }}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
width={700}
height={400}
debug
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default Flow;
```
## /examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte
```svelte path="/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte"
<script lang="ts">
import {
SvelteFlow,
Controls,
Background,
BackgroundVariant,
Position,
ViewportPortal,
type Node,
type Edge,
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
let nodes = $state.raw<Node[]>([
{
id: '0',
position: { x: 0, y: 150 },
data: { label: 'Node 0' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
{
id: 'A',
position: { x: 250, y: 0 },
data: { label: 'A' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
{
id: 'B',
position: { x: 250, y: 150 },
data: { label: 'B' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
{
id: 'C',
position: { x: 950, y: 300 },
data: { label: 'C' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
width: 100,
height: 40,
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
]);
let edges = $state.raw<Edge[]>([
{ id: '0A', source: '0', target: 'A', animated: true },
{ id: '0B', source: '0', target: 'B', animated: true },
{ id: '0C', source: '0', target: 'C', animated: true },
]);
const defaultEdgeOptions = {
animated: true,
};
</script>
<div style="height: 400px; width: 700px;">
<SvelteFlow bind:nodes bind:edges onlyRenderVisibleElements {defaultEdgeOptions} width={700} height={400}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<ViewportPortal target="front">
<div style:transform="translate(100px, 100px)" style:position="absolute">[100, 100] inside the flow.</div>
</ViewportPortal>
</SvelteFlow>
</div>
```
## /examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte
```svelte path="/examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte"
<script lang="ts">
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
type $Props = NodeProps;
let text = 'some default text';
</script>
<Handle type="target" position={Position.Left} />
<div class="custom">
<div>
text: {text}
</div>
<input type="text" bind:value={text} />
</div>
<Handle type="source" position={Position.Right} />
<style>
.custom {
background-color: white;
padding: 10px;
border: 1px solid #777;
border-radius: 20px;
}
</style>
```
## /examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte
```svelte path="/examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte"
<script lang="ts">
import { SvelteFlow, Controls, Background, BackgroundVariant, type Node, type Edge } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './CustomNode.svelte';
let nodes = $state.raw<Node[]>([
{
id: '1',
position: { x: 0, y: 0 },
data: {},
initialWidth: 100,
initialHeight: 40,
type: 'input-node',
},
{
id: '2',
position: { x: 0, y: 150 },
data: {},
initialWidth: 100,
initialHeight: 40,
type: 'input-node',
},
]);
let edges = $state.raw<Edge[]>([{ id: '1-2', source: '1', target: '2' }]);
const nodeTypes = {
'input-node': CustomNode,
};
</script>
<div style="height: 400px; width: 700px;">
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView width={700} height={400}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
</div>
```
## /examples/astro-xyflow/src/pages/index.astro
```astro path="/examples/astro-xyflow/src/pages/index.astro"
---
import ReactFlowApp from '../components/ReactFlowExample';
import ReactFlowInitialApp from '../components/ReactFlowInitialExample';
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte';
import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.svelte';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro example for React Flow and Svelte Flow</title>
<style>
html,
body {
margin: 0;
font-family: sans-serif;
}
</style>
</head>
<body>
<h2>React Flow</h2>
<p>no client hydration</p>
<ReactFlowApp />
<p>client hydration on load (client:load)</p>
<ReactFlowApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<ReactFlowInitialApp client:load />
<h2>Svelte Flow</h2>
<SvelteFlowApp />
<p>client hydration on load (client:load)</p>
<SvelteFlowApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<SvelteFlowInitialApp client:load />
</body>
</html>
```
## /examples/astro-xyflow/tsconfig.json
```json path="/examples/astro-xyflow/tsconfig.json"
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
```
## /examples/react/.gitignore
```gitignore path="/examples/react/.gitignore"
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
```
## /examples/react/README.md
# React Flow examples
This Vite app is used internally to develop and test the library.
## Start local dev server
```sh
pnpm dev
```
## Adding new example
Development of the library is done against `src/examples`. Feel free to add new implementations for features that you develop.
1. Create a new folder & flow at `src/examples/`
2. Register the new route in `src/App/routes.ts`
## Adding new E2E implementation
E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under [`http://localhost:3000/tests/generic/$foldername/$filename`](http://localhost:5173/tests/generic/nodes/general). For further documentation visit [`xyflow/tests/playwright`](/tests/playwright).
## /examples/react/cypress.config.ts
```ts path="/examples/react/cypress.config.ts"
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000/examples',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshotOnRunFailure: false,
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
screenshotOnRunFailure: false,
video: false,
},
});
```
## /examples/react/cypress/components/hooks/useEdges.cy.tsx
```tsx path="/examples/react/cypress/components/hooks/useEdges.cy.tsx"
import React, { useEffect } from 'react';
import { ReactFlow, Edge, useEdges } from '@xyflow/react';
import { nodes as initialNodes, edges as initialEdges } from '../../fixtures/simpleflow';
describe('useEdges.cy.tsx', () => {
it('handles edges', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow nodes={initialNodes} edges={initialEdges}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
});
it('handles defaultEdges', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultNodes={initialNodes} defaultEdges={initialEdges}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
});
});
const HookHelperComponent = ({ onChange }: { onChange: (edges: Edge[]) => void }) => {
const edges = useEdges();
useEffect(() => {
onChange(edges);
}, [edges]);
return null;
};
```
## /examples/react/cypress/components/hooks/useNodes.cy.tsx
```tsx path="/examples/react/cypress/components/hooks/useNodes.cy.tsx"
import { useEffect } from 'react';
import { ReactFlow, Node, useNodes } from '@xyflow/react';
import { nodes } from '../../fixtures/simpleflow';
const nodeDimensions = {
width: 100,
height: 50,
};
const initialNodes: Node[] = nodes.map((n) => ({
...n,
style: nodeDimensions,
}));
const expectedNodes: Node[] = initialNodes.map((n) => ({
...n,
measured: {
...nodeDimensions,
},
}));
describe('useNodes.cy.tsx', () => {
it('handles nodes', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow nodes={initialNodes}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
});
it('handles defaultNodes', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultNodes={initialNodes}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
});
});
const HookHelperComponent = ({ onChange }: { onChange: (nodes: Node[]) => void }) => {
const nodes = useNodes();
useEffect(() => {
onChange(nodes);
}, [nodes]);
return null;
};
```
## /examples/react/cypress/components/hooks/useNodesInitialized.cy.tsx
```tsx path="/examples/react/cypress/components/hooks/useNodesInitialized.cy.tsx"
import { ReactFlow, useNodesInitialized } from '@xyflow/react';
import { nodes } from '../../fixtures/simpleflow';
import ControlledFlow from '../../support/ControlledFlow';
describe('useNodesInitialized.cy.tsx', () => {
it('returns false for no nodes', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow nodes={[]}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
// cy.get('@initSpy').should('to.be.calledOnce');
cy.get('@initSpy').should('have.been.calledWith', false);
});
it('returns false without onNodesChange', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow nodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
// cy.get('@initSpy').should('to.be.calledOnce');
cy.get('@initSpy').should('have.be.calledWith', false);
});
it('returns true for defaultNodes', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow defaultNodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
cy.get('@initSpy').should('have.been.calledWith', false);
cy.get('@initSpy').should('have.been.calledWith', true);
});
it('returns true for nodes + onNodesChange', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ControlledFlow initialNodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ControlledFlow>
);
cy.get('@initSpy').should('have.been.calledWith', false);
cy.get('@initSpy').should('have.been.calledWith', true);
});
});
// test specific helpers
function HookHelperComponent({ onChange }: { onChange: (init: boolean) => void }) {
const initialized = useNodesInitialized();
onChange(initialized);
return null;
}
```
## /examples/react/cypress/components/hooks/useOnViewportChange.cy.tsx
```tsx path="/examples/react/cypress/components/hooks/useOnViewportChange.cy.tsx"
import { ReactFlow, useOnViewportChange, Viewport } from '@xyflow/react';
describe('useOnViewportChange.cy.tsx', () => {
it('listen to viewport drag', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
cy.get('@onStartSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
cy.get('@onEndSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
});
});
it('handles zoom in', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.zoomPane(-200).then(() => {
cy.get('@onStartSpy').should('have.been.callCount', 1);
cy.get('@onChangeSpy').should('have.been.callCount', 1);
cy.get('@onEndSpy').should('have.been.callCount', 1);
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
});
});
it('handles zoom out', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.zoomPane(200).then(() => {
cy.get('@onStartSpy').should('have.been.callCount', 1);
cy.get('@onChangeSpy').should('have.been.callCount', 1);
cy.get('@onEndSpy').should('have.been.callCount', 1);
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
});
});
it('handles default viewport', () => {
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
const onStartSpy = cy.spy().as('onStartSpy');
cy.mount(
<ReactFlow defaultViewport={defaultViewport}>
<HookHelperComponent onStart={onStartSpy} />
</ReactFlow>
);
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
cy.get('@onStartSpy').should('have.been.calledWith', defaultViewport);
});
});
});
// test specific helpers
function HookHelperComponent({
onStart,
onChange,
onEnd,
}: {
onStart?: (viewport: Viewport) => void;
onChange?: (viewport: Viewport) => void;
onEnd?: (viewport: Viewport) => void;
}) {
useOnViewportChange({ onStart, onChange, onEnd });
return null;
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}
```
## /examples/react/cypress/components/hooks/useViewport.cy.tsx
```tsx path="/examples/react/cypress/components/hooks/useViewport.cy.tsx"
import { ReactFlow, useViewport, Viewport } from '@xyflow/react';
describe('useViewport.cy.tsx', () => {
it('handles drag', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
cy.dragPane({ from: { x: 50, y: 0 }, to: { x: 50, y: 400 } }).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
expect(getLatestViewport(onChangeSpy)).to.eql({ x: 0, y: 400, zoom: 1 });
});
});
it('handles zoom in', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.zoomPane(-200).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
});
});
it('handles zoom out', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.zoomPane(200).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
});
});
it('handles default viewport', () => {
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultViewport={defaultViewport}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', defaultViewport);
});
});
// test specific helpers
function HookHelperComponent({ onChange }: { onChange: (vp: Viewport) => void }) {
const viewport = useViewport();
onChange(viewport);
return null;
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}
```
## /examples/react/cypress/components/reactflow/basic-props.cy.tsx
```tsx path="/examples/react/cypress/components/reactflow/basic-props.cy.tsx"
import { ReactFlow, EdgeProps } from '@xyflow/react';
import ControlledFlow from '../../support/ControlledFlow';
import * as simpleflow from '../../fixtures/simpleflow';
describe('<ReactFlow />: Basic Props', () => {
describe('uses defaultNodes and defaultEdges', () => {
beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} nodeDragThreshold={0} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
describe('uses nodes and edges', () => {
beforeEach(() => {
cy.mount(
<ControlledFlow
addOnNodeChangeHandler={false}
initialNodes={simpleflow.nodes}
initialEdges={simpleflow.edges}
/>
);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('can not drag a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.equal(styleAfterDrag);
});
});
});
describe('uses onNodesChange handler', () => {
beforeEach(() => {
cy.mount(<ControlledFlow addOnConnectHandler={false} initialNodes={simpleflow.nodes} />);
});
it('mounts nodes', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('can not connect nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 0);
});
});
describe('uses onEdgeChange handler', () => {
beforeEach(() => {
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={simpleflow.edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('selects an edge', () => {
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
});
});
describe('uses onConnect handlers', () => {
beforeEach(() => {
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} />);
});
it('mounts nodes', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('connects nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 1);
});
});
describe('uses nodeTypes', () => {
it('renders custom nodes', () => {
const nodeTypes = {
custom: () => <div className="customnode">custom node</div>,
};
const initialNodes = simpleflow.nodes.map((n) => ({
...n,
type: 'custom',
}));
cy.mount(<ControlledFlow nodeTypes={nodeTypes} initialNodes={initialNodes} />);
cy.get('.react-flow__node-custom').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow').contains('custom node');
});
it('tries invalid node type - should fallback to default', () => {
const initialNodes = simpleflow.nodes.map((n) => ({
...n,
type: 'invalid',
}));
cy.mount(<ControlledFlow initialNodes={initialNodes} />);
cy.get('.react-flow__node-invalid').should('have.length', 0);
cy.get('.react-flow__node-default').should('have.length', simpleflow.nodes.length);
});
});
describe('uses edgeTypes', () => {
it('renders custom edge', () => {
const edgeTypes = {
custom: ({ sourceX, sourceY, targetX, targetY }: EdgeProps) => (
<path d={`M${sourceX} ${sourceY} L${targetX} ${targetY}`} stroke="black" />
),
};
const initialEdges = simpleflow.edges.map((e) => ({
...e,
type: 'custom',
}));
cy.mount(<ControlledFlow edgeTypes={edgeTypes} initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
cy.get('.react-flow__edge-custom').should('have.length', simpleflow.edges.length);
});
it('tries invalid edge type - should fallback to default', () => {
const initialEdges = simpleflow.edges.map((e) => ({
...e,
type: 'invalid',
}));
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
cy.get('.react-flow__edge-invalid').should('have.length', 0);
cy.get('.react-flow__edge-default').should('have.length', simpleflow.edges.length);
});
});
it('uses style', () => {
const styles = {
backgroundColor: 'red',
};
cy.mount(<ControlledFlow style={styles} />);
cy.get('.react-flow').should('have.css', 'background-color', 'rgb(255, 0, 0)');
});
it('uses classname', () => {
cy.mount(<ControlledFlow className="custom" />);
cy.get('.react-flow').should('have.class', 'custom');
});
});
```
## /examples/react/cypress/components/reactflow/event-handlers.cy.tsx
```tsx path="/examples/react/cypress/components/reactflow/event-handlers.cy.tsx"
import { skipOn } from '@cypress/skip-test';
import { nodes, edges } from '../../fixtures/simpleflow';
import ControlledFlow from '../../support/ControlledFlow';
describe('<ReactFlow />: Event handlers', () => {
describe('Node event handlers', () => {
it('handles onInit', () => {
const onInitSpy = cy.spy().as('onInitSpy');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onInit={onInitSpy} />)
.wait(100)
.then(() => {
expect(onInitSpy.callCount).to.be.eq(1);
});
});
it('handles onNodeClick', () => {
const onNodeClickSpy = cy.spy().as('onNodeClickSpy');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onNodeClick={onNodeClickSpy} />).then(() => {
expect(onNodeClickSpy.callCount).to.be.eq(0);
cy.get('.react-flow__node:first')
.click()
.then(() => {
expect(onNodeClickSpy.callCount).to.be.eq(1);
});
});
});
it('handles onNodeDrag handlers', () => {
const onNodeDragStart = cy.spy().as('onNodeDragStart');
const onNodeDrag = cy.spy().as('onNodeDrag');
const onNodeDragStop = cy.spy().as('onNodeDragStop');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
/>
).then(() => {
expect(onNodeDragStart.callCount).to.be.eq(0);
expect(onNodeDrag.callCount).to.be.eq(0);
expect(onNodeDragStop.callCount).to.be.eq(0);
cy.drag('.react-flow__node:first', { x: 200, y: 0 }).then(() => {
expect(onNodeDragStart.callCount).to.be.gt(0);
expect(onNodeDrag.callCount).to.be.gt(0);
expect(onNodeDragStop.callCount).to.be.gt(0);
});
});
});
it('handles onNodeMouse handlers', () => {
const onNodeMouseEnter = cy.spy().as('onNodeMouseEnter');
const onNodeMouseMove = cy.spy().as('onNodeMouseMove');
const onNodeMouseLeave = cy.spy().as('onNodeMouseLeave');
const onNodeContextMenu = cy.spy().as('onNodeContextMenu');
const onNodeDoubleClick = cy.spy().as('onNodeDoubleClick');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
/>
)
.wait(200)
.then(() => {
expect(onNodeMouseEnter.callCount).to.be.eq(0);
expect(onNodeMouseMove.callCount).to.be.eq(0);
expect(onNodeMouseLeave.callCount).to.be.eq(0);
expect(onNodeContextMenu.callCount).to.be.eq(0);
expect(onNodeDoubleClick.callCount).to.be.eq(0);
const node = cy.get('.react-flow__node').contains('Node 1');
node
.rightclick()
.dblclick()
.then(() => {
expect(onNodeContextMenu.callCount).to.be.eq(1);
expect(onNodeDoubleClick.callCount).to.be.eq(1);
});
skipOn('firefox');
node
.realHover()
.realMouseMove(100, 100)
.then(() => {
expect(onNodeMouseEnter.callCount).to.be.gt(0);
expect(onNodeMouseMove.callCount).to.be.gt(0);
expect(onNodeMouseLeave.callCount).to.be.gt(0);
});
});
});
});
describe('Edge event handlers', () => {
it('handles onEdgeClick', () => {
const onEdgeClick = cy.spy().as('onEdgeClick');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onEdgeClick={onEdgeClick} />).then(() => {
expect(onEdgeClick.callCount).to.be.eq(0);
cy.get('.react-flow__edge:first')
.click()
.then(() => {
expect(onEdgeClick.callCount).to.be.eq(1);
});
});
});
it('handles onEdgeMouse handlers', () => {
const onEdgeMouseEnter = cy.spy().as('onEdgeMouseEnter');
const onEdgeMouseMove = cy.spy().as('onEdgeMouseMove');
const onEdgeMouseLeave = cy.spy().as('onEdgeMouseLeave');
const onEdgeContextMenu = cy.spy().as('onEdgeContextMenu');
const onEdgeDoubleClick = cy.spy().as('onEdgedoubleClick');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeDoubleClick={onEdgeDoubleClick}
/>
)
.wait(200)
.then(() => {
expect(onEdgeMouseEnter.callCount).to.be.eq(0);
expect(onEdgeMouseMove.callCount).to.be.eq(0);
expect(onEdgeMouseLeave.callCount).to.be.eq(0);
expect(onEdgeContextMenu.callCount).to.be.eq(0);
expect(onEdgeDoubleClick.callCount).to.be.eq(0);
const edge = cy.get('.react-flow__edge:first');
edge
.rightclick()
.dblclick()
.then(() => {
expect(onEdgeContextMenu.callCount).to.be.eq(1);
});
skipOn('firefox');
edge
.realHover()
.realMouseMove(100, 100)
.then(() => {
expect(onEdgeMouseEnter.callCount).to.be.gt(0);
expect(onEdgeMouseMove.callCount).to.be.gt(0);
expect(onEdgeMouseLeave.callCount).to.be.gt(0);
});
});
});
});
describe('Pane event handlers', () => {
it('handles onMove handlers', () => {
const onMoveStart = cy.spy().as('onMoveStart');
const onMove = cy.spy().as('onMove');
const onMoveEnd = cy.spy().as('onMoveEnd');
cy.mount(<ControlledFlow onMoveStart={onMoveStart} onMove={onMove} onMoveEnd={onMoveEnd} />).then(() => {
expect(onMoveStart.callCount).to.be.eq(0);
expect(onMove.callCount).to.be.eq(0);
expect(onMoveEnd.callCount).to.be.eq(0);
cy.dragPane({ from: { x: 10, y: 200 }, to: { x: 100, y: 200 } })
.wait(100)
.then(() => {
expect(onMoveStart.callCount).to.be.eq(1);
expect(onMove.callCount).to.be.gt(0);
expect(onMoveEnd.callCount).to.be.eq(1);
});
});
});
it('handles click handlers', () => {
const onPaneClick = cy.spy().as('onPaneClick');
const onPaneContextMenu = cy.spy().as('onPaneContextMenu');
cy.mount(<ControlledFlow onPaneClick={onPaneClick} onPaneContextMenu={onPaneContextMenu} />).then(() => {
expect(onPaneClick.callCount).to.be.eq(0);
expect(onPaneContextMenu.callCount).to.be.eq(0);
cy.get('.react-flow__pane')
.rightclick()
.click()
.wait(100)
.then(() => {
expect(onPaneClick.callCount).to.be.eq(1);
expect(onPaneContextMenu.callCount).to.be.eq(1);
});
});
});
});
});
```
## /examples/react/cypress/components/reactflow/multiple-instance.cy.tsx
```tsx path="/examples/react/cypress/components/reactflow/multiple-instance.cy.tsx"
import { ReactFlow, BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, ReactFlowProvider } from '@xyflow/react';
import * as simpleflow from '../../fixtures/simpleflow';
function CustomEdge(props: EdgeProps) {
const [path, labelX, labelY] = getSmoothStepPath(props);
return (
<>
<BaseEdge path={path} labelX={labelX} labelY={labelY} />
<EdgeLabelRenderer>
<div className="label">{props.id}</div>
</EdgeLabelRenderer>
</>
);
}
const simpleflow1 = { ...simpleflow };
simpleflow1.edges = [...simpleflow1.edges];
simpleflow1.edges[0] = { ...simpleflow1.edges[0], id: 'edge1' };
const simpleflow2 = { ...simpleflow };
simpleflow2.edges = [...simpleflow2.edges];
simpleflow2.edges[0] = { ...simpleflow2.edges[0], id: 'edge2' };
describe('<ReactFlow />: Multiple Instances', () => {
describe('render EdgeLabelRenderer', () => {
beforeEach(() => {
cy.mount(
<>
<ReactFlowProvider>
<ReactFlow
defaultNodes={simpleflow1.nodes}
edgeTypes={{ default: CustomEdge }}
defaultEdges={simpleflow1.edges}
/>
</ReactFlowProvider>
<ReactFlowProvider>
<ReactFlow
defaultNodes={simpleflow2.nodes}
edgeTypes={{ default: CustomEdge }}
defaultEdges={simpleflow2.edges}
/>
</ReactFlowProvider>
</>
);
});
it('Each ReactFlow instance has one edge label in EdgeLabelRenderer', () => {
cy.get('.react-flow__edgelabel-renderer').should('have.length', 2);
cy.get('.react-flow__edgelabel-renderer')
.eq(0)
.within(() => {
cy.get('.label').should('have.length', 1).should('contain.text', 'edge1');
});
cy.get('.react-flow__edgelabel-renderer')
.eq(1)
.within(() => {
cy.get('.label').should('have.length', 1).should('contain.text', 'edge2');
});
});
});
});
```
## /examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx
```tsx path="/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx"
import { useCallback, useState } from 'react';
import { OnNodesChange, Panel, ReactFlow, ReactFlowProps, applyNodeChanges, useReactFlow, Node } from '@xyflow/react';
const nodes = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
style: { width: 100, height: 40 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 200, y: 200 },
style: { width: 80, height: 50 },
},
];
describe('<ReactFlow />: onNodesChange', () => {
beforeEach(() => {
const onNodesChange = cy.spy().as('onNodesChange');
cy.mount(<Comp nodes={nodes} onNodesChange={onNodesChange} />).wait(500);
});
it('receive initial dimension change', () => {
const expectedChanges = nodes.map((n) => ({
type: 'dimensions',
id: n.id,
dimensions: n.style,
}));
cy.get('@onNodesChange').should('have.callCount', 1);
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
it('receive replace and dimension change', () => {
const expectedDimensionChanges = [
{
type: 'dimensions',
id: '1',
dimensions: { width: 200, height: 100 },
},
];
const expectedReplaceChanges = [
{
type: 'replace',
id: '1',
item: {
...nodes[0],
measured: { width: 200, height: 100 },
style: { width: 200, height: 100 },
},
},
];
cy.get('[data-id=update-btn]').click();
cy.get('@onNodesChange').should('have.callCount', 3);
cy.get('@onNodesChange').then((s) => {
// dimension change (already checked above)
// const firstCall = s.getCall(0);
// replace change
// @ts-ignore
const secondChangeCallArgs = s.getCall(1).args[0];
// dimension change
// @ts-ignore
const thirdChangeCallArgs = s.getCall(2).args[0];
expect(secondChangeCallArgs).to.be.deep.eq(expectedReplaceChanges);
expect(thirdChangeCallArgs).to.be.deep.eq(expectedDimensionChanges);
});
});
it('receive select and unselect change', () => {
const expectedSelectChanges = [{ id: '1', type: 'select', selected: true }];
const expectedUnselectChanges = [{ id: '1', type: 'select', selected: false }];
cy.get('.react-flow__node').first().click();
cy.get('@onNodesChange').should('have.calledWith', expectedSelectChanges);
cy.get('.react-flow__pane').first().click({ force: true });
cy.get('@onNodesChange').should('have.calledWith', expectedUnselectChanges);
});
it('receive position change', () => {
const endPosition = { x: 200, y: 25 };
const expectedChanges = [
{ id: '1', type: 'position', dragging: false, position: endPosition, positionAbsolute: endPosition },
];
cy.drag('.react-flow__node:first', endPosition).then(() => {
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
it('receive remove change', () => {
const expectedChanges = [{ id: '1', type: 'remove' }];
cy.get('.react-flow__node').first().click();
cy.realPress('Backspace');
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
// test specific helpers
function UpdateButton() {
const { updateNode } = useReactFlow();
const updateNodeDimensions = () => {
updateNode('1', { style: { width: 200, height: 100 } });
};
return (
<Panel position="top-right">
<button data-id="update-btn" onClick={updateNodeDimensions} id="btn">
update
</button>
</Panel>
);
}
type CompProps = ReactFlowProps & { onNodesChange: (changes: any) => void };
function Comp(props: CompProps) {
const [nodes, setNodes] = useState(props.nodes || []);
const onNodesChange: OnNodesChange<Node> = useCallback((changes) => {
props.onNodesChange(changes);
setNodes((nds) => applyNodeChanges(changes, nds));
}, []);
return (
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} nodeDragThreshold={0}>
<UpdateButton />
</ReactFlow>
);
}
```
## /examples/react/cypress/components/reactflow/storeAvailabilityOnRemount.cy.tsx
```tsx path="/examples/react/cypress/components/reactflow/storeAvailabilityOnRemount.cy.tsx"
import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { ReactFlow, ReactFlowProvider, useStoreApi, type Node, type Edge, type NodeTypes, type NodeProps } from '@xyflow/react';
import { edges as initialEdges } from '../../fixtures/simpleflow';
type CheckResult = { nodesLength: number; lookupSize: number };
// We use a global ref so the custom node can report without prop drilling.
// Each test resets this before mounting.
let checkSpy: ((result: CheckResult) => void) | null = null;
/**
* Custom node rendered inside GraphView's subtree. Verifies that
* the store (nodes, nodeLookup, etc.) is populated by the time
* custom node effects fire after a <ReactFlow> key-based remount.
*/
function CheckerNode({ data }: NodeProps) {
const store = useStoreApi();
const useHook = data.useLayout ? useLayoutEffect : useEffect;
useHook(() => {
if (checkSpy) {
const { nodes, nodeLookup } = store.getState();
checkSpy({ nodesLength: nodes.length, lookupSize: nodeLookup.size });
}
});
return <div>{data.label as string}</div>;
}
const nodeTypes: NodeTypes = { checker: CheckerNode };
const makeNodes = (useLayout = false): Node[] => [
{ id: '1', type: 'checker', data: { label: 'Node 1', useLayout }, position: { x: 0, y: 0 } },
{ id: '2', type: 'checker', data: { label: 'Node 2', useLayout }, position: { x: 200, y: 200 } },
];
describe('<ReactFlow /> remount: store availability', () => {
it('node useEffect sees populated store after <ReactFlow> key remount', () => {
const spy = cy.spy().as('spy');
checkSpy = spy;
const nodes = makeNodes(false);
cy.mount(
<ReactFlowProvider>
<RemountableFlow nodes={nodes} edges={initialEdges} />
</ReactFlowProvider>
);
cy.get('@spy').should('have.been.calledWithMatch', { lookupSize: 2 });
cy.then(() => spy.resetHistory());
cy.get('[data-testid="remount"]').click();
cy.get('@spy').should('have.been.called');
cy.get('@spy').should('always.have.been.calledWithMatch', { lookupSize: 2 });
});
it('node useLayoutEffect sees populated store after <ReactFlow> key remount', () => {
const spy = cy.spy().as('spy');
checkSpy = spy;
const nodes = makeNodes(true);
cy.mount(
<ReactFlowProvider>
<RemountableFlow nodes={nodes} edges={initialEdges} />
</ReactFlowProvider>
);
cy.get('@spy').should('have.been.calledWithMatch', { lookupSize: 2 });
cy.then(() => spy.resetHistory());
cy.get('[data-testid="remount"]').click();
cy.get('@spy').should('have.been.called');
cy.get('@spy').should('always.have.been.calledWithMatch', { lookupSize: 2 });
});
it('node useEffect sees populated store after <ReactFlow> key remount with defaultNodes', () => {
const spy = cy.spy().as('spy');
checkSpy = spy;
const nodes = makeNodes(false);
const DefaultFlow = () => {
const [remountKey, setRemountKey] = useState(0);
return (
<>
<button data-testid="remount" onClick={() => setRemountKey((k) => k + 1)}>
Remount
</button>
<ReactFlow key={remountKey} defaultNodes={nodes} defaultEdges={initialEdges} nodeTypes={nodeTypes}>
<div />
</ReactFlow>
</>
);
};
cy.mount(
<ReactFlowProvider>
<DefaultFlow />
</ReactFlowProvider>
);
cy.get('@spy').should('have.been.calledWithMatch', { lookupSize: 2 });
cy.then(() => spy.resetHistory());
cy.get('[data-testid="remount"]').click();
cy.get('@spy').should('have.been.called');
cy.get('@spy').should('always.have.been.calledWithMatch', { lookupSize: 2 });
});
});
const RemountableFlow = ({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) => {
const [remountKey, setRemountKey] = useState(0);
const handleRemount = useCallback(() => {
setRemountKey((k) => k + 1);
}, []);
return (
<>
<button data-testid="remount" onClick={handleRemount}>
Remount
</button>
<ReactFlow key={remountKey} nodes={nodes} edges={edges} nodeTypes={nodeTypes}>
<div />
</ReactFlow>
</>
);
};
```
## /examples/react/cypress/components/reactflow/uncontrolled.cy.tsx
```tsx path="/examples/react/cypress/components/reactflow/uncontrolled.cy.tsx"
import { ReactFlow } from '@xyflow/react';
import { nodes, edges } from '../../fixtures/simpleflow';
describe('<ReactFlow />: Uncontrolled Flow', () => {
beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={nodes} defaultEdges={edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', nodes.length);
cy.get('.react-flow__edge').should('have.length', edges.length);
});
it('selects a node', () => {
cy.get('.react-flow__node').first().click().should('have.class', 'selected');
cy.get('.react-flow__pane').click();
cy.get('.react-flow__node').first().should('not.have.class', 'selected');
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('selects an edge', () => {
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
cy.get('.react-flow__pane').click();
cy.get('.react-flow__edge').first().should('not.have.class', 'selected');
});
});
```
## /examples/react/cypress/components/reactflow/view-props.cy.tsx
```tsx path="/examples/react/cypress/components/reactflow/view-props.cy.tsx"
import { ReactFlow, ReactFlowProps, Viewport, useViewport, SnapGrid, CoordinateExtent, Node } from '@xyflow/react';
import ControlledFlow from '../../support/ControlledFlow';
import * as simpleflow from '../../fixtures/simpleflow';
const nodesOutsideOfView = [
{
...simpleflow.nodes[0],
position: {
x: -500,
y: 0,
},
},
{
...simpleflow.nodes[1],
position: {
x: 500,
y: 0,
},
},
];
describe('<ReactFlow />: View Props', () => {
it('uses fitView', () => {
cy.mount(<Comp nodes={nodesOutsideOfView} fitView minZoom={0.2} />).wait(200);
cy.get('.react-flow__node:first').isWithinViewport();
});
it('uses fitViewOptions', () => {
const fitViewOptions = { padding: 0.5 };
cy.mount(<ReactFlow nodes={[nodesOutsideOfView[0]]} fitView fitViewOptions={fitViewOptions} />).wait(200);
cy.get('.react-flow__node:first').isWithinViewport();
});
describe('uses minZoom', () => {
it('minZoom: 0.2', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp minZoom={0.2} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(0.2);
});
});
it('minZoom: 1', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp minZoom={1} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
it('sets invalid defaultZoom', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 0.2 };
cy.mount(<Comp minZoom={1} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
});
describe('uses maxZoom', () => {
it('maxZoom: 1', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp maxZoom={1} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
it('maxZoom: 2', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp maxZoom={2} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(2);
});
});
it('sets invalid defaultZoom', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 2 };
cy.mount(<Comp maxZoom={1.5} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1.5);
});
});
});
describe('uses defaultViewport', () => {
it('defaultViewport: { x: 0, y: 0, zoom: 1 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 1 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: { x: 0, y: 0, zoom: 1.5 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: { x: 100, y: 100, zoom: 1.5 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 100, y: 100, zoom: 1.5 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: invalid { x: 0, y: 0, zoom: -1 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: -1 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
// this should be min zoom because zoom is clamped
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(0.5);
});
});
it('defaultViewport: invalid { x: 0, y: 0, zoom: 100 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 100 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
// this should be max zoom because zoom is clamped
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(2);
});
});
});
it('uses snapGrid and snapToGrid', () => {
const snapGrid: SnapGrid = [100, 100];
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} snapGrid={snapGrid} snapToGrid />).then(() => {
cy.drag('.react-flow__node:first', { x: 125, y: 125 }).then(($el: any) => {
const transformAfterDrag = $el.css('transform');
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transformAfterDrag);
expect([nodeX, nodeY]).to.eql(snapGrid);
});
});
});
describe('uses onlyRenderVisibleElements', () => {
it('displays no nodes', () => {
cy.mount(<ControlledFlow initialNodes={nodesOutsideOfView} onlyRenderVisibleElements />).then(() => {
cy.get('.react-flow__node').should('have.length', 0);
});
});
it('displays one node', () => {
const nodes = nodesOutsideOfView.map((n) => {
if (n.id === '1') {
return { ...n, position: { x: 0, y: 0 } };
}
return n;
});
cy.mount(<ControlledFlow initialNodes={nodes} onlyRenderVisibleElements />).then(() => {
cy.get('.react-flow__node').should('have.length', 1);
});
});
});
it('uses translateExtent', () => {
const translateExtent: CoordinateExtent = [
[0, 0],
[1000, 1000],
];
cy.mount(<ControlledFlow translateExtent={translateExtent} />).then(() => {
const transformBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
cy.dragPane({ from: { x: 0, y: 0 }, to: { x: 100, y: 100 } }).then(() => {
const transformAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(transformBeforeDrag).to.eql(transformAfterDrag);
});
});
});
it('uses nodeExtent', () => {
const nodes: Node[] = [
{
id: '1',
position: { x: 200, y: 200 },
data: { label: '1' },
},
];
const nodeExtent: CoordinateExtent = [
[0, 0],
[50, 50],
];
cy.mount(<ControlledFlow nodes={nodes} nodeExtent={nodeExtent} />)
.wait(500)
.then(() => {
const transform = Cypress.$('.react-flow__node:first').css('transform');
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transform);
expect(nodeX).to.equal(50);
expect(nodeY).to.equal(50);
});
});
describe('uses preventScrolling', () => {
function ScrollFlow({ preventScrolling }: { preventScrolling: boolean }) {
return (
<div className="wrapper" style={{ height: 2000 }}>
<div className="inner" style={{ height: 500 }}>
<ControlledFlow preventScrolling={preventScrolling} />
</div>
</div>
);
}
it('lets user scroll', () => {
cy.mount(<ScrollFlow preventScrolling={false} />).then(() => {
cy.window().then((window) => {
cy.get('.react-flow__pane').trigger('wheel', {
wheelDelta: 240,
wheelDeltaX: 0,
wheelDeltaY: 240,
eventConstructor: 'WheelEvent',
view: window,
});
// @TODO: why is this not working?
// it seems that the event is somehow broken. This works fine when using the mouse.
// cy.window().its('scrollY').should('be.gt', 0);
});
});
});
it('does not user scroll', () => {
cy.mount(<ScrollFlow preventScrolling={true} />).then(() => {
cy.window().then((window) => {
cy.get('.react-flow__pane').trigger('wheel', {
wheelDelta: 240,
wheelDeltaX: 0,
wheelDeltaY: 240,
eventConstructor: 'WheelEvent',
view: window,
});
cy.window().its('scrollY').should('be.equal', 0);
});
});
});
});
describe('uses attributionPosition', () => {
it('displays it on the bottom right', () => {
cy.mount(<ControlledFlow />).then(() => {
cy.get('.react-flow__attribution').then(($el) => {
const { left, top, width, height } = $el[0].getBoundingClientRect();
expect(left).equal(window.innerWidth - width);
expect(top).equal(window.innerHeight - height);
});
});
});
it('displays it on the top left', () => {
cy.mount(<ControlledFlow attributionPosition="top-left" />).then(() => {
cy.get('.react-flow__attribution').then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(left).equal(0);
expect(top).equal(0);
});
});
});
});
});
// test specific helpers
type CompProps = {
onChange?: (vp: Viewport) => void;
};
function InnerComp({ onChange }: CompProps) {
const viewport = useViewport();
onChange?.(viewport);
return null;
}
function Comp({ onChange, ...rest }: CompProps & ReactFlowProps) {
return (
<ReactFlow {...rest}>
<InnerComp onChange={onChange} />
</ReactFlow>
);
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}
```
## /examples/react/cypress/components/utils/adopt-user-nodes.cy.ts
```ts path="/examples/react/cypress/components/utils/adopt-user-nodes.cy.ts"
import { adoptUserNodes, type NodeLookup, type ParentLookup } from '@xyflow/system';
import type { CoordinateExtent, Node, NodeOrigin } from '@xyflow/react';
describe('adoptUserNodes Testing', () => {
it('builds node lookup', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const userNode: Node = {
id: '1',
data: { label: 'node' },
position: { x: 250, y: 5 },
measured: { width: 100, height: 50 },
};
adoptUserNodes([userNode], nodeLookup, parentLookup);
const internalNode = nodeLookup.get('1');
expect(nodeLookup.size).to.equal(1);
expect(internalNode).to.have.property('internals');
});
it('calculates positionAbsolute with nodeOrigin', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const userNode: Node = {
id: '1',
data: { label: 'node' },
position: { x: 250, y: 5 },
measured: { width: 100, height: 50 },
};
adoptUserNodes([userNode], nodeLookup, parentLookup, {
nodeOrigin: [0.5, 0.5],
});
const internalNode = nodeLookup.get('1');
expect(internalNode?.internals.positionAbsolute.x).to.equal(
userNode.position.x - (userNode.measured?.width ?? 0) / 2
);
expect(internalNode?.internals.positionAbsolute.y).to.equal(
userNode.position.y - (userNode.measured?.height ?? 0) / 2
);
});
it('calculates positionAbsolute with nodeExtent', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodeExtent: CoordinateExtent = [
[0, 0],
[100, 100],
];
const userNode: Node = {
id: '1',
data: { label: 'node' },
position: { x: 500, y: 500 },
measured: { width: 50, height: 25 },
};
adoptUserNodes([userNode], nodeLookup, parentLookup, {
nodeExtent,
});
const internalNode = nodeLookup.get('1');
expect(internalNode?.internals.positionAbsolute.x).to.equal(nodeExtent[1][0] - userNode.measured!.width!);
expect(internalNode?.internals.positionAbsolute.y).to.equal(nodeExtent[1][1] - userNode.measured!.height!);
});
it('calculates positionAbsolute for sub flow', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const userParentNode: Node = {
id: '1',
data: { label: 'node' },
position: { x: 100, y: 100 },
measured: { width: 100, height: 50 },
};
const userChildNode: Node = {
id: '2',
data: { label: 'child' },
position: { x: 0, y: 0 },
measured: { width: 100, height: 50 },
parentId: '1',
};
adoptUserNodes([userParentNode, userChildNode], nodeLookup, parentLookup);
const internalChildNode = nodeLookup.get('2');
expect(nodeLookup.size).to.equal(2);
expect(internalChildNode?.internals.positionAbsolute.x).to.equal(
userChildNode.position.x + userParentNode.position.x
);
expect(internalChildNode?.internals.positionAbsolute.y).to.equal(
userChildNode.position.y + userParentNode.position.y
);
});
it('calculates positionAbsolute for sub flow with nodeOrigin', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const userParentNode: Node = {
id: '1',
data: { label: 'node' },
position: { x: 100, y: 100 },
measured: { width: 100, height: 50 },
};
const userChildNode: Node = {
id: '2',
data: { label: 'child' },
position: { x: 0, y: 0 },
measured: { width: 100, height: 50 },
parentId: '1',
};
adoptUserNodes([userParentNode, userChildNode], nodeLookup, parentLookup, {
nodeOrigin,
});
const internalParentNode = nodeLookup.get('1');
const internalChildNode = nodeLookup.get('2');
const expectedParentX = userParentNode.position.x - userParentNode.measured!.width! * nodeOrigin[0];
const expectedParentY = userParentNode.position.y - userParentNode.measured!.height! * nodeOrigin[1];
expect(internalParentNode?.internals.positionAbsolute.x).to.equal(expectedParentX);
expect(internalParentNode?.internals.positionAbsolute.y).to.equal(expectedParentY);
const expectedChildX = userChildNode.position.x - userChildNode.measured!.width! * nodeOrigin[0] + expectedParentX;
const expectedChildY = userChildNode.position.y - userChildNode.measured!.height! * nodeOrigin[1] + expectedParentY;
expect(internalChildNode?.internals.positionAbsolute.x).to.equal(expectedChildX);
expect(internalChildNode?.internals.positionAbsolute.y).to.equal(expectedChildY);
});
it('calculates positionAbsolute for sub flow with nodeExtent', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodeExtent: CoordinateExtent = [
[0, 0],
[200, 200],
];
const userParentNodeA: Node = {
id: 'a',
data: { label: 'node' },
position: { x: 500, y: 500 },
measured: { width: 100, height: 50 },
};
const userParentNodeB: Node = {
id: 'b',
data: { label: 'node' },
position: { x: 0, y: 0 },
measured: { width: 100, height: 50 },
};
const userChildNode: Node = {
id: 'c',
data: { label: 'child' },
position: { x: 1000, y: 1000 },
measured: { width: 100, height: 50 },
parentId: 'a',
};
adoptUserNodes([userParentNodeA, userParentNodeB, userChildNode], nodeLookup, parentLookup, {
nodeExtent,
});
const internalParentNodeA = nodeLookup.get('a');
const internalParentNodeB = nodeLookup.get('b');
const internalChildNode = nodeLookup.get('c');
const expectedParentX = nodeExtent[1][0] - userParentNodeA.measured!.width!;
const expectedParentY = nodeExtent[1][1] - userParentNodeA.measured!.height!;
// this node is inside the nodeExtent and should be restricted to it
expect(internalParentNodeA?.internals.positionAbsolute.x).to.equal(expectedParentX);
expect(internalParentNodeA?.internals.positionAbsolute.y).to.equal(expectedParentY);
// this node is inside the nodeExtent and should not be affected by it
expect(internalParentNodeB?.internals.positionAbsolute.x).to.equal(userParentNodeB.position.x);
expect(internalParentNodeB?.internals.positionAbsolute.y).to.equal(userParentNodeB.position.y);
const expectedChildX = nodeExtent[1][0] - userChildNode.measured!.width!;
const expectedChildY = nodeExtent[1][1] - userChildNode.measured!.height!;
expect(internalChildNode?.internals.positionAbsolute.x).to.equal(expectedChildX);
expect(internalChildNode?.internals.positionAbsolute.y).to.equal(expectedChildY);
});
});
export {};
```
## /examples/react/cypress/components/utils/apply-changes.cy.ts
```ts path="/examples/react/cypress/components/utils/apply-changes.cy.ts"
import {
applyNodeChanges,
applyEdgeChanges,
type Node,
type Edge,
NodeChange,
EdgeChange,
XYPosition,
} from '@xyflow/react';
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const edges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-3', source: '2', target: '3' },
];
describe('applyChanges Testing', () => {
it('adds a node/edge', () => {
const currentNodes: Node[] = [];
const nodeChanges: NodeChange[] = [{ type: 'add', item: nodes[0] }];
const nextNodes = applyNodeChanges(nodeChanges, currentNodes);
expect(nextNodes.length).to.be.equal(nodeChanges.length);
const currentEdges: Edge[] = [];
const edgeChanges: EdgeChange[] = [{ type: 'add', item: edges[0] }];
const nextEdges = applyEdgeChanges(edgeChanges, currentEdges);
expect(nextEdges.length).to.be.equal(1);
});
it('adds multiple nodes/edges', () => {
const currentNodes: Node[] = [];
const nodeChanges: NodeChange[] = [
{ type: 'add', item: nodes[0] },
{ type: 'add', item: nodes[1] },
];
const nextNodes = applyNodeChanges(nodeChanges, currentNodes);
expect(nextNodes.length).to.be.equal(nodeChanges.length);
const currentEdges: Edge[] = [];
const edgeChanges: EdgeChange[] = [
{ type: 'add', item: edges[0] },
{ type: 'add', item: edges[1] },
];
const nextEdges = applyEdgeChanges(edgeChanges, currentEdges);
expect(nextEdges.length).to.be.equal(edgeChanges.length);
});
it('removes a node/edge', () => {
const nodesLength = nodes.length;
const nodeChanges: NodeChange[] = [{ type: 'remove', id: '1' }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes.length).to.be.equal(nodes.length - nodeChanges.length);
expect(nodes.length).to.be.equal(nodesLength);
const edgesLength = edges.length;
const edgeChanges: EdgeChange[] = [{ type: 'remove', id: 'e1-2' }];
const nextEdges = applyEdgeChanges(edgeChanges, edges);
expect(nextEdges.length).to.be.equal(edges.length - 1);
expect(edges.length).to.be.equal(edgesLength);
});
it('removes multiple node/edge', () => {
const nodesLength = nodes.length;
const nodeChanges: NodeChange[] = [
{ type: 'remove', id: '1' },
{ type: 'remove', id: '2' },
];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes.length).to.be.equal(nodes.length - nodeChanges.length);
expect(nodes.length).to.be.equal(nodesLength);
const edgesLength = edges.length;
const edgeChanges: EdgeChange[] = [
{ type: 'remove', id: 'e1-2' },
{ type: 'remove', id: 'e1-3' },
];
const nextEdges = applyEdgeChanges(edgeChanges, edges);
expect(nextEdges.length).to.be.equal(edges.length - edgeChanges.length);
expect(edges.length).to.be.equal(edgesLength);
});
it('selects a node/edge', () => {
const nodeChanges: NodeChange[] = [{ type: 'select', id: '1', selected: true }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].selected).to.be.true;
const edgeChanges: EdgeChange[] = [{ type: 'select', id: 'e1-2', selected: true }];
const nextEdges = applyEdgeChanges(edgeChanges, edges);
expect(nextEdges[0].selected).to.be.true;
});
it('deselects a node/edge', () => {
const nodeChanges: NodeChange[] = [{ type: 'select', id: '1', selected: false }];
const nextNodes = applyNodeChanges(
nodeChanges,
nodes.map((n) => ({ ...n, selected: true }))
);
expect(nextNodes[0].selected).to.be.false;
expect(nextNodes[1].selected).to.be.true;
const edgeChanges: EdgeChange[] = [{ type: 'select', id: 'e1-2', selected: false }];
const nextEdges = applyEdgeChanges(
edgeChanges,
edges.map((e) => ({ ...e, selected: true }))
);
expect(nextEdges[0].selected).to.be.false;
expect(nextEdges[1].selected).to.be.true;
});
it('updates node position', () => {
const newPosition: XYPosition = { x: 100, y: 100 };
const nodeChanges: NodeChange[] = [{ type: 'position', id: '1', position: newPosition }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
});
it('updates node dimensions', () => {
const newWidth = 200;
const newHeight = 200;
const nodeChanges: NodeChange[] = [
{ type: 'dimensions', id: '1', dimensions: { width: newWidth, height: newHeight } },
];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nodes[0].measured).to.be.undefined;
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
expect(nextNodes[0].width).to.be.undefined;
expect(nextNodes[0].height).to.be.undefined;
});
it('updates node position and dimensions', () => {
const newPosition: XYPosition = { x: 100, y: 100 };
const newWidth = 200;
const newHeight = 200;
const nodeChanges: NodeChange[] = [
{ type: 'position', id: '1', position: newPosition },
{ type: 'dimensions', id: '1', dimensions: { width: newWidth, height: newHeight } },
];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
});
it('replaces nodes/edges', () => {
const nodesLength = nodes.length;
const nodeChanges: NodeChange[] = [{ type: 'replace', id: nodes[0].id, item: nodes[1] }];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nodes.length).length.to.be.equal(nodesLength);
expect(nextNodes.length).to.be.equal(nodesLength);
expect(nextNodes[0]).to.be.deep.equal(nodes[1]);
const edgesLength = edges.length;
const edgeChange: EdgeChange[] = [{ type: 'replace', id: edges[0].id, item: edges[1] }];
const nextEdges = applyEdgeChanges(edgeChange, edges);
expect(edges.length).length.to.be.equal(edgesLength);
expect(nextEdges.length).to.be.equal(edgesLength);
expect(nextEdges[0]).to.be.deep.equal(edges[1]);
});
});
export {};
```
## /examples/react/cypress/components/utils/calculate-node-position.cy.ts
```ts path="/examples/react/cypress/components/utils/calculate-node-position.cy.ts"
import { adoptUserNodes, calculateNodePosition, type NodeLookup, type ParentLookup } from '@xyflow/system';
import type { Node } from '@xyflow/react';
describe('calculateNodePosition', () => {
it("clamps a child with extent 'parent' to a parent that has an intrinsic size but is not measured yet", () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodes: Node[] = [
{
id: 'parent',
data: { label: 'parent' },
position: { x: 0, y: 0 },
// intrinsic size, but no measured dimensions yet (e.g. before measuring / SSR)
initialWidth: 200,
initialHeight: 100,
},
{
id: 'child',
data: { label: 'child' },
position: { x: 0, y: 0 },
measured: { width: 50, height: 50 },
parentId: 'parent',
extent: 'parent',
},
];
adoptUserNodes(nodes, nodeLookup, parentLookup);
const { positionAbsolute } = calculateNodePosition({
nodeId: 'child',
nextPosition: { x: 500, y: 500 },
nodeLookup,
});
// The child must stay inside the parent: max x = 200 - 50, max y = 100 - 50.
// Before the fix the parent's unmeasured size was ignored and the child
// escaped to { x: 500, y: 500 }.
expect(positionAbsolute.x).to.equal(150);
expect(positionAbsolute.y).to.equal(50);
});
});
export {};
```
## /examples/react/cypress/components/utils/get-nodes-bounds.cy.ts
```ts path="/examples/react/cypress/components/utils/get-nodes-bounds.cy.ts"
import { adoptUserNodes, getNodesBounds, type NodeLookup, type ParentLookup } from '@xyflow/system';
import type { Node } from '@xyflow/react';
describe('getNodesBounds Testing', () => {
it('calculates bounds for a node', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodes: Node[] = [
{
id: '1',
data: { label: 'node' },
position: { x: 10, y: 10 },
measured: { width: 100, height: 50 },
},
];
adoptUserNodes(nodes, nodeLookup, parentLookup);
const bounds = getNodesBounds(nodes, { nodeLookup });
expect(bounds.x).to.equal(10);
expect(bounds.y).to.equal(10);
expect(bounds.width).to.equal(100);
expect(bounds.height).to.equal(50);
});
it('calculates bounds for a node (-x, -y)', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodes: Node[] = [
{
id: '1',
data: { label: 'node' },
position: { x: -10, y: -10 },
measured: { width: 100, height: 50 },
},
];
adoptUserNodes(nodes, nodeLookup, parentLookup);
const bounds = getNodesBounds(nodes, { nodeLookup });
expect(bounds.x).to.equal(-10);
expect(bounds.y).to.equal(-10);
expect(bounds.width).to.equal(100);
expect(bounds.height).to.equal(50);
});
it('calculates bounds for multitple nodes', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodes: Node[] = [
{
id: '1',
data: { label: 'node' },
position: { x: -10, y: 10 },
measured: { width: 100, height: 50 },
},
{
id: '2',
data: { label: 'node' },
position: { x: 100, y: 100 },
measured: { width: 100, height: 50 },
},
];
adoptUserNodes(nodes, nodeLookup, parentLookup);
const bounds = getNodesBounds(nodes, { nodeLookup });
expect(bounds.x).to.equal(-10);
expect(bounds.y).to.equal(10);
expect(bounds.width).to.equal(210);
expect(bounds.height).to.equal(140);
});
it('calculates bounds for a node with a child node', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodes: Node[] = [
{
id: '1',
data: { label: 'node' },
position: { x: 10, y: 10 },
measured: { width: 100, height: 50 },
},
{
id: '2',
data: { label: 'node' },
position: { x: 200, y: 100 },
measured: { width: 100, height: 50 },
parentId: '1',
},
{
id: '3',
data: { label: 'node' },
position: { x: 0, y: 200 },
measured: { width: 100, height: 50 },
parentId: '1',
},
];
adoptUserNodes(nodes, nodeLookup, parentLookup);
const bounds = getNodesBounds(nodes, { nodeLookup });
expect(bounds.x).to.equal(10);
expect(bounds.y).to.equal(10);
expect(bounds.width).to.equal(300);
expect(bounds.height).to.equal(250);
});
it('calculates bounds for child nodes', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodes: Node[] = [
{
id: '1',
data: { label: 'node' },
position: { x: 10, y: 10 },
measured: { width: 100, height: 50 },
},
{
id: '2',
data: { label: 'node' },
position: { x: 20, y: 20 },
measured: { width: 100, height: 50 },
parentId: '1',
},
{
id: '3',
data: { label: 'node' },
position: { x: 20, y: 200 },
measured: { width: 100, height: 50 },
parentId: '1',
},
];
adoptUserNodes(nodes, nodeLookup, parentLookup);
const bounds = getNodesBounds([nodes[1], nodes[2]], { nodeLookup });
expect(bounds.x).to.equal(30);
expect(bounds.y).to.equal(30);
expect(bounds.width).to.equal(100);
expect(bounds.height).to.equal(230);
});
it('ignores unresolvable ids instead of stretching the bounds to the origin', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
const nodes: Node[] = [
{
id: '1',
data: { label: 'node' },
position: { x: 100, y: 100 },
measured: { width: 100, height: 50 },
},
];
adoptUserNodes(nodes, nodeLookup, parentLookup);
// '2' is not in the lookup (e.g. a stale id after that node was removed)
const bounds = getNodesBounds(['1', '2'], { nodeLookup });
// bounds should match node '1' only, not be stretched to include (0, 0)
expect(bounds.x).to.equal(100);
expect(bounds.y).to.equal(100);
expect(bounds.width).to.equal(100);
expect(bounds.height).to.equal(50);
});
});
export {};
```
## /examples/react/cypress/components/utils/graph-utils.cy.ts
```ts path="/examples/react/cypress/components/utils/graph-utils.cy.ts"
import { Node, Edge, isNode, isEdge, getOutgoers, getIncomers, addEdge } from '@xyflow/react';
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const edges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-3', source: '2', target: '3' },
];
describe('Graph Utils Testing', () => {
it('tests isNode function', () => {
expect(isNode(nodes[0])).to.be.true;
expect(isNode(edges[0])).to.be.false;
});
it('tests isEdge function', () => {
expect(isEdge(edges[0])).to.be.true;
expect(isEdge(nodes[0])).to.be.false;
});
it('tests getOutgoers function', () => {
const outgoers = getOutgoers(nodes[0], nodes, edges);
expect(outgoers.length).to.be.equal(2);
const noOutgoers = getOutgoers(nodes[2], nodes, edges);
expect(noOutgoers.length).to.be.equal(0);
});
it('tests getIncomers function', () => {
const incomers = getIncomers(nodes[2], nodes, edges);
expect(incomers.length).to.be.equal(2);
const noIncomers = getIncomers(nodes[0], nodes, edges);
expect(noIncomers.length).to.be.equal(0);
});
describe('tests addEdge function', () => {
it('adds edge', () => {
const newEdge: Edge = { source: '1', target: '4', id: 'new-edge-1-4' };
const nextEdges = addEdge(newEdge, edges);
expect(nextEdges.length).to.be.equal(edges.length + 1);
});
it('tries to add existing edge', () => {
const newEdge: Edge = { source: '2', target: '3', id: 'new-edge-2-3' };
const nextEdges = addEdge(newEdge, edges);
expect(nextEdges.length).to.be.equal(edges.length);
});
it('tries to add invalid edge', () => {
// @ts-ignore
const newEdge: Edge = { nosource: '1', notarget: '3' };
try {
addEdge(newEdge, edges);
} catch (e: any) {
console.log(e.message);
expect(e.message).to.be.equal("Can't create edge. An edge needs a source and a target.");
}
});
});
});
export {};
```
## /examples/react/cypress/components/utils/update-node-internals.cy.ts
```ts path="/examples/react/cypress/components/utils/update-node-internals.cy.ts"
import {
updateNodeInternals,
adoptUserNodes,
type InternalNodeUpdate,
type NodeLookup,
type ParentLookup,
} from '@xyflow/system';
import type { Node } from '@xyflow/react';
// Regression test for https://github.com/xyflow/xyflow/issues/5835
// When a node references a `parentId` that is absent from `nodeLookup` (for
// example the parent was removed while the child's ResizeObserver still fires),
// `updateNodeInternals` used to call `clampPositionToParent(..., nodeLookup.get(parentId)!)`
// with an `undefined` parent and crash with
// `TypeError: Cannot read properties of undefined (reading 'measured')`.
describe('updateNodeInternals with a missing parent', () => {
it('does not crash when a node parent is absent from nodeLookup', () => {
const nodeLookup: NodeLookup = new Map();
const parentLookup: ParentLookup = new Map();
// A child whose parent ("missing-parent") is never added to the lookup.
const child: Node = {
id: 'child',
data: { label: 'child' },
position: { x: 0, y: 0 },
parentId: 'missing-parent',
extent: 'parent',
measured: { width: 50, height: 50 },
};
adoptUserNodes([child], nodeLookup, parentLookup);
// Minimal flow DOM so `updateNodeInternals` does not bail out early on the
// missing `.xyflow__viewport` and reads a real zoom from the transform.
const domNode = document.createElement('div');
const viewport = document.createElement('div');
viewport.className = 'xyflow__viewport';
viewport.style.transform = 'translate(0px, 0px) scale(1)';
domNode.appendChild(viewport);
document.body.appendChild(domNode);
const nodeElement = document.createElement('div');
nodeElement.style.width = '50px';
nodeElement.style.height = '50px';
document.body.appendChild(nodeElement);
const updates = new Map<string, InternalNodeUpdate>([
['child', { id: 'child', nodeElement, force: true }],
]);
expect(() =>
updateNodeInternals(updates, nodeLookup, parentLookup, domNode, [0, 0])
).to.not.throw();
domNode.remove();
nodeElement.remove();
});
});
```
## /examples/react/cypress/e2e/basic.cy.ts
```ts path="/examples/react/cypress/e2e/basic.cy.ts"
describe('Basic Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/');
});
it('renders a flow with three nodes', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow-basic-example'); // check if className prop works
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 2);
cy.get('.react-flow__node').children('.react-flow__handle');
});
it('renders a grid', () => {
cy.get('.react-flow__background');
});
it('selects two nodes by clicks', () => {
cy.get('body').type('{cmd}', { release: false });
cy.get('.react-flow__node:first')
.click()
.should('have.class', 'selected')
.get('.react-flow__node:last')
.click()
.should('have.class', 'selected')
.get('.react-flow__node:first')
.should('have.class', 'selected');
cy.get('body').type('{cmd}', { release: true });
});
it('selects a node by click', () => {
cy.get('.react-flow__node:first').as('node').click({ force: true }).should('have.class', 'selected');
});
it('deselects node', () => {
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
});
it('selects an edge by click', () => {
cy.get('.react-flow__edge:first').as('edge').click({ force: true });
cy.get('.react-flow__edge:first').should('have.class', 'selected');
});
it('deselects edge', () => {
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__edge:first').should('not.have.class', 'selected');
});
it('selects one node with a selection', () => {
cy.get('body')
.type('{Shift}', { release: false })
.get('.react-flow__pane')
.trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 200, { button: 0 })
.trigger('mouseup', 1000, 200, { button: 0 });
cy.get('.react-flow__node').eq(0).should('have.class', 'selected');
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true });
});
it('selects all nodes', () => {
cy.get('body')
.type('{shift}', { release: false })
.get('.react-flow__pane')
.trigger('mousedown', 'topRight', { button: 0, force: true })
.trigger('mousemove', 'bottomLeft', { button: 0 })
.wait(50)
.trigger('mouseup', 'bottomLeft', { button: 0, force: true })
.wait(400)
.get('.react-flow__node')
.should('have.class', 'selected');
cy.wait(200);
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true });
});
it('removes selection', () => {
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__nodesselection-rect').should('not.exist');
});
it('selects an edge', () => {
cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected');
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 10, y: 10 }).then(($el: any) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('removes a node', () => {
cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
cy.get('html').realPress('Backspace');
cy.get('.react-flow__node').should('have.length', 3);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('connects nodes', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.trigger('mousedown', { force: true, button: 0 });
cy.get('.react-flow__node')
.contains('Node 4')
.find('.react-flow__handle.target')
.trigger('mousemove', { force: true, button: 0 })
.wait(200)
.trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').as('edge');
cy.get('@edge').should('have.length', 1);
});
it('removes an edge', () => {
cy.get('.react-flow__edge:first').click();
cy.get('html').realPress('Backspace');
cy.get('.react-flow__edge').should('have.length', 0);
});
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 'bottomLeft')
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
it('zooms the pane', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.wait(50)
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
});
export {};
```
## /examples/react/cypress/e2e/controls.cy.ts
```ts path="/examples/react/cypress/e2e/controls.cy.ts"
describe('Controls Testing', { testIsolation: false }, () => {
before(() => {
cy.visit('/');
});
it('renders the control panel', () => {
cy.get('.react-flow__controls');
});
it('zooms in', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-zoomin')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
it('zooms out', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-zoomout')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
// view is already fitted so we drag the pane to un-fit it
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__renderer')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 10, 400)
.wait(50)
.trigger('mouseup', 10, 400, { force: true, view: win })
.then(() => {
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
it('fits view', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-fitview')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
it('uses interactive control - not interactive', () => {
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
cy.get('.react-flow__pane').click('topLeft');
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
cy.get('.react-flow__controls-interactive')
.click()
.then(() => {
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
});
});
it('uses interactive control - interactive', () => {
cy.get('.react-flow__controls-interactive')
.click()
.then(() => {
cy.get('.react-flow__node:first').click({ force: true }).should('have.class', 'selected');
});
});
});
export {};
```
## /examples/react/cypress/e2e/draghandle.cy.ts
```ts path="/examples/react/cypress/e2e/draghandle.cy.ts"
describe('DragHandle Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/DragHandle');
});
it('renders a flow with a node', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 1);
});
it('tries to drag a node', () => {
const $nodeElement = Cypress.$('.react-flow__node:first');
const styleBeforeDrag = $nodeElement.css('transform');
cy.drag('.react-flow__node:first', { x: 500, y: 500 }).then(() => {
const styleAfterDrag = $nodeElement.css('transform');
expect(styleBeforeDrag).to.be.equal(styleAfterDrag);
});
});
it('drags a node', () => {
const $nodeElement = Cypress.$('.react-flow__node:first');
const styleBeforeDrag = $nodeElement.css('transform');
cy.drag('.custom-drag-handle:first', { x: 500, y: 500 }).then(() => {
const styleAfterDrag = $nodeElement.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
export {};
```
## /examples/react/cypress/e2e/empty.cy.ts
```ts path="/examples/react/cypress/e2e/empty.cy.ts"
describe('Empty Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/Empty');
});
it('renders an empty flow', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('not.exist');
cy.get('.react-flow__edge').should('not.exist');
});
it('renders empty selection', () => {
cy.get('.react-flow__renderer').click();
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.get('.react-flow__pane')
.trigger('mousedown', 400, 50, { button: 0, force: true })
.trigger('mousemove', 200, 200, { button: 0 })
.wait(50)
.trigger('mouseup', 200, 200, { force: true });
cy.get('body').type('{shift}', { release: true });
});
it('adds two nodes', () => {
cy.contains('add node').click();
cy.contains('add node').click();
});
it('connects nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 1);
});
});
export {};
```
## /examples/react/cypress/e2e/figma.cy.ts
```ts path="/examples/react/cypress/e2e/figma.cy.ts"
describe('Figma Flow UI', { testIsolation: false }, () => {
before(() => {
cy.visit('/figma');
});
it('renders a flow with three nodes', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 2);
cy.get('.react-flow__node').children('.react-flow__handle');
});
it('renders a grid', () => {
cy.get('.react-flow__background');
});
it('selects all nodes by drag', () => {
cy.window().then((win) => {
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 'bottomRight', { force: true })
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
cy.get('.react-flow__node').should('have.class', 'selected');
});
});
});
it('removes selection', () => {
cy.get('.react-flow__pane').click('topLeft');
cy.get('.react-flow__node').should('not.have.class', 'selected');
});
it('drags using right click', () => {
cy.window().then((win) => {
cy.get('.react-flow__node:last').isWithinViewport();
cy.get('.react-flow__pane')
.trigger('mousedown', 'center', { button: 2, view: win })
.trigger('mousemove', 'bottom', { force: true })
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
cy.get('.react-flow__node').should('not.have.class', 'selected');
cy.get('.react-flow__node:last').isOutsideViewport();
});
});
});
});
export {};
```
## /examples/react/cypress/e2e/hidden.cy.ts
```ts path="/examples/react/cypress/e2e/hidden.cy.ts"
describe('Hidden Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/Hidden');
});
it('renders empty flow', () => {
cy.get('.react-flow__node').should('not.exist');
cy.get('.react-flow__edge').should('not.exist');
cy.get('.react-flow__minimap-node').should('not.exist');
});
it('toggles isHidden mode', () => {
cy.get('.react-flow__ishidden').click();
});
it('renders initial flow', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 3);
cy.get('.react-flow__minimap-node').should('have.length', 4);
});
it('toggles isHidden mode again', () => {
cy.get('.react-flow__ishidden').click();
});
it('renders empty flow', () => {
cy.get('.react-flow__node').should('not.exist');
cy.get('.react-flow__edge').should('not.exist');
cy.get('.react-flow__minimap-node').should('not.exist');
});
});
export {};
```
## /examples/react/cypress/e2e/interaction.cy.ts
```ts path="/examples/react/cypress/e2e/interaction.cy.ts"
describe('Interaction Flow Rendering', { testIsolation: false }, () => {
before(() => {
cy.visit('/Interaction');
});
it('renders initial flow', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 2);
cy.get('.react-flow__node').children('.react-flow__handle');
});
it('tries to select a node by click', () => {
const pointerEvents = Cypress.$('.react-flow__node:first').css('pointer-events');
expect(pointerEvents).to.equal('none');
});
it('tries to select an edge by click', () => {
const pointerEvents = Cypress.$('.react-flow__edge:first').css('pointer-events');
expect(pointerEvents).to.equal('none');
});
it('toggles on capture element click', () => {
cy.get('.react-flow__captureelementclick').click();
});
it('allows node clicks when enabled', () => {
const pointerEvents = Cypress.$('.react-flow__node:first').css('pointer-events');
expect(pointerEvents).to.equal('all');
});
it('allows edge clicks when enabled', () => {
const pointerEvents = Cypress.$('.react-flow__edge:first').css('pointer-events');
expect(pointerEvents.toLowerCase()).to.equal('visiblestroke');
});
it('tries to do a selection', () => {
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.get('.react-flow__pane')
.trigger('mousedown', 1000, 50, { button: 0, force: true })
.trigger('mousemove', 1, 400, { button: 0 })
.wait(50)
.get('.react-flow__selection')
.should('not.exist');
cy.get('.react-flow__pane').trigger('mouseup', 1, 200, { force: true });
cy.get('body').type('{shift}', { release: true });
});
it('tries to connect to nodes', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.then(($el) => {
const pointerEvents = $el.css('pointer-events');
expect(pointerEvents).to.equal('none');
});
});
it('tries to zoom by double click', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__renderer')
.dblclick()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
expect(styleBeforeZoom).to.equal(styleAfterZoom);
});
});
it('tries to zoom by scroll', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__renderer')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
expect(styleBeforeZoom).to.equal(styleAfterZoom);
});
});
it('toggles draggable mode', () => {
cy.get('.react-flow__draggable').click();
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 325, y: 100 }).then(($el) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('toggles selectable mode', () => {
cy.get('.react-flow__selectable').click();
});
it('selects a node by click', () => {
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
});
it('selects an edge by click', () => {
cy.get('.react-flow__edge:first').click({ force: true });
cy.get('.react-flow__edge:first').should('have.class', 'selected');
});
it('toggles connectable mode', () => {
cy.get('.react-flow__connectable').click();
});
it('connects two nodes', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.contains('Node 4')
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 3);
});
it('toggles zoom on scroll', () => {
cy.get('.react-flow__zoomonscroll').click();
});
it('zooms by scroll', () => {
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: 200 })
.wait(50)
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
});
});
it('toggles zoom on double click', () => {
cy.get('.react-flow__zoomondbl').click();
});
it('zooms by double click', () => {
cy.get('.react-flow__controls-zoomout').click();
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__pane')
.dblclick()
.wait(50)
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
});
});
});
export {};
```
## /examples/react/cypress/e2e/minimap.cy.ts
```ts path="/examples/react/cypress/e2e/minimap.cy.ts"
describe('Minimap Testing', { testIsolation: false }, () => {
before(() => {
cy.visit('/');
});
it('renders the mini map', () => {
cy.get('.react-flow__minimap');
cy.get('.react-flow__minimap-mask');
});
it('has same number of nodes as the pane', () => {
cy.get('.react-flow__minimap-node').then(() => {
const paneNodes = Cypress.$('.react-flow__node').length;
const minimapNodes = Cypress.$('.react-flow__minimap-node').length;
expect(paneNodes).equal(minimapNodes);
});
});
it('changes zoom level', () => {
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.wait(50)
.then(() => {
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathAfterZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
expect(viewBoxBeforeZoom).to.not.equal(viewBoxAfterZoom);
expect(maskPathBeforeZoom).to.not.equal(maskPathAfterZoom);
});
});
it('changes node position', () => {
const minimapNode = Cypress.$('.react-flow__minimap-node:first');
const xPosBeforeDrag = Number(minimapNode.attr('x'));
const yPosBeforeDrag = Number(minimapNode.attr('y'));
cy.drag('.react-flow__node:first', { x: 500, y: 25 })
.wait(100)
.then(() => {
const xPosAfterDrag = Number(minimapNode.attr('x'));
const yPosAfterDrag = Number(minimapNode.attr('y'));
expect(xPosAfterDrag).not.to.equal(xPosBeforeDrag);
expect(yPosAfterDrag).not.to.equal(yPosBeforeDrag);
expect(xPosAfterDrag - xPosBeforeDrag).to.be.greaterThan(yPosAfterDrag - yPosBeforeDrag);
});
});
it('changes node positions via pane drag', () => {
const viewBoxBeforeDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { button: 0, view: win })
.trigger('mousemove', 'bottomLeft')
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
const viewBoxAfterDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
expect(viewBoxBeforeDrag).to.not.equal(viewBoxAfterDrag);
expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
});
});
});
});
export {};
```
## /examples/react/cypress/fixtures/simpleflow.ts
```ts path="/examples/react/cypress/fixtures/simpleflow.ts"
import { Node, Edge } from '@xyflow/react';
export const nodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 200, y: 200 },
},
];
export const edges: Edge[] = [
{
id: 'e1',
source: '1',
target: '2',
},
];
```
## /examples/react/cypress/support/ControlledFlow.tsx
```tsx path="/examples/react/cypress/support/ControlledFlow.tsx"
import { useCallback, useState } from 'react';
import {
ReactFlow,
Node,
Edge,
NodeChange,
EdgeChange,
applyNodeChanges,
applyEdgeChanges,
Connection,
addEdge,
ReactFlowProps,
} from '@xyflow/react';
function ControlledFlow({
addOnNodeChangeHandler = true,
addOnEdgeChangeHandler = true,
addOnConnectHandler = true,
initialNodes = [],
initialEdges = [],
...rest
}: {
initialNodes?: Node[];
initialEdges?: Edge[];
addOnNodeChangeHandler?: boolean;
addOnEdgeChangeHandler?: boolean;
addOnConnectHandler?: boolean;
} & Partial<ReactFlowProps>) {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onNodesChange = useCallback(
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const handlers: {
onNodesChange?: (changes: NodeChange[]) => void;
onEdgesChange?: (changes: EdgeChange[]) => void;
onConnect?: (params: Connection | Edge) => void;
} = {};
if (addOnNodeChangeHandler) {
handlers.onNodesChange = onNodesChange;
}
if (addOnEdgeChangeHandler) {
handlers.onEdgesChange = onEdgesChange;
}
if (addOnConnectHandler) {
handlers.onConnect = onConnect;
}
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} nodeDragThreshold={0} />;
}
export default ControlledFlow;
```
## /examples/react/cypress/support/commands.ts
```ts path="/examples/react/cypress/support/commands.ts"
Cypress.Commands.add('drag', (selector, { x, y }) =>
cy.window().then((window) => {
const elementToDrag = cy.get(selector as string);
return elementToDrag.then(($el) => {
const { left, top, width, height } = $el[0].getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;
const nextX: number = centerX + x;
const nextY: number = centerY + y;
return elementToDrag
.trigger('mousedown', { view: window, force: true })
.trigger('mousemove', nextX, nextY, { force: true })
.wait(50)
.trigger('mouseup', { view: window, force: true });
});
})
);
Cypress.Commands.add('dragPane', ({ from, to }) =>
cy
.window()
.then((window) =>
cy
.get('.react-flow__pane')
.trigger('mousedown', from.x, from.y, { view: window })
.trigger('mousemove', to.x, to.y)
.trigger('mouseup', { force: true, view: window })
)
);
Cypress.Commands.add('zoomPane', (wheelDelta: number) =>
cy.get('.react-flow__pane').trigger('wheel', 'center', { deltaY: wheelDelta }).wait(250)
);
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
return cy.window().then((window) => {
expect(rect.top).to.be.within(0, window.innerHeight);
expect(rect.right).to.be.within(0, window.innerWidth);
expect(rect.bottom).to.be.within(0, window.innerHeight);
expect(rect.left).to.be.within(0, window.innerWidth);
return subject;
});
});
Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
return cy.window().then((window) => {
expect(window.innerHeight < rect.top || rect.bottom < 0 || window.innerWidth < rect.left || rect.right < 0).to.be
.true;
return subject;
});
});
export {};
```
## /examples/react/cypress/support/component-index.html
```html path="/examples/react/cypress/support/component-index.html"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Components App</title>
<style>
html,
body,
#root {
margin: 0;
height: 100%;
}
</style>
</head>
<body>
<div data-cy-root id="root"></div>
</body>
</html>
```
## /examples/react/cypress/support/component.ts
```ts path="/examples/react/cypress/support/component.ts"
///<reference types="cypress" />
///<reference types="@cypress/skip-test" />
///<reference types="cypress-real-events" />
import './commands';
import 'cypress-real-events/support';
import { mount } from 'cypress/react18';
import { XYPosition } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount;
drag: (selector: string, toPosition: XYPosition) => Cypress.Chainable<JQuery<HTMLElement>>;
dragPane: ({ from, to }: { from: XYPosition; to: XYPosition }) => Cypress.Chainable<JQuery<HTMLElement>>;
zoomPane: (wheelDelta: number) => Cypress.Chainable<JQuery<HTMLElement>>;
isWithinViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
isOutsideViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add('mount', mount);
```
## /examples/react/cypress/support/e2e.ts
```ts path="/examples/react/cypress/support/e2e.ts"
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import 'cypress-real-events/support';
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
Cypress.on('uncaught:exception', (err) => {
if (resizeObserverLoopErrRe.test(err.message)) {
return false;
}
});
```
## /examples/react/index.html
```html path="/examples/react/index.html"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Flow Examples</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
## /examples/react/package.json
```json path="/examples/react/package.json"
{
"name": "react-examples",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3000 --open --host",
"serve": "vite serve --port 3000",
"build": "vite build",
"preview": "vite preview",
"test:dev": "cypress open",
"test": "pnpm test-component && pnpm test-e2e",
"test-component": "cypress run --component",
"test-e2e-cypress": "cypress run --e2e --headless",
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.3",
"@xyflow/react": "workspace:*",
"@xyflow/system": "workspace:*",
"classcat": "^5.0.4",
"dagre": "^0.8.5",
"localforage": "^1.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.18.0",
"redux": "^5.0.1",
"zustand": "^4.4.6"
},
"devDependencies": {
"@cypress/skip-test": "^2.6.1",
"@types/dagre": "^0.7.52",
"@types/react": "^18.2.36",
"@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "6.0.2",
"cypress": "13.6.6",
"cypress-real-events": "1.12.0",
"start-server-and-test": "^2.0.2",
"typescript": "5.4.5",
"vite": "^8.0.12"
}
}
```
## /examples/react/public/favicon.ico
Binary file available at https://raw.githubusercontent.com/xyflow/xyflow/refs/heads/main/examples/react/public/favicon.ico
## /examples/react/src/App/header.tsx
```tsx path="/examples/react/src/App/header.tsx"
import { useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import routes from './routes';
export default function Header() {
const location = useLocation();
const navigate = useNavigate();
const pathParts = location.pathname.split('/').filter(Boolean);
const initialExample = pathParts.length > 1 ? pathParts[1] : 'basic';
const [currentPath, setCurrentPath] = useState(initialExample);
useEffect(() => {
const name = routes.find((route) => route.path === currentPath)?.name;
document.title = `React Flow Examples${name ? ' - ' + name : ''}`;
navigate(currentPath);
}, [currentPath, navigate]);
return (
<>
<header>
<a className="logo" href="https://github.com/xyflow/xyflow">
React Flow Dev
</a>
<select
value={currentPath}
onChange={(event) => setCurrentPath(event.target.value)}
aria-label="select an example"
>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.name}
</option>
))}
</select>
</header>
<Outlet />
</>
);
}
```
## /examples/react/src/App/index.tsx
```tsx path="/examples/react/src/App/index.tsx"
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import routes from './routes';
import GenericTestRoutes from '../generic-tests';
import Basic from '../examples/Basic';
import Header from './header';
export default () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/examples" />} />
<Route path="examples" element={<Header />}>
<Route index element={<Basic />} />
{routes.map((route) => (
<Route path={route.path} key={route.path} element={<route.component />} />
))}
</Route>
<Route path="tests/generic/*" element={<GenericTestRoutes />} />
</Routes>
</BrowserRouter>
);
```
## /examples/react/src/App/routes.ts
```ts path="/examples/react/src/App/routes.ts"
import A11y from '../examples/A11y';
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import BrokenNodes from '../examples/BrokenNodes';
import ColorMode from '../examples/ColorMode';
import ClickDistance from '../examples/ClickDistance';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import ControlledViewport from '../examples/ControlledViewport';
import CustomConnectionLine from '../examples/CustomConnectionLine';
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
import CustomNode from '../examples/CustomNode';
import DefaultEdgeOverwrite from '../examples/DefaultEdgeOverwrite';
import DefaultNodeOverwrite from '../examples/DefaultNodeOverwrite';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
import DragNDrop from '../examples/DragNDrop';
import EasyConnect from '../examples/EasyConnect';
import Edges from '../examples/Edges';
import EdgeRenderer from '../examples/EdgeRenderer';
import EdgeTypes from '../examples/EdgeTypes';
import Empty from '../examples/Empty';
import Figma from '../examples/Figma';
import FloatingEdges from '../examples/FloatingEdges';
import Hidden from '../examples/Hidden';
import Interaction from '../examples/Interaction';
import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
import MultiSetNodes from '../examples/MultiSetNodes';
import NodeResizer from '../examples/NodeResizer';
import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
import Overview from '../examples/Overview';
import Provider from '../examples/Provider';
import SaveRestore from '../examples/SaveRestore';
import SetNodesBatching from '../examples/SetNodesBatching';
import Stress from '../examples/Stress';
import Subflow from '../examples/Subflow';
import SwitchFlow from '../examples/Switch';
import TouchDevice from '../examples/TouchDevice';
import Undirectional from '../examples/Undirectional';
import ReconnectEdge from '../examples/ReconnectEdge';
import UpdateNode from '../examples/UpdateNode';
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
import UseReactFlow from '../examples/UseReactFlow';
import Validation from '../examples/Validation';
import UseKeyPress from '../examples/UseKeyPress';
import EdgeRouting from '../examples/EdgeRouting';
import CancelConnection from '../examples/CancelConnection';
import InteractiveMinimap from '../examples/InteractiveMinimap';
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
import NodeToolbar from '../examples/NodeToolbar';
import EdgeToolbar from '../examples/EdgeToolbar';
import UseConnection from '../examples/UseConnection';
import UseNodesInitialized from '../examples/UseNodesInit';
import UseNodesData from '../examples/UseNodesData';
import UseNodeConnections from '../examples/UseNodeConnections';
import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
import DevTools from '../examples/DevTools';
import Redux from '../examples/Redux';
import MovingHandles from '../examples/MovingHandles';
import DetachedHandle from '../examples/DetachedHandle';
import ZIndexMode from '../examples/ZIndexMode';
import Middlewares from '../examples/Middlewares';
import NodeSelectionBug from '../examples/NodeSelectionBug';
export interface IRoute {
name: string;
path: string;
component: React.ComponentType;
}
const routes: IRoute[] = [
{
name: 'Add Node on edge Drop',
path: 'add-node-edge-drop',
component: AddNodeOnEdgeDrop,
},
{
name: 'A11y',
path: 'a11y',
component: A11y,
},
{
name: 'Basic',
path: 'basic',
component: Basic,
},
{
name: 'Backgrounds',
path: 'backgrounds',
component: Backgrounds,
},
{
name: 'Broken Nodes',
path: 'broken-nodes',
component: BrokenNodes,
},
{
name: 'Color Mode',
path: 'color-mode',
component: ColorMode,
},
{
name: 'Cancel Connection',
path: 'cancel-connection',
component: CancelConnection,
},
{
name: 'Click Distance',
path: 'click-distance',
component: ClickDistance,
},
{
name: 'Controlled/Uncontrolled',
path: 'controlled-uncontrolled',
component: ControlledUncontrolled,
},
{
name: 'Controlled Viewport',
path: 'controlled-viewport',
component: ControlledViewport,
},
{
name: 'Custom Connection Line',
path: 'custom-connectionline',
component: CustomConnectionLine,
},
{
name: 'Custom Minimap Node',
path: 'custom-minimap-node',
component: CustomMiniMapNode,
},
{
name: 'Custom Node',
path: 'custom-node',
component: CustomNode,
},
{
name: 'Default Node Overwrite',
path: 'default-node-overwrite',
component: DefaultNodeOverwrite,
},
{
name: 'Default Edge Overwrite',
path: 'default-edge-overwrite',
component: DefaultEdgeOverwrite,
},
{
name: 'Default Nodes',
path: 'default-nodes',
component: DefaultNodes,
},
{
name: 'DetachedHandle',
path: 'detached-handle',
component: DetachedHandle,
},
{
name: 'DevTools',
path: 'devtools',
component: DevTools,
},
{
name: 'Drag Handle',
path: 'draghandle',
component: DragHandle,
},
{
name: 'Drag and Drop',
path: 'dragndrop',
component: DragNDrop,
},
{
name: 'EasyConnect',
path: 'easy-connect',
component: EasyConnect,
},
{
name: 'Edges',
path: 'edges',
component: Edges,
},
{
name: 'Edge Renderer',
path: 'edge-renderer',
component: EdgeRenderer,
},
{
name: 'Edge Types',
path: 'edge-types',
component: EdgeTypes,
},
{
name: 'Edge Routing',
path: 'edge-routing',
component: EdgeRouting,
},
{
name: 'Edge Toolbar',
path: 'edge-toolbar',
component: EdgeToolbar,
},
{
name: 'Empty',
path: 'empty',
component: Empty,
},
{
name: 'Figma',
path: 'figma',
component: Figma,
},
{
name: 'Floating Edges',
path: 'floating-edges',
component: FloatingEdges,
},
{
name: 'Hidden',
path: 'hidden',
component: Hidden,
},
{
name: 'Interaction',
path: 'interaction',
component: Interaction,
},
{
name: 'Intersection',
path: 'intersection',
component: Intersection,
},
{
name: 'Interactive Minimap',
path: 'interactive-minimap',
component: InteractiveMinimap,
},
{
name: 'Layouting',
path: 'layouting',
component: Layouting,
},
{
name: 'Middlewares',
path: 'middlewares',
component: Middlewares,
},
{
name: 'Multi setNodes',
path: 'multi-setnodes',
component: MultiSetNodes,
},
{
name: 'Moving Handles',
path: 'moving-handles',
component: MovingHandles,
},
{
name: 'Multi Flows',
path: 'multiflows',
component: MultiFlows,
},
{
name: 'Node Type Change',
path: 'nodetype-change',
component: NodeTypeChange,
},
{
name: 'nodeTypes Object Change',
path: 'nodetypesobject-change',
component: NodeTypesObjectChange,
},
{
name: 'NodeToolbar',
path: 'node-toolbar',
component: NodeToolbar,
},
{
name: 'NodeResizer',
path: 'node-resizer',
component: NodeResizer,
},
{
name: 'Overview',
path: 'overview',
component: Overview,
},
{
name: 'Provider',
path: 'provider',
component: Provider,
},
{
name: 'Save/Restore',
path: 'save-restore',
component: SaveRestore,
},
{
name: 'SetNodes Batching',
path: 'setnodes-batching',
component: SetNodesBatching,
},
{
name: 'Stress',
path: 'stress',
component: Stress,
},
{
name: 'Subflow',
path: 'subflow',
component: Subflow,
},
{
name: 'Switch Flow',
path: 'switch',
component: SwitchFlow,
},
{
name: 'Touch Device',
path: 'touch-device',
component: TouchDevice,
},
{
name: 'Undirectional',
path: 'undirectional',
component: Undirectional,
},
{
name: 'Reconnect Edge',
path: 'reconnect-edge',
component: ReconnectEdge,
},
{
name: 'Update Node',
path: 'update-node',
component: UpdateNode,
},
{
name: 'useConnection',
path: 'use-connection',
component: UseConnection,
},
{
name: 'useNodesInitialized',
path: 'use-nodes-initialized',
component: UseNodesInitialized,
},
{
name: 'useOnSelectionChange',
path: 'use-on-selection-change',
component: UseOnSelectionChange,
},
{
name: 'useReactFlow',
path: 'usereactflow',
component: UseReactFlow,
},
{
name: 'useNodeConnections',
path: 'usenodeconnections',
component: UseNodeConnections,
},
{
name: 'useNodesData',
path: 'usenodesdata',
component: UseNodesData,
},
{
name: 'useUpdateNodeInternals',
path: 'useupdatenodeinternals',
component: UseUpdateNodeInternals,
},
{
name: 'redux',
path: 'redux',
component: Redux,
},
{
name: 'Validation',
path: 'validation',
component: Validation,
},
{
name: 'useKeyPress',
path: 'use-key-press',
component: UseKeyPress,
},
{
name: 'zIndexMode',
path: 'z-index-mode',
component: ZIndexMode,
},
{
name: 'Node Selection Bug',
path: 'node-selection-bug',
component: NodeSelectionBug,
},
];
export default routes;
```
## /examples/react/src/examples/A11y/index.tsx
```tsx path="/examples/react/src/examples/A11y/index.tsx"
import { useState } from 'react';
import {
ReactFlow,
MiniMap,
Background,
BackgroundVariant,
Controls,
ReactFlowProvider,
Node,
Edge,
AriaLabelConfig,
Panel,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'A11y Node 1' },
position: { x: 250, y: 5 },
className: 'light',
domAttributes: {
tabIndex: 10,
'aria-roledescription': 'A11y Node',
},
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 1000, y: 100 },
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 100, y: 100 },
className: 'light',
ariaRole: 'button',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 300, y: 100 },
},
{
id: '5',
data: { label: 'Node 5' },
position: { x: 400, y: 200 },
},
{
id: '6',
data: { label: 'Node 6' },
position: { x: -1000, y: 200 },
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e1-4', source: '1', target: '4' },
{ id: 'e1-5', source: '4', target: '5' },
{ id: 'e1-6', source: '3', target: '6' },
];
const ariaLabelConfig: Partial<AriaLabelConfig> = {
'node.a11yDescription.default': 'Custom Node Desc.',
'node.a11yDescription.keyboardDisabled': 'Custom Keyboard Desc.',
'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }) =>
`Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`,
'edge.a11yDescription.default': 'Custom Edge Desc.',
'controls.ariaLabel': 'Custom Controls Aria Label',
'controls.zoomIn.ariaLabel': 'Custom Zoom in',
'controls.zoomOut.ariaLabel': 'Custom Zoom Out',
'controls.fitView.ariaLabel': 'Custom Fit View',
'controls.interactive.ariaLabel': 'Custom Toggle Interactivity',
'minimap.ariaLabel': 'Custom Aria Label',
};
const A11y = () => {
const [autoPanOnNodeFocus, setAutoPanOnNodeFocus] = useState(true);
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
autoPanOnNodeFocus={autoPanOnNodeFocus}
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
nodeDragThreshold={0}
ariaLabelConfig={ariaLabelConfig}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<Panel position="top-right">
<div>
<label htmlFor="focusPannable">
<input
id="focusPannable"
type="checkbox"
checked={autoPanOnNodeFocus}
onChange={(event) => setAutoPanOnNodeFocus(event.target.checked)}
className="xy-theme__checkbox"
/>
autoPanOnNodeFocus
</label>
</div>
</Panel>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<A11y />
</ReactFlowProvider>
);
}
```
## /examples/react/src/examples/AddNodeOnEdgeDrop/index.tsx
```tsx path="/examples/react/src/examples/AddNodeOnEdgeDrop/index.tsx"
import { useCallback, useRef } from 'react';
import {
ReactFlow,
useNodesState,
useEdgesState,
addEdge,
useReactFlow,
ReactFlowProvider,
OnConnect,
OnConnectStart,
OnConnectEnd,
Node,
Edge,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '0',
type: 'input',
data: { label: 'Node' },
position: { x: 0, y: 50 },
},
];
const initialEdges: Edge[] = [];
let id = 1;
const getId = () => `${id++}`;
const AddNodeOnEdgeDrop = () => {
const reactFlowWrapper = useRef(null);
const connectingNodeId = useRef<string | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { screenToFlowPosition } = useReactFlow();
const onConnect: OnConnect = useCallback((params) => {
// reset the start node on connections
connectingNodeId.current = null;
setEdges((eds) => addEdge(params, eds));
}, []);
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
if (!connectingNodeId.current) return;
const targetIsPane = (event.target as Partial<Element> | null)?.classList?.contains('react-flow__pane');
if (targetIsPane && 'clientX' in event && 'clientY' in event) {
// we need to remove the wrapper bounds, in order to get the correct position
const id = getId();
const newNode: Node = {
id,
position: screenToFlowPosition({
x: event.clientX,
y: event.clientY,
}),
data: { label: `Node ${id}` },
origin: [0.5, 0.0],
};
const newEdge: Edge = {
id,
source: connectingNodeId.current,
target: id,
};
setNodes((nds) => nds.concat(newNode));
setEdges((eds) => eds.concat(newEdge));
}
},
[screenToFlowPosition]
);
return (
<div className="wrapper" ref={reactFlowWrapper} style={{ height: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
fitView
/>
</div>
);
};
export default () => (
<ReactFlowProvider>
<AddNodeOnEdgeDrop />
</ReactFlowProvider>
);
```
## /examples/react/src/examples/Backgrounds/index.tsx
```tsx path="/examples/react/src/examples/Backgrounds/index.tsx"
import { FC } from 'react';
import {
ReactFlow,
Node,
ReactFlowProvider,
useNodesState,
Background,
BackgroundProps,
BackgroundVariant,
} from '@xyflow/react';
import styles from './style.module.css';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 50, y: 50 },
},
];
const Flow: FC<{ id: string; bgProps: BackgroundProps[] }> = ({ id, bgProps }) => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
return (
<ReactFlowProvider>
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} id={id}>
{bgProps.map((props, idx) => (
<Background key={idx} id={idx.toString()} {...props} />
))}
</ReactFlow>
</ReactFlowProvider>
);
};
const Backgrounds: FC = () => (
<div className={styles.wrapper}>
<Flow id="flow-a" bgProps={[{ variant: BackgroundVariant.Dots }]} />
<Flow id="flow-b" bgProps={[{ variant: BackgroundVariant.Lines, gap: [50, 50] }]} />
<Flow id="flow-c" bgProps={[{ variant: BackgroundVariant.Cross, gap: [100, 50] }]} />
<Flow
id="flow-d"
bgProps={[
{ variant: BackgroundVariant.Lines, gap: 10 },
{ variant: BackgroundVariant.Lines, gap: 100, offset: 2, color: '#ccc' },
]}
/>
</div>
);
export default Backgrounds;
```
## /examples/react/src/examples/Backgrounds/style.module.css
```css path="/examples/react/src/examples/Backgrounds/style.module.css"
.wrapper {
display: flex;
height: 100%;
}
.wrapper :global .react-flow {
width: 100%;
height: 100%;
}
.wrapper :global .react-flow {
border-right: 1px solid #ddd;
}
```
## /examples/react/src/examples/Basic/index.tsx
```tsx path="/examples/react/src/examples/Basic/index.tsx"
import { MouseEvent, useCallback, useState } from 'react';
import {
ReactFlow,
MiniMap,
Background,
BackgroundVariant,
Controls,
ReactFlowProvider,
Node,
Edge,
useReactFlow,
Panel,
OnNodeDrag,
FitViewOptions,
} from '@xyflow/react';
const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const printSelectionEvent = (name: string) => (_: MouseEvent, nodes: Node[]) => console.log(name, nodes);
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const defaultEdgeOptions = {};
const fitViewOptions: FitViewOptions = {
padding: { top: '100px', left: '0%', right: '10%', bottom: 0.1 },
};
const BasicFlow = () => {
const {
addNodes,
setNodes,
getNodes,
setEdges,
getEdges,
deleteElements,
updateNodeData,
toObject,
setViewport,
fitView,
} = useReactFlow();
const updatePos = () => {
setNodes((nodes) =>
nodes.map((node) => {
return {
...node,
position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
};
})
);
};
const logToObject = () => console.log(toObject());
const resetTransform = () => setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nodes) =>
nodes.map((node) => {
return {
...node,
className: node.className === 'light' ? 'dark' : 'light',
};
})
);
};
const deleteSelectedElements = useCallback(() => {
const selectedNodes = getNodes().filter((node) => node.selected);
const selectedEdges = getEdges().filter((edge) => edge.selected);
deleteElements({ nodes: selectedNodes, edges: selectedEdges });
}, [deleteElements]);
const deleteSomeElements = useCallback(() => {
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
}, []);
const onSetNodes = () => {
setNodes([
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
]);
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
fitView();
};
const onUpdateNode = () => {
updateNodeData('1', { label: 'update' });
updateNodeData('2', { label: 'update' });
};
const addNode = () => {
addNodes({
id: `${Math.random()}`,
data: { label: 'Node' },
position: { x: Math.random() * 300, y: Math.random() * 300 },
className: 'light',
});
fitView();
};
const [isHidden, setIsHidden] = useState(false);
const toggleVisibility = () => {
setIsHidden(!isHidden);
};
return (
<>
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodesChange={console.log}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onSelectionDragStart={printSelectionEvent('selection drag start')}
onSelectionDrag={printSelectionEvent('selection drag')}
onSelectionDragStop={printSelectionEvent('selection drag stop')}
className="react-flow-basic-example"
style={{ display: isHidden ? 'none' : 'block' }}
minZoom={0.2}
maxZoom={4}
fitView
fitViewOptions={fitViewOptions}
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
autoPanOnSelection={true}
nodeDragThreshold={0}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<Panel position="top-right">
<button onClick={resetTransform}>reset transform</button>
<button onClick={updatePos}>change pos</button>
<button onClick={toggleClassnames}>toggle classnames</button>
<button onClick={logToObject}>toObject</button>
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
<button onClick={deleteSomeElements}>deleteSomeElements</button>
<button onClick={onSetNodes}>setNodes</button>
<button onClick={onUpdateNode}>updateNode</button>
<button onClick={addNode}>addNode</button>
</Panel>
</ReactFlow>
<button onClick={toggleVisibility} style={{ position: 'absolute', zIndex: 10, right: 10, top: 100 }}>
{isHidden ? 'Show' : 'Hide'} Flow
</button>
</>
);
};
export default function App() {
return (
<ReactFlowProvider>
<BasicFlow />
</ReactFlowProvider>
);
}
```
## /examples/react/src/examples/BrokenNodes/index.tsx
```tsx path="/examples/react/src/examples/BrokenNodes/index.tsx"
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;
```
## /examples/react/src/examples/CancelConnection/Timer.module.css
```css path="/examples/react/src/examples/CancelConnection/Timer.module.css"
.Timer {
position: absolute;
bottom: 0;
transition-duration: 0.3s;
left: 50%;
transform: translate(-50%, 100%);
font-size: 1.5rem;
z-index: 999;
background: linear-gradient(#fff, #fff, #f5f5f5);
padding: 0.8rem 1.5rem;
border-radius: 5px;
border: 1px solid #ccc;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.2);
}
.Timer.show {
transform: translate(-50%, 0%) translateY(-15px);
}
.progress {
display: none;
position: absolute;
bottom: 0;
left: 0;
height: 5px;
background: linear-gradient(to right, #42df96, #16dfed);
}
.Timer.show .progress {
display: block;
}
```
## /examples/react/src/examples/CancelConnection/Timer.tsx
```tsx path="/examples/react/src/examples/CancelConnection/Timer.tsx"
import cc from 'classcat';
import styles from './Timer.module.css';
interface Props {
duration: number;
remaining: number;
show: boolean;
}
export default function Timer({
duration,
remaining,
show,
}: Props) {
const percentage = 100 - (remaining / duration) * 100;
return (
<div className={cc({
[styles.Timer]: true,
[styles.show]: show,
})}>
<div className={styles.progress} style={{
width: `${percentage}%`,
}} />
Connection will be canceled in {remaining} seconds
</div>
)
}
```
## /examples/react/src/examples/CancelConnection/data.ts
```ts path="/examples/react/src/examples/CancelConnection/data.ts"
import { Edge, Node } from '@xyflow/react';
export const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
export const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
```
## /examples/react/src/examples/CancelConnection/hooks/useCountdown.ts
```ts path="/examples/react/src/examples/CancelConnection/hooks/useCountdown.ts"
import { useRef, useState } from 'react';
const useCountdown = (callback: () => void) => {
const interval = useRef<NodeJS.Timer>();
const [remaining, setRemaining] = useState(0);
const start = (duration: number) => {
setRemaining(duration);
interval.current = setInterval(() => {
setRemaining((prev) => {
if (prev === 1) {
clearInterval(interval.current);
callback();
}
return prev - 1;
});
}, 1000);
};
const stop = () => {
clearInterval(interval.current);
setRemaining(0);
};
return {
start,
remaining,
stop,
counting: remaining > 0,
};
};
export default useCountdown;
```
## /examples/react/src/examples/CancelConnection/index.tsx
```tsx path="/examples/react/src/examples/CancelConnection/index.tsx"
import {
ReactFlow,
Background,
MiniMap,
addEdge,
ReactFlowProvider,
Connection,
Edge,
useNodesState,
useEdgesState,
OnConnectStart,
OnConnectEnd,
useStore,
} from '@xyflow/react';
import useCountdown from './hooks/useCountdown';
import { initialEdges, initialNodes } from './data';
import Timer from './Timer';
const CANCEL_AFTER = 5; // seconds
const CancelConnection = () => {
const [nodes, _, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const cancelConnection = useStore((state) => state.cancelConnection);
// Cancels connection after 5 seconds
const countdown = useCountdown(() => cancelConnection());
const onConnectStart: OnConnectStart = () => countdown.start(CANCEL_AFTER);
const onConnectEnd: OnConnectEnd = () => countdown.stop();
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<>
<Timer duration={CANCEL_AFTER} show={countdown.counting} remaining={countdown.remaining} />
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onConnect={onConnect}
fitView
maxZoom={2}
>
<Background />
<MiniMap />
</ReactFlow>
</>
);
};
export default () => (
<ReactFlowProvider>
<CancelConnection />
</ReactFlowProvider>
);
```
## /examples/react/src/examples/ClickDistance/index.tsx
```tsx path="/examples/react/src/examples/ClickDistance/index.tsx"
import { useCallback, useState } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState, Panel } from '@xyflow/react';
const initNodes: 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 initEdges: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const onPaneClick = () => console.log('pane click');
const BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const [paneClickDistance, setPaneClickDistance] = useState(0);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
paneClickDistance={paneClickDistance}
onPaneClick={onPaneClick}
>
<Panel position="top-right">
<input
type="range"
min={0}
max={100}
value={paneClickDistance}
onChange={(evt) => setPaneClickDistance(+evt.target.value)}
/>
click distance: {paneClickDistance}
</Panel>
</ReactFlow>
);
};
export default BasicFlow;
```
## /examples/react/src/examples/ColorMode/index.tsx
```tsx path="/examples/react/src/examples/ColorMode/index.tsx"
import { ChangeEventHandler, useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
Node,
useNodesState,
useEdgesState,
OnConnect,
Edge,
MiniMap,
Background,
Controls,
Panel,
ColorMode,
Position,
} from '@xyflow/react';
const nodeDefaults = {
sourcePosition: Position.Right,
targetPosition: Position.Left,
};
const initialNodes: Node[] = [
{ id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults },
{ id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults },
{ id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults },
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults },
];
const initialEdges: Edge[] = [
{
id: 'A-B',
source: 'A',
target: 'B',
},
{
id: 'A-C',
source: 'A',
target: 'C',
},
{
id: 'A-D',
source: 'A',
target: 'D',
},
];
const ColorModeFlow = () => {
const [colorMode, setColorMode] = useState<ColorMode>('light');
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback(
(params) => {
console.log('on connect', params);
setEdges((eds) => addEdge(params, eds));
},
[setEdges]
);
const onChange: ChangeEventHandler<HTMLSelectElement> = (evt) => setColorMode(evt.target.value as ColorMode);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
colorMode={colorMode}
fitView
>
<MiniMap />
<Background />
<Controls />
<Panel position="top-right">
<select onChange={onChange} data-testid="colormode-select">
<option value="light">light</option>
<option value="dark">dark</option>
<option value="system">system</option>
</select>
</Panel>
</ReactFlow>
);
};
export default ColorModeFlow;
```
## /examples/react/src/examples/ControlledUncontrolled/index.tsx
```tsx path="/examples/react/src/examples/ControlledUncontrolled/index.tsx"
import {
ReactFlow,
useReactFlow,
Node,
Edge,
ReactFlowProvider,
useNodesState,
useEdgesState,
Background,
BackgroundVariant,
} from '@xyflow/react';
const defaultNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const defaultEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
];
const defaultEdgeOptions = {
animated: true,
};
// This is bad practise. You should either use a controlled or an uncontrolled component.
// This is just an example for testing the API.
const ControlledUncontrolled = () => {
const [nodes, , onNodesChange] = useNodesState(defaultNodes);
const [edges, , onEdgesChange] = useEdgesState(defaultEdges);
const instance = useReactFlow();
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const updateNodePositions = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
return {
...node,
position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
};
})
);
};
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => {
return {
...edge,
style: {
stroke: '#ff5050',
},
};
})
);
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
defaultEdgeOptions={defaultEdgeOptions}
fitView
>
<Background variant={BackgroundVariant.Lines} />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updateNodePositions} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={updateEdgeColors} style={{ marginRight: 5 }}>
red edges
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<ControlledUncontrolled />
</ReactFlowProvider>
);
}
```
## /examples/react/src/examples/ControlledViewport/index.tsx
```tsx path="/examples/react/src/examples/ControlledViewport/index.tsx"
import { useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
Node,
Connection,
Edge,
useNodesState,
useEdgesState,
Viewport,
Panel,
MiniMap,
Background,
ReactFlowProvider,
useReactFlow,
Controls,
} from '@xyflow/react';
const initNodes: 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 initEdges: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const Flow = () => {
const [nodes, _, onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const [viewport, setViewport] = useState<Viewport>({ x: 0, y: 0, zoom: 1 });
const [viewport2, setViewport2] = useState<Viewport>({ x: 100, y: 100, zoom: 1.5 });
const [currentViewport, setCurrentViewport] = useState(0);
const { fitView } = useReactFlow();
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const setter = currentViewport === 0 ? setViewport : setViewport2;
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
viewport={currentViewport === 0 ? viewport : viewport2}
onViewportChange={setter}
>
<Panel position="top-left">
<button onClick={() => setter((vp) => ({ ...vp, y: vp.y + 10 }))}>update viewport</button>
<button onClick={() => fitView()}>fitView</button>
<button onClick={() => setCurrentViewport(currentViewport === 0 ? 1 : 0)}>toggle viewport</button>
</Panel>
<MiniMap />
<Background />
<Controls />
</ReactFlow>
);
};
export default () => (
<ReactFlowProvider>
<Flow />
</ReactFlowProvider>
);
```
## /examples/react/src/examples/CustomConnectionLine/ConnectionLine.tsx
```tsx path="/examples/react/src/examples/CustomConnectionLine/ConnectionLine.tsx"
import { ConnectionLineComponentProps } from '@xyflow/react';
function ConnectionLine({ fromX, fromY, toX, toY, pointer }: ConnectionLineComponentProps) {
console.log('pointer', pointer);
return (
<>
<path
fill="none"
stroke="#222"
strokeWidth={1.5}
className="animated"
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</>
);
}
export default ConnectionLine;
```
## /examples/react/src/examples/CustomConnectionLine/index.tsx
```tsx path="/examples/react/src/examples/CustomConnectionLine/index.tsx"
import { useCallback } from 'react';
import {
ReactFlow,
Node,
addEdge,
Connection,
Edge,
useNodesState,
useEdgesState,
Background,
BackgroundVariant,
} from '@xyflow/react';
import ConnectionLine from './ConnectionLine';
const initialNodes: Node[] = [
{
id: '1',
type: 'default',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
];
const initialEdges: Edge[] = [];
const ConnectionLineFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
connectionLineComponent={ConnectionLine}
onConnect={onConnect}
connectionDragThreshold={25}
>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default ConnectionLineFlow;
```
## /examples/react/src/examples/CustomMiniMapNode/index.tsx
```tsx path="/examples/react/src/examples/CustomMiniMapNode/index.tsx"
import { MouseEvent, CSSProperties, useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
Background,
BackgroundVariant,
Connection,
Controls,
Edge,
MiniMap,
MiniMapNodeProps,
Node,
ReactFlowInstance,
useEdgesState,
useNodesState,
Panel,
} from '@xyflow/react';
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
return <circle cx={x} cy={y} r={Math.max(width, height) / 2} fill="#ffcc00" />;
};
const CustomMiniMapNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const [hideAllNodes, setHideAllNodes] = useState(false);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => {
const nodeId = (nodes.length + 1).toString();
const newNode: Node = {
id: nodeId,
data: { label: `Node: ${nodeId}` },
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
hidden: hideAllNodes,
};
setNodes((nds) => nds.concat(newNode));
};
const toggleHideAllNodes = () => {
setHideAllNodes(prev => {
const next = !prev;
setNodes(nds => nds.map(n => ({ ...n, hidden: next })));
setEdges(eds => eds.map(e => ({ ...e, hidden: next })));
return next;
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={(p) => onConnect(p)}
onNodeDragStop={onNodeDragStop}
onlyRenderVisibleElements={true}
>
<Controls />
<Background variant={BackgroundVariant.Lines} />
<MiniMap nodeComponent={CustomMiniMapNode} />
<Panel position="top-left">
<button type="button" onClick={addRandomNode}>
add node
</button>
<button type="button" onClick={toggleHideAllNodes}>
{hideAllNodes ? 'show all nodes' : 'hide all nodes'}
</button>
</Panel>
</ReactFlow>
);
};
export default CustomMiniMapNodeFlow;
```
## /examples/react/src/examples/CustomNode/ColorSelectorNode.tsx
```tsx path="/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx"
import React, { memo, CSSProperties, useCallback } from 'react';
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
import type { ColorSelectorNode } from '.';
const targetHandleStyle: CSSProperties = { background: '#555' };
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
const sourceHandleStyleB: CSSProperties = {
...targetHandleStyle,
bottom: 10,
top: 'auto',
};
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
function ColorSelectorNode({ data, isConnectable }: NodeProps<ColorSelectorNode>) {
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
useOnViewportChange({
onStart,
onChange,
onEnd,
});
return (
<>
<Handle type="target" position={Position.Left} style={targetHandleStyle} onConnect={onConnect} />
<div>
Custom Color Picker Node: <strong>{data.color}</strong>
</div>
<input className="nodrag nokey" type="color" onChange={data.onChange} defaultValue={data.color} />
<Handle
type="source"
position={Position.Right}
id="a"
style={sourceHandleStyleA}
isConnectable={isConnectable}
onMouseDown={(e) => {
console.log('You trigger mousedown event', e);
}}
/>
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
</>
);
}
export default memo(ColorSelectorNode);
```
## /examples/react/src/examples/CustomNode/index.tsx
```tsx path="/examples/react/src/examples/CustomNode/index.tsx"
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback, useRef } from 'react';
import {
ReactFlow,
MiniMap,
Controls,
addEdge,
Node,
Position,
SnapGrid,
useEdgesState,
Background,
OnNodeDrag,
OnInit,
applyNodeChanges,
OnNodesChange,
OnConnect,
OnBeforeDelete,
BuiltInNode,
BuiltInEdge,
NodeTypes,
ReactFlowProvider,
} from '@xyflow/react';
import ColorSelectorNode from './ColorSelectorNode';
export type ColorSelectorNode = Node<
{ color: string; onChange: (event: ChangeEvent<HTMLInputElement>) => void },
'selectorNode'
>;
export type MyNode = BuiltInNode | ColorSelectorNode;
export type MyEdge = BuiltInEdge;
const onInit: OnInit<MyNode, MyEdge> = (reactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance);
};
const onNodeDragStop: OnNodeDrag<MyNode> = (_, node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: MyNode) => console.log('click', node);
const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes: NodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const ref = useRef(null);
const [nodes, setNodes] = useState<MyNode[]>([]);
const onNodesChange: OnNodesChange<MyNode> = useCallback(
(changes) =>
setNodes((nds) => {
const nextNodes = applyNodeChanges(changes, nds);
return nextNodes;
}),
[setNodes]
);
const [edges, setEdges, onEdgesChange] = useEdgesState<MyEdge>([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setNodes((nds) =>
nds.map((node) => {
if (node.id !== '2' || node.type !== 'selectorNode') {
return node;
}
const color = event.target.value;
setBgColor(color);
return {
...node,
data: {
...node.data,
color,
},
};
})
);
};
setNodes([
{
id: '1',
type: 'input',
data: { label: 'An input node' },
position: { x: 0, y: 50 },
sourcePosition: Position.Right,
},
{
id: '2',
type: 'selectorNode',
data: { onChange: onChange, color: initBgColor },
style: { border: '1px solid #777', padding: 10 },
position: { x: 250, y: 50 },
},
{
id: '3',
type: 'output',
data: { label: 'Output A' },
position: { x: 550, y: 25 },
targetPosition: Position.Left,
},
{
id: '4',
type: 'output',
data: { label: 'Output B' },
position: { x: 550, y: 100 },
targetPosition: Position.Left,
},
]);
setEdges([
{
id: 'e1-2',
source: '1',
target: '2',
animated: true,
style: { stroke: '#fff' },
},
{
id: 'e2a-3',
source: '2',
sourceHandle: 'a',
target: '3',
animated: true,
style: { stroke: '#fff' },
},
{
id: 'e2b-4',
source: '2',
sourceHandle: 'b',
target: '4',
animated: true,
style: { stroke: '#fff' },
},
]);
}, []);
const onConnect: OnConnect = useCallback(
(connection) => setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
[setEdges]
);
const onBeforeDelete: OnBeforeDelete<MyNode, MyEdge> = useCallback(async (params) => true, []);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
onInit={onInit}
nodeTypes={nodeTypes}
connectionLineStyle={connectionLineStyle}
snapToGrid={true}
snapGrid={snapGrid}
fitView
minZoom={0.3}
maxZoom={2}
onBeforeDelete={onBeforeDelete}
ref={ref}
>
<MiniMap<MyNode>
nodeStrokeColor={(n: MyNode): string => {
if (n.type === 'input') return '#0041d0';
if (n.type === 'selectorNode') return bgColor;
if (n.type === 'output') return '#ff0072';
return '#eee';
}}
nodeColor={(n: MyNode): string => {
if (n.type === 'selectorNode') return bgColor;
return '#fff';
}}
/>
<Controls />
<Background bgColor={bgColor} />
</ReactFlow>
);
};
export default () => (
<ReactFlowProvider>
<CustomNodeFlow />
</ReactFlowProvider>
);
```
## /examples/react/src/examples/DefaultEdgeOverwrite/index.tsx
```tsx path="/examples/react/src/examples/DefaultEdgeOverwrite/index.tsx"
import {
ReactFlow,
Node,
Edge,
ReactFlowProvider,
Background,
BackgroundVariant,
EdgeProps,
getBezierPath,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
];
const initialEdges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'unregistered', // This will fallback to custom default
},
];
const CustomEdge = ({ sourceX, sourceY, targetX, targetY }: EdgeProps) => {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
<path d={edgePath} stroke="red" strokeWidth={3} fill="none" strokeDasharray="5,5" />
</>
);
};
const edgeTypes = {
default: CustomEdge,
};
const DefaultEdgeOverwrite = () => {
return (
<ReactFlow defaultNodes={initialNodes} defaultEdges={initialEdges} edgeTypes={edgeTypes} fitView>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<DefaultEdgeOverwrite />
</ReactFlowProvider>
);
}
```
## /examples/react/src/examples/DefaultNodeOverwrite/index.tsx
```tsx path="/examples/react/src/examples/DefaultNodeOverwrite/index.tsx"
import { ReactFlow, Node, ReactFlowProvider, Background, BackgroundVariant, NodeProps } from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
type: 'unregistered',
position: { x: 100, y: 100 },
className: 'light',
},
];
const CustomNode = (_: NodeProps) => {
return <div>Custom node</div>;
};
const nodeTypes = {
default: CustomNode,
};
const DefaultNodeOverwrite = () => {
return (
<ReactFlow defaultNodes={initialNodes} nodeTypes={nodeTypes} fitView>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<DefaultNodeOverwrite />
</ReactFlowProvider>
);
}
```
## /examples/react/src/examples/DefaultNodes/index.tsx
```tsx path="/examples/react/src/examples/DefaultNodes/index.tsx"
import {
ReactFlow,
useReactFlow,
Node,
Edge,
ReactFlowProvider,
Background,
BackgroundVariant,
Panel,
} from '@xyflow/react';
const defaultNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
type: 'output',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const defaultEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
];
const defaultEdgeOptions = {
animated: true,
};
const DefaultNodes = () => {
const instance = useReactFlow();
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const updateNodePositions = () => {
instance.setNodes((nodes) =>
nodes.map((node) => ({
...node,
position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
}))
);
};
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => ({
...edge,
style: {
stroke: '#ff5050',
},
}))
);
};
return (
<ReactFlow defaultNodes={defaultNodes} defaultEdges={defaultEdges} defaultEdgeOptions={defaultEdgeOptions} fitView>
<Background variant={BackgroundVariant.Lines} />
<Panel position="top-right">
<button onClick={resetTransform}>reset transform</button>
<button onClick={updateNodePositions}>change pos</button>
<button onClick={updateEdgeColors}>red edges</button>
<button onClick={logToObject}>toObject</button>
</Panel>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<DefaultNodes />
</ReactFlowProvider>
);
}
```
## /examples/react/src/examples/DetachedHandle/index.tsx
```tsx path="/examples/react/src/examples/DetachedHandle/index.tsx"
import {
ReactFlow,
Node,
ReactFlowProvider,
Background,
BackgroundVariant,
NodeProps,
Handle,
Position,
} from '@xyflow/react';
import './style.css';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 50, y: 100 },
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 450, y: 100 },
},
];
const CustomNode = (_: NodeProps) => {
return (
<>
<Handle type="target" position={Position.Left} />
<div>Custom node</div>
<Handle type="source" position={Position.Right}>
<button className="detached-handle">➡️</button>
</Handle>
</>
);
};
const nodeTypes = {
default: CustomNode,
};
const DetachedHandle = () => {
return (
<ReactFlow defaultNodes={initialNodes} defaultEdges={[]} connectionRadius={10} nodeTypes={nodeTypes} fitView>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<DetachedHandle />
</ReactFlowProvider>
);
}
```
## /examples/react/src/examples/DetachedHandle/style.css
```css path="/examples/react/src/examples/DetachedHandle/style.css"
.detached-handle {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
width: 2rem;
height: 2rem;
border: none;
border-radius: 50%;
}
```
## /examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx
```tsx path="/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx"
import { useEffect, useRef, useState } from 'react';
import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react';
type ChangeLoggerProps = {
color?: string;
limit?: number;
};
type ChangeInfoProps = {
change: NodeChange;
};
function ChangeInfo({ change }: ChangeInfoProps) {
const id = 'id' in change ? change.id : '-';
const { type } = change;
return (
<div style={{ marginBottom: 4 }}>
<div>node id: {id}</div>
<div>
{type === 'add' ? JSON.stringify(change.item, null, 2) : null}
{type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null}
{type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null}
{type === 'remove' ? 'remove' : null}
{type === 'replace' ? JSON.stringify(change.item, null, 2) : null}
{type === 'select' ? (change.selected ? 'select' : 'unselect') : null}
</div>
</div>
);
}
export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) {
const [changes, setChanges] = useState<NodeChange[]>([]);
const onNodesChangeIntercepted = useRef(false);
const onNodesChange = useStore((s) => s.onNodesChange);
const store = useStoreApi();
useEffect(() => {
if (!onNodesChange || onNodesChangeIntercepted.current) {
return;
}
onNodesChangeIntercepted.current = true;
const userOnNodesChange = onNodesChange;
const onNodesChangeLogger: OnNodesChange = (changes) => {
userOnNodesChange(changes);
setChanges((c) => {
changes.forEach((change) => {
if (c.length >= limit) {
c.pop();
}
c = [change, ...c];
});
return c;
});
};
store.setState({ onNodesChange: onNodesChangeLogger });
}, [onNodesChange]);
return (
<div className="react-flow__devtools-changelogger">
<div className="react-flow__devtools-title">Change Logger</div>
{changes.length === 0 ? (
<>no changes triggered</>
) : (
changes.map((change, index) => <ChangeInfo key={index} change={change} />)
)}
</div>
);
}
```
## /examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx
```tsx path="/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx"
import { useNodes, ViewportPortal } from '@xyflow/react';
type NodeInfoProps = {
id: string;
type: string;
x: number;
y: number;
width?: number;
height?: number;
data: any;
};
function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) {
if (!width || !height) {
return null;
}
return (
<div
className="react-flow__devtools-nodeinfo"
style={{
position: 'absolute',
transform: `translate(${x}px, ${y + height}px)`,
width: width * 2,
}}
>
<div>id: {id}</div>
<div>type: {type}</div>
<div>
position: {x.toFixed(1)}, {y.toFixed(1)}
</div>
<div>
dimensions: {width} × {height}
</div>
<div>data: {JSON.stringify(data, null, 2)}</div>
</div>
);
}
export default function NodeInspector() {
const nodes = useNodes();
return (
<ViewportPortal>
<div className="react-flow__devtools-nodeinspector">
{nodes.map((node) => {
const x = node?.position?.x || 0;
const y = node?.position?.y || 0;
const width = node.measured?.width || 0;
const height = node.measured?.height || 0;
return (
<NodeInfo
key={node.id}
id={node.id}
type={node.type || 'default'}
x={x}
y={y}
width={width}
height={height}
data={node.data}
/>
);
})}
</div>
</ViewportPortal>
);
}
```
## /examples/react/src/examples/DevTools/DevTools/index.tsx
```tsx path="/examples/react/src/examples/DevTools/DevTools/index.tsx"
import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react';
import { Panel, PanelPosition } from '@xyflow/react';
import NodeInspector from './NodeInspector';
import ChangeLogger from './ChangeLogger';
import './style.css';
export default function ReactFlowDevTools({ position = 'top-left' }: { position?: PanelPosition }) {
const [nodeInspectorActive, setNodeInspectorActive] = useState(false);
const [changeLoggerActive, setChangeLoggerActive] = useState(false);
return (
<div className="react-flow__devtools">
<Panel position={position}>
<DevToolButton setActive={setNodeInspectorActive} active={nodeInspectorActive} title="Toggle Node Inspector">
Node Inspector
</DevToolButton>
<DevToolButton setActive={setChangeLoggerActive} active={changeLoggerActive} title="Toggle Change Logger">
Change Logger
</DevToolButton>
</Panel>
{changeLoggerActive && <ChangeLogger />}
{nodeInspectorActive && <NodeInspector />}
</div>
);
}
function DevToolButton({
active,
setActive,
children,
...rest
}: {
active: boolean;
setActive: Dispatch<SetStateAction<boolean>>;
children: ReactNode;
} & HTMLAttributes<HTMLButtonElement>) {
return (
<button onClick={() => setActive((a) => !a)} className={active ? 'active' : ''} {...rest}>
{children}
</button>
);
}
```
## /examples/react/src/examples/DevTools/DevTools/style.css
```css path="/examples/react/src/examples/DevTools/DevTools/style.css"
.react-flow__devtools {
--border-radius: 4px;
--highlight-color: rgba(238, 58, 115, 1);
--font: monospace, sans-serif;
border-radius: var(--border-radius);
font-size: 11px;
font-family: var(--font);
}
.react-flow__devtools button {
background: white;
border: none;
padding: 5px 15px;
color: #222;
font-weight: bold;
font-size: 12px;
cursor: pointer;
font-family: var(--font);
background-color: #f4f4f4;
}
.react-flow__devtools button:hover {
background: var(--highlight-color);
color: white;
}
.react-flow__devtools button.active {
background: var(--highlight-color);
color: white;
}
.react-flow__devtools button:first-child {
border-radius: var(--border-radius) 0 0 var(--border-radius);
border-right: 1px solid #ddd;
}
.react-flow__devtools button:last-child {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.react-flow__devtools-changelogger {
pointer-events: none;
position: relative;
top: 50px;
left: 20px;
font-family: var(--font);
}
.react-flow__devtools-title {
font-weight: bold;
margin-bottom: 5px;
}
.react-flow__devtools-nodeinspector {
pointer-events: none;
font-family: monospace, sans-serif;
font-size: 10px;
}
.react-flow__devtools-nodeinfo {
top: 5px;
}
```
## /examples/react/src/examples/DevTools/index.tsx
```tsx path="/examples/react/src/examples/DevTools/index.tsx"
import { useCallback } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
import DevTools from './DevTools';
const initNodes: Node[] = [
{
id: '1a',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{
id: '2a',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
{
id: '3a',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
},
{
id: '4a',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
},
];
const initEdges: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a' },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const BasicFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<DevTools />
</ReactFlow>
);
};
export default BasicFlow;
```
## /examples/react/src/examples/DragHandle/DragHandleNode.tsx
```tsx path="/examples/react/src/examples/DragHandle/DragHandleNode.tsx"
import { memo, FC } from 'react';
import { Handle, Position, NodeProps, Connection, Edge } from '@xyflow/react';
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const labelStyle = {
display: 'flex',
alignItems: 'center',
};
const dragHandleStyle = {
display: 'inline-block',
width: 25,
height: 25,
backgroundColor: 'teal',
marginLeft: 5,
borderRadius: '50%',
};
const ColorSelectorNode: FC<NodeProps> = () => {
return (
<>
<Handle type="target" position={Position.Left} onConnect={onConnect} />
<div style={labelStyle}>
Only draggable here → <span className="custom-drag-handle" style={dragHandleStyle} />
</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(ColorSelectorNode);
```
## /examples/react/src/examples/DragHandle/index.tsx
```tsx path="/examples/react/src/examples/DragHandle/index.tsx"
import { MouseEvent } from 'react';
import { ReactFlow, Node, Edge, useNodesState, useEdgesState } from '@xyflow/react';
import DragHandleNode from './DragHandleNode';
const nodeTypes = {
dragHandleNode: DragHandleNode,
};
const initialNodes: Node[] = [
{
id: '2',
type: 'dragHandleNode',
dragHandle: '.custom-drag-handle',
style: { border: '1px solid #ddd', padding: '20px 40px' },
position: { x: 200, y: 200 },
data: {},
},
];
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialEdges: Edge[] = [];
const DragHandleFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges] = useEdgesState(initialEdges);
return (
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
nodeDragThreshold={0}
/>
);
};
export default DragHandleFlow;
```
## /examples/react/src/examples/DragNDrop/Sidebar.tsx
```tsx path="/examples/react/src/examples/DragNDrop/Sidebar.tsx"
import { DragEvent } from 'react';
import styles from './dnd.module.css';
const onDragStart = (event: DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
const Sidebar = () => {
return (
<aside className={styles.aside}>
<div className={styles.description}>You can drag these nodes to the pane on the left.</div>
<div className="react-flow__node-input" onDragStart={(event: DragEvent) => onDragStart(event, 'input')} draggable>
Input Node
</div>
<div
className="react-flow__node-default"
onDragStart={(event: DragEvent) => onDragStart(event, 'default')}
draggable
>
Default Node
</div>
<div
className="react-flow__node-output"
onDragStart={(event: DragEvent) => onDragStart(event, 'output')}
draggable
>
Output Node
</div>
</aside>
);
};
export default Sidebar;
```
## /examples/react/src/examples/DragNDrop/dnd.module.css
```css path="/examples/react/src/examples/DragNDrop/dnd.module.css"
.dndflow {
flex-direction: column;
display: flex;
height: 100%;
}
.aside {
border-right: 1px solid #eee;
padding: 15px 10px;
font-size: 12px;
background: #fcfcfc;
}
.aside > * {
margin-bottom: 10px;
cursor: grab;
}
.description {
margin-bottom: 10px;
}
.wrapper {
flex-grow: 1;
height: 100%;
}
@media screen and (min-width: 768px) {
.aside {
width: 20%;
max-width: 180px;
}
.dndflow {
flex-direction: row;
}
}
```
## /examples/react/src/examples/DragNDrop/index.tsx
```tsx path="/examples/react/src/examples/DragNDrop/index.tsx"
import { useState, DragEvent } from 'react';
import {
ReactFlow,
ReactFlowProvider,
addEdge,
ReactFlowInstance,
Connection,
Edge,
Node,
useNodesState,
useEdgesState,
Controls,
NodeOrigin,
} from '@xyflow/react';
import Sidebar from './Sidebar';
import styles from './dnd.module.css';
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'input node' },
position: { x: 250, y: 5 },
},
];
const onDragOver = (event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
};
let id = 0;
const getId = () => `dndnode_${id++}`;
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const DnDFlow = () => {
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
const onDrop = (event: DragEvent) => {
event.preventDefault();
if (reactFlowInstance) {
const type = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: Node = {
id: getId(),
type,
position,
data: { label: `${type} node` },
};
setNodes((nds) => nds.concat(newNode));
}
};
return (
<div className={styles.dndflow}>
<ReactFlowProvider initialNodes={initialNodes} initialEdges={[]}>
<div className={styles.wrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onEdgesChange={onEdgesChange}
onNodesChange={onNodesChange}
onConnect={onConnect}
onInit={onInit}
onDrop={onDrop}
onDragOver={onDragOver}
nodeOrigin={nodeOrigin}
>
<Controls />
</ReactFlow>
</div>
<Sidebar />
</ReactFlowProvider>
</div>
);
};
export default DnDFlow;
```
## /examples/react/src/examples/EasyConnect/CustomConnectionLine.tsx
```tsx path="/examples/react/src/examples/EasyConnect/CustomConnectionLine.tsx"
import { ConnectionLineComponentProps, getStraightPath } from '@xyflow/react';
function CustomConnectionLine({ fromX, fromY, toX, toY, connectionLineStyle }: ConnectionLineComponentProps) {
const [edgePath] = getStraightPath({
sourceX: fromX,
sourceY: fromY,
targetX: toX,
targetY: toY,
});
return (
<g>
<path style={connectionLineStyle} fill="none" d={edgePath} />
<circle cx={toX} cy={toY} fill="black" r={3} stroke="black" strokeWidth={1.5} />
</g>
);
}
export default CustomConnectionLine;
```
## /examples/react/src/examples/EasyConnect/CustomNode.tsx
```tsx path="/examples/react/src/examples/EasyConnect/CustomNode.tsx"
import { Handle, NodeProps, Position, useConnection } from '@xyflow/react';
export default function CustomNode({ id }: NodeProps) {
const connection = useConnection();
const isTarget = connection.inProgress && connection.fromNode.id !== id;
const label = isTarget ? 'Drop here' : 'Drag to connect';
return (
<div className="customNode">
<div
className="customNodeBody"
style={{
borderStyle: isTarget ? 'dashed' : 'solid',
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
}}
>
{/* If handles are conditionally rendered and not present initially, you need to update the node internals https://reactflow.dev/docs/api/hooks/use-update-node-internals/ */}
{/* In this case we don't need to use useUpdateNodeInternals, since !isConnecting is true at the beginning and all handles are rendered initially. */}
{!connection.inProgress && <Handle className="customHandle" position={Position.Right} type="source" />}
{/* We want to disable the target handle, if the connection was started from this node */}
{(!connection.inProgress || isTarget) && (
<Handle className="customHandle" position={Position.Left} type="target" isConnectableStart={false} />
)}
{label}
</div>
</div>
);
}
```
## /examples/react/src/examples/EasyConnect/FloatingEdge.tsx
```tsx path="/examples/react/src/examples/EasyConnect/FloatingEdge.tsx"
import { EdgeProps, getStraightPath, useInternalNode } from '@xyflow/react';
import { getEdgeParams } from './utils.js';
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
if (!sourceNode || !targetNode) {
return null;
}
const { sx, sy, tx, ty } = getEdgeParams(sourceNode, targetNode);
const [edgePath] = getStraightPath({
sourceX: sx,
sourceY: sy,
targetX: tx,
targetY: ty,
});
return <path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} style={style} />;
}
export default FloatingEdge;
```
## /examples/react/src/examples/EasyConnect/index.tsx
```tsx path="/examples/react/src/examples/EasyConnect/index.tsx"
import { useCallback } from 'react';
import {
ReactFlow,
Node,
Edge,
addEdge,
useNodesState,
useEdgesState,
MarkerType,
OnConnect,
ConnectionMode,
} from '@xyflow/react';
import CustomNode from './CustomNode';
import FloatingEdge from './FloatingEdge';
import CustomConnectionLine from './CustomConnectionLine';
import './style.css';
const initialNodes: Node[] = [
{
id: '1',
type: 'custom',
position: { x: 0, y: 0 },
data: {},
},
{
id: '2',
type: 'custom',
position: { x: 250, y: 320 },
data: {},
},
{
id: '3',
type: 'custom',
position: { x: 40, y: 300 },
data: {},
},
{
id: '4',
type: 'custom',
position: { x: 300, y: 0 },
data: {},
},
];
const initialEdges: Edge[] = [];
const connectionLineStyle = {
strokeWidth: 3,
stroke: 'black',
};
const nodeTypes = {
custom: CustomNode,
};
const edgeTypes = {
floating: FloatingEdge,
};
const defaultEdgeOptions = {
style: { strokeWidth: 3, stroke: 'black' },
type: 'floating',
markerEnd: {
type: MarkerType.ArrowClosed,
color: 'black',
},
};
const EasyConnectExample = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
defaultEdgeOptions={defaultEdgeOptions}
connectionLineComponent={CustomConnectionLine}
connectionLineStyle={connectionLineStyle}
/>
);
};
export default EasyConnectExample;
```
## /examples/react/src/examples/EasyConnect/style.css
```css path="/examples/react/src/examples/EasyConnect/style.css"
.customNodeBody {
width: 150px;
height: 80px;
border: 3px solid black;
position: relative;
overflow: hidden;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
}
.customNode:before {
content: '';
position: absolute;
top: -10px;
left: 50%;
height: 20px;
width: 40px;
transform: translate(-50%, 0);
background: #d6d5e6;
z-index: 1000;
line-height: 1;
border-radius: 4px;
color: #fff;
font-size: 9px;
border: 2px solid #222138;
}
div.customHandle {
width: 100%;
height: 100%;
background: blue;
position: absolute;
top: 0;
left: 0;
border-radius: 0;
transform: none;
border: none;
opacity: 0;
}
```
## /examples/react/src/examples/EasyConnect/utils.tsx
```tsx path="/examples/react/src/examples/EasyConnect/utils.tsx"
import { Node, Position, MarkerType, XYPosition, InternalNode } from '@xyflow/react';
// this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: InternalNode, targetNode: InternalNode) {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
const targetPosition = targetNode.internals.positionAbsolute!;
const w = intersectionNodeWidth! / 2;
const h = intersectionNodeHeight! / 2;
const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition.y + h;
const x1 = targetPosition.x + w;
const y1 = targetPosition.y + h;
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
const a = 1 / (Math.abs(xx1) + Math.abs(yy1) || 1);
const xx3 = a * xx1;
const yy3 = a * yy1;
const x = w * (xx3 + yy3) + x2;
const y = h * (-xx3 + yy3) + y2;
return { x, y };
}
// returns the position (top,right,bottom or right) passed node compared to the intersection point
function getEdgePosition(node: InternalNode, intersectionPoint: XYPosition) {
const n = { ...node.internals.positionAbsolute, ...node };
const nx = Math.round(n.x!);
const ny = Math.round(n.y!);
const px = Math.round(intersectionPoint.x);
const py = Math.round(intersectionPoint.y);
if (px <= nx + 1) {
return Position.Left;
}
if (px >= nx + n.measured?.width! - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y! + n.measured?.height! - 1) {
return Position.Bottom;
}
return Position.Top;
}
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source: InternalNode, target: InternalNode) {
const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source);
const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
const targetPos = getEdgePosition(target, targetIntersectionPoint);
return {
sx: sourceIntersectionPoint.x,
sy: sourceIntersectionPoint.y,
tx: targetIntersectionPoint.x,
ty: targetIntersectionPoint.y,
sourcePos,
targetPos,
};
}
export function createNodesAndEdges() {
const nodes = [];
const edges = [];
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
nodes.push({ id: 'target', data: { label: 'Target' }, position: center });
for (let i = 0; i < 8; i++) {
const degrees = i * (360 / 8);
const radians = degrees * (Math.PI / 180);
const x = 250 * Math.cos(radians) + center.x;
const y = 250 * Math.sin(radians) + center.y;
nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
edges.push({
id: `edge-${i}`,
target: 'target',
source: `${i}`,
type: 'floating',
markerEnd: {
type: MarkerType.Arrow,
},
});
}
return { nodes, edges };
}
```
## /examples/react/src/examples/EdgeRenderer/CustomEdge.tsx
```tsx path="/examples/react/src/examples/EdgeRenderer/CustomEdge.tsx"
import { FC, MouseEvent } from 'react';
import { EdgeProps, getBezierPath, EdgeLabelRenderer, useStore } from '@xyflow/react';
const CustomEdge: FC<EdgeProps> = ({
id,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
}) => {
const isConnectedNodeDragging = useStore((s) =>
s.nodes.find((n) => n.dragging && (target === n.id || source === n.id))
);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const onClick = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
console.log('click', data.text);
};
return (
<>
<path id={id} className="react-flow__edge-path" d={edgePath} />
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
background: '#ffcc00',
padding: 10,
zIndex: isConnectedNodeDragging ? 10 : 0,
pointerEvents: 'all',
}}
className="nodrag nopan"
>
{data.text}
<input style={{ display: 'block' }} />
<button onClick={onClick}>send</button>
</div>
</EdgeLabelRenderer>
</>
);
};
export default CustomEdge;
```
## /examples/react/src/examples/EdgeRenderer/CustomEdge2.tsx
```tsx path="/examples/react/src/examples/EdgeRenderer/CustomEdge2.tsx"
import { FC } from 'react';
import { EdgeProps, getBezierPath, EdgeLabelRenderer, useStore } from '@xyflow/react';
const CustomEdge: FC<EdgeProps> = ({
id,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
}) => {
const isConnectedNodeDragging = useStore((s) =>
s.nodes.find((n) => n.dragging && (target === n.id || source === n.id))
);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<path id={id} className="react-flow__edge-path" d={edgePath} />
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
background: 'white',
border: '1px solid #555',
padding: 5,
zIndex: isConnectedNodeDragging ? 10 : 0,
}}
>
{data.text}
</div>
</EdgeLabelRenderer>
</>
);
};
export default CustomEdge;
```
## /examples/react/src/examples/EdgeRenderer/index.tsx
```tsx path="/examples/react/src/examples/EdgeRenderer/index.tsx"
import { MouseEvent, useCallback } from 'react';
import {
ReactFlow,
Controls,
Background,
MiniMap,
addEdge,
Connection,
Edge,
EdgeTypes,
MarkerType,
Node,
useEdgesState,
useNodesState,
} from '@xyflow/react';
import CustomEdge from './CustomEdge';
import CustomEdge2 from './CustomEdge2';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('dblclick', edge);
const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('enter', edge);
const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('move', edge);
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('leave', edge);
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Input 1' },
position: { x: 250, y: 0 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
{ id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } },
{ id: '2b', data: { label: 'Node 2b' }, position: { x: -40, y: 300 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } },
{ id: '3a', data: { label: 'Node 3a' }, position: { x: 150, y: 300 } },
{ id: '5', data: { label: 'Node 5' }, position: { x: 250, y: 400 } },
{
id: '6',
type: 'output',
data: { label: 'Output 6' },
position: { x: 50, y: 550 },
},
{
id: '7',
type: 'output',
data: { label: 'Output 7' },
position: { x: 250, y: 550 },
},
{
id: '8',
type: 'output',
data: { label: 'Output 8' },
position: { x: 525, y: 600 },
},
{
id: '9',
type: 'output',
data: { label: 'Output 9' },
position: { x: 675, y: 500 },
},
];
const initialEdges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
label: 'bezier edge (default)',
className: 'normal-edge',
},
{
id: 'e2-2a',
source: '2',
target: '2a',
type: 'smoothstep',
label: 'smoothstep edge',
},
{
id: 'e2a-2b',
source: '2a',
target: '2b',
type: 'simplebezier',
label: 'simple bezier edge',
},
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
{
id: 'e3-4',
source: '3',
target: '4',
type: 'straight',
label: 'straight edge',
},
{
id: 'e3-3a',
source: '3',
target: '3a',
type: 'straight',
label: 'label only edge',
style: { stroke: 'none' },
},
{
id: 'e3-5',
source: '4',
target: '5',
animated: true,
label: 'animated styled edge',
style: { stroke: 'red' },
},
{
id: 'e5-7',
source: '5',
target: '7',
label: 'label with styled bg',
labelBgPadding: [8, 4],
labelBgBorderRadius: 4,
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
markerEnd: {
type: MarkerType.ArrowClosed,
},
},
{
id: 'e5-8',
source: '5',
target: '8',
type: 'custom',
data: { text: 'custom edge' },
},
{
id: 'e5-9',
source: '5',
target: '9',
type: 'custom2',
data: { text: 'custom edge 2' },
},
{
id: 'e5-6',
source: '5',
target: '6',
label: (
<>
<tspan>i am using</tspan>
<tspan dy={10} x={0}>
{'<tspan>'}
</tspan>
</>
),
labelStyle: { fill: 'red', fontWeight: 700 },
style: { stroke: '#ffcc00' },
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2,
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
},
},
];
const edgeTypes: EdgeTypes = {
custom: CustomEdge,
custom2: CustomEdge2,
};
const EdgesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
snapToGrid={true}
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
};
export default EdgesFlow;
```
## /examples/react/src/examples/EdgeRouting/index.tsx
```tsx path="/examples/react/src/examples/EdgeRouting/index.tsx"
import { ReactFlow, Node, Position, MarkerType, BuiltInEdge } from '@xyflow/react';
const nodes: Node[] = [
// LTR
{
id: '1',
position: { x: 50, y: -100 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '2',
position: { x: -100, y: 0 },
data: { label: 'Target' },
sourcePosition: Position.Left,
targetPosition: Position.Left,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Right Right
{
id: '3',
position: { x: -100, y: 250 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '4',
position: { x: 50, y: 150 },
data: { label: 'Target' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Right Top
{
id: '5',
position: { x: -100, y: 450 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '6',
position: { x: 100, y: 400 },
data: { label: 'Target' },
sourcePosition: Position.Top,
targetPosition: Position.Top,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Right Bottom
{
id: '7',
position: { x: 100, y: 700 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '8',
position: { x: -100, y: 600 },
data: { label: 'Target' },
sourcePosition: Position.Bottom,
targetPosition: Position.Bottom,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Bend Position Examples (horizontal)
{
id: '9',
position: { x: 300, y: 0 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '10',
position: { x: 600, y: 150 },
data: { label: 'Target' },
sourcePosition: Position.Left,
targetPosition: Position.Left,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '11',
position: { x: 300, y: 300 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '12',
position: { x: 600, y: 450 },
data: { label: 'Target' },
sourcePosition: Position.Left,
targetPosition: Position.Left,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '13',
position: { x: 300, y: 600 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '14',
position: { x: 600, y: 750 },
data: { label: 'Target' },
sourcePosition: Position.Left,
targetPosition: Position.Left,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Bend Position Examples (vertical)
{
id: '15',
position: { x: 800, y: 0 },
data: { label: 'Source' },
sourcePosition: Position.Bottom,
targetPosition: Position.Bottom,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '16',
position: { x: 950, y: 150 },
data: { label: 'Target' },
sourcePosition: Position.Top,
targetPosition: Position.Top,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '17',
position: { x: 800, y: 300 },
data: { label: 'Source' },
sourcePosition: Position.Bottom,
targetPosition: Position.Bottom,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '18',
position: { x: 950, y: 450 },
data: { label: 'Target' },
sourcePosition: Position.Top,
targetPosition: Position.Top,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '19',
position: { x: 800, y: 600 },
data: { label: 'Source' },
sourcePosition: Position.Bottom,
targetPosition: Position.Bottom,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '20',
position: { x: 950, y: 750 },
data: { label: 'Target' },
sourcePosition: Position.Top,
targetPosition: Position.Top,
style: { background: 'rgba(255,255,255,0.5)' },
},
];
const edges: BuiltInEdge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
pathOptions: {
offset: 30,
},
interactionWidth: 0,
},
{
id: 'e3-4',
source: '3',
target: '4',
pathOptions: {
borderRadius: 2,
},
interactionWidth: 0,
},
{
id: 'e4-5',
source: '5',
target: '6',
},
{
id: 'e7-8',
source: '7',
target: '8',
},
// Bend Position Examples (horizontal)
{
id: 'e9-10',
source: '9',
target: '10',
label: 'stepPosition: 0.2',
pathOptions: {
stepPosition: 0.2,
},
interactionWidth: 0,
},
{
id: 'e11-12',
source: '11',
target: '12',
label: 'stepPosition: 0.5 (default)',
pathOptions: {
stepPosition: 0.5,
},
interactionWidth: 0,
},
{
id: 'e13-14',
source: '13',
target: '14',
label: 'stepPosition: 0.8',
pathOptions: {
stepPosition: 0.8,
},
interactionWidth: 0,
},
// Bend Position Examples (vertical)
{
id: 'e15-16',
source: '15',
target: '16',
label: 'stepPosition: 0.2',
pathOptions: {
stepPosition: 0.2,
},
interactionWidth: 0,
},
{
id: 'e17-18',
source: '17',
target: '18',
label: 'stepPosition: 0.5',
pathOptions: {
stepPosition: 0.5,
},
interactionWidth: 0,
},
{
id: 'e19-20',
source: '19',
target: '20',
label: 'stepPosition: 0.8',
pathOptions: {
stepPosition: 0.8,
},
interactionWidth: 0,
},
];
const defaultEdgeOptions = {
label: 'Edge Label',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
style: {
strokeWidth: 1,
},
};
function EdgeRouting() {
return <ReactFlow defaultNodes={nodes} defaultEdges={edges} fitView defaultEdgeOptions={defaultEdgeOptions} />;
}
export default EdgeRouting;
```
## /examples/react/src/examples/EdgeToolbar/CustomEdge.tsx
```tsx path="/examples/react/src/examples/EdgeToolbar/CustomEdge.tsx"
import { getBezierPath, BaseEdge, EdgeProps, useReactFlow, getStraightPath, getSmoothStepPath } from '@xyflow/react';
import { EdgeToolbar } from '@xyflow/react';
const getPath = (props: EdgeProps) => {
switch (props.data!.type) {
case 'smoothstep':
return getSmoothStepPath(props);
case 'straight':
return getStraightPath(props);
default:
return getBezierPath(props);
}
};
export function CustomEdge(props: EdgeProps) {
const [edgePath, centerX, centerY] = getPath(props);
const { setEdges } = useReactFlow();
const deleteEdge = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== props.id));
};
return (
<>
<BaseEdge id={props.id} path={edgePath} />
<EdgeToolbar
edgeId={props.id}
x={centerX + 0}
y={centerY + 0}
alignX={props.data?.align?.[0] ?? 'center'}
alignY={props.data?.align?.[1] ?? 'center'}
isVisible
>
<button style={{}} onClick={deleteEdge}>
Delete
</button>
</EdgeToolbar>
</>
);
}
```
## /examples/react/src/examples/EdgeToolbar/index.tsx
```tsx path="/examples/react/src/examples/EdgeToolbar/index.tsx"
import {
Background,
BackgroundVariant,
Controls,
Edge,
EdgeTypes,
MiniMap,
Node,
Position,
ReactFlow,
} from '@xyflow/react';
import { CustomEdge } from './CustomEdge';
const edgeTypes: EdgeTypes = {
custom: CustomEdge,
};
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1', toolbarPosition: Position.Top },
position: { x: 0, y: 0 },
className: 'react-flow__node-default',
},
{
id: '2',
data: { label: 'Node 2', toolbarPosition: Position.Top },
position: { x: 100, y: 150 },
className: 'react-flow__node-default',
},
{
id: '3',
data: { label: 'Node 3', toolbarPosition: Position.Top },
position: { x: 200, y: 0 },
className: 'react-flow__node-default',
},
];
const initialEdges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'custom',
data: { type: 'smoothstep', align: ['left', 'bottom'] },
},
{
id: 'e3-2',
source: '3',
target: '2',
type: 'custom',
data: { type: 'bezier', align: ['right', 'bottom'] },
},
{
id: 'e1-3',
source: '1',
target: '3',
type: 'custom',
data: { type: 'straight', align: ['center', 'center'] },
},
];
export default function EdgeToolbarExample() {
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
className="react-flow-edge-toolbar-example"
minZoom={0.2}
maxZoom={4}
fitView
edgeTypes={edgeTypes}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
</ReactFlow>
);
}
```
## /examples/react/src/examples/EdgeTypes/index.tsx
```tsx path="/examples/react/src/examples/EdgeTypes/index.tsx"
import { useCallback } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
addEdge,
ReactFlowInstance,
Connection,
Edge,
useNodesState,
useEdgesState,
} from '@xyflow/react';
import { getElements } from './utils';
const onInit = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getNodes());
};
const { nodes: initialNodes, edges: initialEdges } = getElements();
const multiSelectionKeyCode = ['ShiftLeft', 'ShiftRight'];
const deleteKeyCode = ['AltLeft+KeyD', 'Backspace'];
const EdgeTypesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={onInit}
onConnect={onConnect}
minZoom={0.2}
selectionKeyCode="a+s"
multiSelectionKeyCode={multiSelectionKeyCode}
deleteKeyCode={deleteKeyCode}
zoomActivationKeyCode="z"
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
};
export default EdgeTypesFlow;
```
## /examples/react/src/examples/EdgeTypes/utils.ts
```ts path="/examples/react/src/examples/EdgeTypes/utils.ts"
import { Edge, Node, Position } from '@xyflow/react';
const nodeWidth = 80;
const nodeGapWidth = nodeWidth * 2;
const nodeStyle = { width: nodeWidth, fontSize: 11, color: 'white' };
const sourceTargetPositions = [
{ source: Position.Bottom, target: Position.Top },
{ source: Position.Right, target: Position.Left },
];
const nodeColors = [
['#0c5956', '#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8', '#4fa6e0'],
];
const edgeTypes = ['default', 'step', 'smoothstep', 'straight', 'simplebezier'];
const offsets = [
{
x: 0,
y: -nodeGapWidth,
},
{
x: nodeGapWidth,
y: -nodeGapWidth,
},
{
x: nodeGapWidth,
y: 0,
},
{
x: nodeGapWidth,
y: nodeGapWidth,
},
{
x: 0,
y: nodeGapWidth,
},
{
x: -nodeGapWidth,
y: nodeGapWidth,
},
{
x: -nodeGapWidth,
y: 0,
},
{
x: -nodeGapWidth,
y: -nodeGapWidth,
},
];
let id = 0;
const getNodeId = (): string => (id++).toString();
export function getElements(): { nodes: Node[]; edges: Edge[] } {
const initialElements = { nodes: [] as Node[], edges: [] as Edge[] };
for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) {
const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex];
for (let edgeTypeIndex = 0; edgeTypeIndex < edgeTypes.length; edgeTypeIndex++) {
const currEdgeType = edgeTypes[edgeTypeIndex];
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
const currOffset = offsets[offsetIndex];
const style = {
...nodeStyle,
background: nodeColors[sourceTargetIndex][edgeTypeIndex],
};
const sourcePosition = {
x: offsetIndex * nodeWidth * 4,
y: edgeTypeIndex * 300 + sourceTargetIndex * edgeTypes.length * 300,
};
const sourceId = getNodeId();
const sourceData = { label: `Source ${sourceId}` };
const sourceNode: Node = {
id: sourceId,
style,
data: sourceData,
position: sourcePosition,
sourcePosition: currSourceTargetPos.source,
targetPosition: currSourceTargetPos.target,
};
const targetId = getNodeId();
const targetData = { label: `Target ${targetId}` };
const targetPosition = {
x: sourcePosition.x + currOffset.x,
y: sourcePosition.y + currOffset.y,
};
const targetNode: Node = {
id: targetId,
style,
data: targetData,
position: targetPosition,
sourcePosition: currSourceTargetPos.source,
targetPosition: currSourceTargetPos.target,
};
initialElements.nodes.push(sourceNode);
initialElements.nodes.push(targetNode);
initialElements.edges.push({
id: `${sourceId}-${targetId}`,
source: sourceId,
target: targetId,
type: currEdgeType,
} as Edge);
}
}
}
return initialElements;
}
```
## /examples/react/src/examples/Edges/CustomEdge.tsx
```tsx path="/examples/react/src/examples/Edges/CustomEdge.tsx"
import { FC } from 'react';
import { BaseEdge, EdgeProps, getBezierPath } from '@xyflow/react';
const CustomEdge: FC<EdgeProps> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
}) => {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<BaseEdge path={edgePath} id={id} />
<text>
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
{data.text}
</textPath>
</text>
</>
);
};
export default CustomEdge;
```
## /examples/react/src/examples/Edges/CustomEdge3.css
```css path="/examples/react/src/examples/Edges/CustomEdge3.css"
@keyframes react-flow-edge-dash {
from {
stroke-dashoffset: 100;
}
to {
stroke-dashoffset: 0;
}
}
.react-flow__edge-custom3 {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: react-flow-edge-dash 1s linear forwards;
}
```
## /examples/react/src/examples/FloatingEdges/style.module.css
```css path="/examples/react/src/examples/FloatingEdges/style.module.css"
.floatingedges {
flex-direction: column;
display: flex;
height: 100%;
}
.floatingedges :global .react-flow__handle {
opacity: 0;
}
```
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.