chore(svelte-examples): pull out of lib package (#3363)

* chore(svelte-examples): pull out of lib package

* chore(svelte-examples): cleanup

* chore(examples): add readme files
This commit is contained in:
Moritz Klack
2023-08-31 16:44:27 +02:00
committed by GitHub
parent 3da34c739e
commit dcc38794a6
197 changed files with 2003 additions and 1575 deletions
+24
View File
@@ -0,0 +1,24 @@
# 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?
+12
View File
@@ -0,0 +1,12 @@
# React Flow examples
This Vite app is used internally to test the library.
## Start local dev server
```sh
pnpm dev
```
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshotOnRunFailure: false,
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
screenshotOnRunFailure: false,
video: false,
},
});
@@ -0,0 +1,42 @@
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', []);
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;
};
@@ -0,0 +1,58 @@
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,
positionAbsolute: n.position,
...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', []);
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;
};
@@ -0,0 +1,68 @@
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;
}
@@ -0,0 +1,96 @@
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;
}
@@ -0,0 +1,79 @@
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;
}
@@ -0,0 +1,197 @@
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} />);
});
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');
});
});
@@ -0,0 +1,210 @@
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);
});
});
});
});
});
@@ -0,0 +1,63 @@
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');
});
});
});
});
@@ -0,0 +1,35 @@
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');
});
});
@@ -0,0 +1,312 @@
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;
}
@@ -0,0 +1,79 @@
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 {};
+170
View File
@@ -0,0 +1,170 @@
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 })
.wait(50)
.get('.react-flow__pane')
.trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 450, { button: 0 })
.wait(50)
.trigger('mouseup', 1000, 450, { button: 0, force: true });
cy.wait(200);
cy.get('.react-flow__node').eq(1).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: 500, y: 25 }).then(($el: any) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
// @TODO: why does this fail since react18?
// it('removes a node', () => {
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
// cy.get('html').type('{backspace}').wait(100);
// cy.get('.react-flow__node').should('have.length', 3);
// cy.get('.react-flow__edge').should('have.length', 1);
// });
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', 3);
});
// @TODO: why does this fail since react18?
// it('removes an edge', () => {
// cy.get('.react-flow__edge:first').click();
// cy.get('body').type('{backspace}');
// cy.get('.react-flow__edge').should('have.length', 1);
// });
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 {};
+83
View File
@@ -0,0 +1,83 @@
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 {};
@@ -0,0 +1,32 @@
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 {};
+44
View File
@@ -0,0 +1,44 @@
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 {};
+51
View File
@@ -0,0 +1,51 @@
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 {};
+34
View File
@@ -0,0 +1,34 @@
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 {};
@@ -0,0 +1,164 @@
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 {};
+77
View File
@@ -0,0 +1,77 @@
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 {};
@@ -0,0 +1,22 @@
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',
},
];
@@ -0,0 +1,65 @@
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} />;
}
export default ControlledFlow;
@@ -0,0 +1,60 @@
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 })
.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 {};
@@ -0,0 +1,20 @@
<!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>
@@ -0,0 +1,27 @@
///<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);
+25
View File
@@ -0,0 +1,25 @@
// ***********************************************************
// 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';
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
Cypress.on('uncaught:exception', (err) => {
if (resizeObserverLoopErrRe.test(err.message)) {
return false;
}
});
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<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>
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@xyflow/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",
"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": {
"@xyflow/react": "workspace:*",
"classcat": "^5.0.3",
"dagre": "^0.8.5",
"localforage": "^1.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"zustand": "^4.4.1"
},
"devDependencies": {
"@cypress/skip-test": "^2.6.1",
"@types/dagre": "^0.7.48",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "4.0.4",
"@vitejs/plugin-react-swc": "^3.3.2",
"cypress": "12.17.3",
"cypress-real-events": "1.10.0",
"start-server-and-test": "^1.14.0",
"typescript": "5.1.3",
"vite": "4.4.9"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+320
View File
@@ -0,0 +1,320 @@
import { useEffect, useState } from 'react';
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import CustomConnectionLine from '../examples/CustomConnectionLine';
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
import CustomNode from '../examples/CustomNode';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
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 NestedNodes from '../examples/NestedNodes';
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 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 UpdatableEdge from '../examples/UpdatableEdge';
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 useNodesInitialized from '../examples/UseNodesInit';
interface IRoute {
name: string;
path: string;
component: React.ComponentType;
}
const routes: IRoute[] = [
{
name: 'Basic',
path: '/',
component: Basic,
},
{
name: 'Backgrounds',
path: '/backgrounds',
component: Backgrounds,
},
{
name: 'Cancel Connection',
path: '/cancel-connection',
component: CancelConnection,
},
{
name: 'Controlled/Uncontrolled',
path: '/controlled-uncontrolled',
component: ControlledUncontrolled,
},
{
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 Nodes',
path: '/default-nodes',
component: DefaultNodes,
},
{
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: '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: 'Multi Flows',
path: '/multiflows',
component: MultiFlows,
},
{
name: 'Nested Nodes',
path: '/nested-nodes',
component: NestedNodes,
},
{
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: '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: 'Updatable Edge',
path: '/updatable-edge',
component: UpdatableEdge,
},
{
name: 'Update Node',
path: '/update-node',
component: UpdateNode,
},
{
name: 'useNodesInitialized',
path: '/use-nodes-initialized',
component: useNodesInitialized,
},
{
name: 'useOnSelectionChange',
path: '/use-on-selection-change',
component: UseOnSelectionChange,
},
{
name: 'useReactFlow',
path: '/usereactflow',
component: UseReactFlow,
},
{
name: 'useUpdateNodeInternals',
path: '/useupdatenodeinternals',
component: UseUpdateNodeInternals,
},
{
name: 'Validation',
path: '/validation',
component: Validation,
},
{
name: 'useKeyPress',
path: '/use-key-press',
component: UseKeyPress,
},
];
const Header = () => {
const location = useLocation();
const navigate = useNavigate();
const [currentPath, setCurrentPath] = useState(location.pathname);
useEffect(() => {
const name = routes.find((route) => route.path === currentPath)?.name;
document.title = `React Flow Examples${name ? ' - ' + name : ''}`;
navigate(currentPath);
}, [currentPath]);
return (
<header>
<a className="logo" href="https://github.com/wbkd/react-flow">
React Flow Dev
</a>
<select value={currentPath} onChange={(event) => setCurrentPath(event.target.value)}>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.name}
</option>
))}
</select>
</header>
);
};
export default () => (
<BrowserRouter>
<Header />
<Routes>
{routes.map((route) => (
<Route path={route.path} key={route.path} element={<route.component />} />
))}
</Routes>
</BrowserRouter>
);
@@ -0,0 +1,52 @@
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;
@@ -0,0 +1,13 @@
.wrapper {
display: flex;
height: 100%;
}
.wrapper :global .react-flow {
width: 100%;
height: 100%;
}
.wrapper :global .react-flow {
border-right: 1px solid #ddd;
}
+119
View File
@@ -0,0 +1,119 @@
import { MouseEvent } from 'react';
import {
ReactFlow,
MiniMap,
Background,
BackgroundVariant,
Controls,
ReactFlowProvider,
Node,
Edge,
useReactFlow,
Panel,
} from '@xyflow/react';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
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 BasicFlow = () => {
const instance = useReactFlow();
const updatePos = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
})
);
};
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.className = node.className === 'light' ? 'dark' : 'light';
return node;
})
);
};
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
fitView
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
>
<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>
</Panel>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<BasicFlow />
</ReactFlowProvider>
);
}
@@ -0,0 +1,31 @@
.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;
}
@@ -0,0 +1,28 @@
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>
)
}
@@ -0,0 +1,34 @@
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' },
];
@@ -0,0 +1,35 @@
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;
@@ -0,0 +1,59 @@
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>
);
@@ -0,0 +1,120 @@
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) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
})
);
};
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => {
edge.style = {
stroke: '#ff5050',
};
return edge;
})
);
};
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>
);
}
@@ -0,0 +1,18 @@
import { ConnectionLineComponentProps } from '@xyflow/react';
function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) {
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;
@@ -0,0 +1,45 @@
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}
>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default ConnectionLineFlow;
@@ -0,0 +1,75 @@
import { MouseEvent, CSSProperties, useCallback } from 'react';
import {
ReactFlow,
addEdge,
Background,
BackgroundVariant,
Connection,
Controls,
Edge,
MiniMap,
MiniMapNodeProps,
Node,
ReactFlowInstance,
useEdgesState,
useNodesState,
} 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 buttonStyle: CSSProperties = {
position: 'absolute',
left: 10,
top: 10,
zIndex: 4,
};
const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => (
<circle cx={x} cy={y} r={Math.max(width, height) / 2} fill={color} />
);
const CustomMiniMapNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => {
const nodeId = (nodes.length + 1).toString();
const newNode: Node = {
id: nodeId,
data: { label: `Node: ${nodeId}` },
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
setNodes((nds) => nds.concat(newNode));
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={(p) => onConnect(p)}
onNodeDragStop={onNodeDragStop}
onlyRenderVisibleElements={false}
>
<Controls />
<Background variant={BackgroundVariant.Lines} />
<MiniMap nodeComponent={CustomMiniMapNode} />
<button type="button" onClick={addRandomNode} style={buttonStyle}>
add node
</button>
</ReactFlow>
);
};
export default CustomMiniMapNodeFlow;
@@ -0,0 +1,47 @@
import React, { memo, FC, CSSProperties, useCallback } from 'react';
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
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);
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
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" 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);
@@ -0,0 +1,166 @@
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react';
import {
ReactFlow,
MiniMap,
Controls,
addEdge,
Node,
ReactFlowInstance,
Position,
SnapGrid,
Connection,
useNodesState,
useEdgesState,
Background,
} from '@xyflow/react';
import ColorSelectorNode from './ColorSelectorNode';
const onInit = (reactFlowInstance: ReactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance);
};
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setNodes((nds) =>
nds.map((node) => {
if (node.id !== '2') {
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 = useCallback(
(connection: Connection) =>
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
[setEdges]
);
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}
>
<MiniMap
nodeStrokeColor={(n: Node): string => {
if (n.type === 'input') return '#0041d0';
if (n.type === 'selectorNode') return bgColor;
if (n.type === 'output') return '#ff0072';
return '#eee';
}}
nodeColor={(n: Node): string => {
if (n.type === 'selectorNode') return bgColor;
return '#fff';
}}
/>
<Controls />
<Background bgColor={bgColor} />
</ReactFlow>
);
};
export default CustomNodeFlow;
@@ -0,0 +1,100 @@
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',
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,
};
return node;
})
);
};
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => {
edge.style = {
stroke: '#ff5050',
};
return edge;
})
);
};
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>
);
}
@@ -0,0 +1,32 @@
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);
@@ -0,0 +1,40 @@
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: null,
},
];
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}
/>
);
};
export default DragHandleFlow;
@@ -0,0 +1,35 @@
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;
@@ -0,0 +1,37 @@
.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;
}
}
@@ -0,0 +1,91 @@
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([]);
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.project({
x: event.clientX,
y: event.clientY - 40,
});
const newNode: Node = {
id: getId(),
type,
position,
data: { label: `${type} node` },
};
setNodes((nds) => nds.concat(newNode));
}
};
return (
<div className={styles.dndflow}>
<ReactFlowProvider>
<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;
@@ -0,0 +1,19 @@
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;
@@ -0,0 +1,37 @@
import { Handle, NodeProps, Position, ReactFlowState, useStore } from '@xyflow/react';
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionStartHandle?.nodeId;
export default function CustomNode({ id }: NodeProps) {
const connectionNodeId = useStore(connectionNodeIdSelector);
const isConnecting = !!connectionNodeId;
const isTarget = connectionNodeId && connectionNodeId !== id;
const targetHandleStyle = { zIndex: isTarget ? 3 : 1 };
const label = isTarget ? 'Drop here' : 'Drag to connect';
return (
<div className="customNode">
<div
className="customNodeBody"
style={{
borderStyle: isTarget ? 'dashed' : 'solid',
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
}}
>
{!isConnecting && (
<Handle className="customHandle" style={{ zIndex: 2 }} position={Position.Right} type="source" />
)}
<Handle
className="customHandle"
style={targetHandleStyle}
position={Position.Left}
type="target"
isConnectableStart={false}
/>
{label}
</div>
</div>
);
}
@@ -0,0 +1,26 @@
import { useCallback } from 'react';
import { useStore, getStraightPath, EdgeProps } from '@xyflow/react';
import { getEdgeParams } from './utils.js';
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
const sourceNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === source), [source]));
const targetNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === target), [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;
@@ -0,0 +1,85 @@
import { useCallback } from 'react';
import { ReactFlow, Node, Edge, addEdge, useNodesState, useEdgesState, MarkerType, OnConnect } from '@xyflow/react';
import CustomNode from './CustomNode';
import FloatingEdge from './FloatingEdge';
import CustomConnectionLine from './CustomConnectionLine';
import '@xyflow/react/dist/style.css';
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;
@@ -0,0 +1,42 @@
.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;
}
@@ -0,0 +1,102 @@
import { Node, Position, MarkerType, XYPosition } 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: Node, targetNode: Node) {
// 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,
positionAbsolute: intersectionNodePosition,
} = intersectionNode;
const targetPosition = targetNode.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));
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: Node, intersectionPoint: XYPosition) {
const n = { ...node.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.width! - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y! + n.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: Node, target: Node) {
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 };
}
@@ -0,0 +1,61 @@
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;
@@ -0,0 +1,51 @@
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;
@@ -0,0 +1,207 @@
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;
@@ -0,0 +1,122 @@
import { ReactFlow, Node, Edge, Position, MarkerType } 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)' },
},
];
const edges: Edge[] = [
{
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',
},
];
const defaultEdgeOptions = {
label: 'Edge Label',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
style: {
strokeWidth: 1,
},
};
const SimpleEdge = () => {
return <ReactFlow defaultNodes={nodes} defaultEdges={edges} fitView defaultEdgeOptions={defaultEdgeOptions} />;
};
export default SimpleEdge;
@@ -0,0 +1,54 @@
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;
@@ -0,0 +1,114 @@
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;
}
@@ -0,0 +1,35 @@
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;
@@ -0,0 +1,41 @@
import { FC } from 'react';
import { EdgeProps, getBezierPath, EdgeText, BaseEdge } from '@xyflow/react';
const CustomEdge: FC<EdgeProps> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
}) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<BaseEdge id={id} path={edgePath} />
<EdgeText
x={labelX}
y={labelY}
label={data.text}
labelStyle={{ fill: 'white' }}
labelShowBg
labelBgStyle={{ fill: 'red' }}
labelBgPadding={[2, 4]}
labelBgBorderRadius={2}
onClick={() => console.log(data)}
/>
</>
);
};
export default CustomEdge;
+207
View File
@@ -0,0 +1,207 @@
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;
@@ -0,0 +1,68 @@
import { MouseEvent, CSSProperties, useCallback } from 'react';
import {
ReactFlow,
Background,
BackgroundVariant,
Controls,
addEdge,
Node,
Connection,
Edge,
useNodesState,
useEdgesState,
ReactFlowInstance,
} 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 buttonStyle: CSSProperties = {
position: 'absolute',
left: 10,
top: 10,
zIndex: 4,
};
const EmptyFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => {
const nodeId = (nodes.length + 1).toString();
const newNode: Node = {
id: nodeId,
data: { label: `Node: ${nodeId}` },
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
setNodes((nds) => nds.concat(newNode));
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={(p) => onConnect(p)}
onNodeDragStop={onNodeDragStop}
onlyRenderVisibleElements={false}
>
<Controls />
<Background variant={BackgroundVariant.Lines} />
<button type="button" onClick={addRandomNode} style={buttonStyle}>
add node
</button>
</ReactFlow>
);
};
export default EmptyFlow;
@@ -0,0 +1,56 @@
import { ReactFlow, Background, BackgroundVariant, Node, Edge, SelectionMode, Controls, Panel } from '@xyflow/react';
const MULTI_SELECT_KEY = ['Meta', 'Shift'];
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 onPaneContextMenu = (e: any) => {
e.preventDefault();
console.log('context menu');
};
const panOnDrag = [1, 2];
const onMoveStart = (e: any) => console.log('move start', e);
const onMove = (e: any) => console.log('move', e);
const onMoveEnd = (e: any) => console.log('move end', e);
const BasicFlow = () => {
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
selectionOnDrag
selectionMode={SelectionMode.Partial}
panOnDrag={panOnDrag}
panOnScroll
zoomActivationKeyCode="Meta"
multiSelectionKeyCode={MULTI_SELECT_KEY}
onPaneContextMenu={onPaneContextMenu}
fitView
selectNodesOnDrag={false}
onSelectionContextMenu={onPaneContextMenu}
onMoveStart={onMoveStart}
onMove={onMove}
onMoveEnd={onMoveEnd}
>
<Background variant={BackgroundVariant.Cross} />
<Controls />
<Panel position="top-right">
<input type={'text'} placeholder={'name'} />
</Panel>
</ReactFlow>
);
};
export default BasicFlow;
@@ -0,0 +1,37 @@
import { FC } from 'react';
import { getBezierPath, ConnectionLineComponentProps, Node } from '@xyflow/react';
import { getEdgeParams } from './utils';
const FloatingConnectionLine: FC<ConnectionLineComponentProps> = ({ toX, toY, fromPosition, toPosition, fromNode }) => {
if (!fromNode) {
return null;
}
const targetNode = {
id: 'connection-target',
width: 1,
height: 1,
position: { x: toX, y: toY },
} as Node;
const { sx, sy } = getEdgeParams(fromNode, targetNode);
const [path] = getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: fromPosition,
targetPosition: toPosition,
targetX: toX,
targetY: toY,
});
return (
<g>
<path fill="none" stroke="#222" strokeWidth={1.5} className="animated" d={path} />
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</g>
);
};
export default FloatingConnectionLine;
@@ -0,0 +1,36 @@
import { FC, CSSProperties } from 'react';
import { EdgeProps, useStore, getBezierPath } from '@xyflow/react';
import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const { sourceNode, targetNode } = useStore((s) => {
const sourceNode = s.nodes.find((n) => n.id === source);
const targetNode = s.nodes.find((n) => n.id === target);
return { sourceNode, targetNode };
});
if (!sourceNode || !targetNode) {
return null;
}
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const [path] = getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
targetPosition: targetPos,
targetX: tx,
targetY: ty,
});
return (
<g className="react-flow__connection">
<path id={id} className="react-flow__edge-path" d={path} style={style as CSSProperties} />
</g>
);
};
export default FloatingEdge;
@@ -0,0 +1,54 @@
import { useCallback } from 'react';
import {
ReactFlow,
Background,
addEdge,
ReactFlowInstance,
EdgeTypes,
Connection,
useNodesState,
useEdgesState,
} from '@xyflow/react';
import styles from './style.module.css';
import FloatingConnectionLine from './FloatingConnectionLine';
import FloatingEdge from './FloatingEdge';
import { createElements } from './utils';
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const { nodes: initialNodes, edges: initialEdges } =
typeof window !== 'undefined' ? createElements() : { nodes: [], edges: [] };
const edgeTypes: EdgeTypes = {
floating: FloatingEdge,
};
const FloatingEdges = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
return (
<div className={styles.floatingedges}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={onInit}
edgeTypes={edgeTypes}
connectionLineComponent={FloatingConnectionLine}
>
<Background />
</ReactFlow>
</div>
);
};
export default FloatingEdges;
@@ -0,0 +1,9 @@
.floatingedges {
flex-direction: column;
display: flex;
height: 100%;
}
.floatingedges :global .react-flow__handle {
opacity: 0;
}
@@ -0,0 +1,106 @@
import { Position, XYPosition, Node, Edge } 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: Node, targetNode: Node): XYPosition {
// 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,
position: intersectionNodePosition,
} = intersectionNode;
const targetPosition = targetNode.position;
const w = (intersectionNodeWidth ?? 0) / 2;
const h = (intersectionNodeHeight ?? 0) / 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));
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: Node, intersectionPoint: XYPosition) {
const n = { ...node.position, ...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.width ?? 0) - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y + (n.height ?? 0) - 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: Node, target: Node) {
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,
};
}
type NodesAndEdges = {
nodes: Node[];
edges: Edge[];
};
export function createElements(): NodesAndEdges {
const nodes: Node[] = [];
const edges: Edge[] = [];
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',
});
}
return { nodes, edges };
}
@@ -0,0 +1,102 @@
import { useState, useEffect, useCallback } from 'react';
import {
ReactFlow,
addEdge,
Connection,
Edge,
Node,
useNodesState,
useEdgesState,
MiniMap,
Controls,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
hidden: true,
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{
id: '2',
hidden: true,
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
{
id: '3',
hidden: true,
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
},
{
id: '4',
hidden: true,
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4' },
];
const setHidden = (hidden: boolean) => (els: any[]) =>
els.map((e: any) => {
e.hidden = hidden;
return e;
});
const HiddenFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [isHidden, setIsHidden] = useState<boolean>(true);
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
},
[setEdges]
);
useEffect(() => {
setNodes(setHidden(isHidden));
setEdges(setHidden(isHidden));
}, [isHidden, setEdges, setNodes]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<MiniMap />
<Controls />
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
<div>
<label htmlFor="ishidden">
isHidden
<input
id="ishidden"
type="checkbox"
checked={isHidden}
onChange={(event) => setIsHidden(event.target.checked)}
className="react-flow__ishidden"
/>
</label>
</div>
</div>
</ReactFlow>
);
};
export default HiddenFlow;
@@ -0,0 +1,241 @@
import { useState, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
import {
ReactFlow,
addEdge,
Node,
Connection,
Edge,
PanOnScrollMode,
Viewport,
useNodesState,
useEdgesState,
Controls,
MiniMap,
} from '@xyflow/react';
const initialNodes: 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 initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport);
const InteractionFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
const [isSelectable, setIsSelectable] = useState<boolean>(false);
const [isDraggable, setIsDraggable] = useState<boolean>(false);
const [isConnectable, setIsConnectable] = useState<boolean>(false);
const [zoomOnScroll, setZoomOnScroll] = useState<boolean>(false);
const [zoomOnPinch, setZoomOnPinch] = useState<boolean>(false);
const [panOnScroll, setPanOnScroll] = useState<boolean>(false);
const [panOnScrollMode, setPanOnScrollMode] = useState<PanOnScrollMode>(PanOnScrollMode.Free);
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState<boolean>(false);
const [panOnDrag, setPanOnDrag] = useState<boolean>(true);
const [captureZoomClick, setCaptureZoomClick] = useState<boolean>(false);
const [captureZoomScroll, setCaptureZoomScroll] = useState<boolean>(false);
const [captureElementClick, setCaptureElementClick] = useState<boolean>(false);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
elementsSelectable={isSelectable}
nodesConnectable={isConnectable}
nodesDraggable={isDraggable}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
panOnScroll={panOnScroll}
panOnScrollMode={panOnScrollMode}
zoomOnDoubleClick={zoomOnDoubleClick}
onConnect={onConnect}
onNodeClick={captureElementClick ? onNodeClick : undefined}
onEdgeClick={captureElementClick ? onEdgeClick : undefined}
onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
panOnDrag={panOnDrag}
onPaneClick={captureZoomClick ? onPaneClick : undefined}
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
onMoveEnd={onMoveEnd}
>
<MiniMap />
<Controls />
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
<div>
<label htmlFor="draggable">
nodesDraggable
<input
id="draggable"
type="checkbox"
checked={isDraggable}
onChange={(event) => setIsDraggable(event.target.checked)}
className="react-flow__draggable"
/>
</label>
</div>
<div>
<label htmlFor="connectable">
nodesConnectable
<input
id="connectable"
type="checkbox"
checked={isConnectable}
onChange={(event) => setIsConnectable(event.target.checked)}
className="react-flow__connectable"
/>
</label>
</div>
<div>
<label htmlFor="selectable">
elementsSelectable
<input
id="selectable"
type="checkbox"
checked={isSelectable}
onChange={(event) => setIsSelectable(event.target.checked)}
className="react-flow__selectable"
/>
</label>
</div>
<div>
<label htmlFor="zoomonscroll">
zoomOnScroll
<input
id="zoomonscroll"
type="checkbox"
checked={zoomOnScroll}
onChange={(event) => setZoomOnScroll(event.target.checked)}
className="react-flow__zoomonscroll"
/>
</label>
</div>
<div>
<label htmlFor="zoomonpinch">
zoomOnPinch
<input
id="zoomonpinch"
type="checkbox"
checked={zoomOnPinch}
onChange={(event) => setZoomOnPinch(event.target.checked)}
className="react-flow__zoomonpinch"
/>
</label>
</div>
<div>
<label htmlFor="panonscroll">
panOnScroll
<input
id="panonscroll"
type="checkbox"
checked={panOnScroll}
onChange={(event) => setPanOnScroll(event.target.checked)}
className="react-flow__panonscroll"
/>
</label>
</div>
<div>
<label htmlFor="panonscrollmode">
panOnScrollMode
<select
id="panonscrollmode"
value={panOnScrollMode}
onChange={(event) => setPanOnScrollMode(event.target.value as PanOnScrollMode)}
className="react-flow__panonscrollmode"
>
<option value="free">free</option>
<option value="horizontal">horizontal</option>
<option value="vertical">vertical</option>
</select>
</label>
</div>
<div>
<label htmlFor="zoomondbl">
zoomOnDoubleClick
<input
id="zoomondbl"
type="checkbox"
checked={zoomOnDoubleClick}
onChange={(event) => setZoomOnDoubleClick(event.target.checked)}
className="react-flow__zoomondbl"
/>
</label>
</div>
<div>
<label htmlFor="panondrag">
panOnDrag
<input
id="panondrag"
type="checkbox"
checked={panOnDrag}
onChange={(event) => setPanOnDrag(event.target.checked)}
className="react-flow__panondrag"
/>
</label>
</div>
<div>
<label htmlFor="capturezoompaneclick">
capture onPaneClick
<input
id="capturezoompaneclick"
type="checkbox"
checked={captureZoomClick}
onChange={(event) => setCaptureZoomClick(event.target.checked)}
className="react-flow__capturezoompaneclick"
/>
</label>
</div>
<div>
<label htmlFor="capturezoompanescroll">
capture onPaneScroll
<input
id="capturezoompanescroll"
type="checkbox"
checked={captureZoomScroll}
onChange={(event) => setCaptureZoomScroll(event.target.checked)}
className="react-flow__capturezoompanescroll"
/>
</label>
</div>
<div>
<label htmlFor="captureelementclick">
capture onElementClick
<input
id="captureelementclick"
type="checkbox"
checked={captureElementClick}
onChange={(event) => setCaptureElementClick(event.target.checked)}
className="react-flow__captureelementclick"
/>
</label>
</div>
</div>
</ReactFlow>
);
};
export default InteractionFlow;
@@ -0,0 +1,173 @@
import { MouseEvent, useCallback, useState } from 'react';
import {
ReactFlow,
MiniMap,
Background,
BackgroundVariant,
Controls,
ReactFlowProvider,
Node,
Edge,
useReactFlow,
XYPosition,
} from '@xyflow/react';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 0, y: 200 },
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 200, y: 0 },
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 1000, y: 0 },
},
{
id: '5',
data: { label: 'Node 5' },
position: { x: 1000, y: 200 },
},
{
id: '6',
data: { label: 'Node 6' },
position: { x: 800, y: 0 },
},
{
id: '7',
data: { label: 'Node 4' },
position: { x: 0, y: 1000 },
},
{
id: '8',
data: { label: 'Node 5' },
position: { x: 0, y: 800 },
},
{
id: '9',
data: { label: 'Node 6' },
position: { x: 200, y: 1000 },
},
{
id: '10',
data: { label: 'Node 4' },
position: { x: 1000, y: 1000 },
},
{
id: '11',
data: { label: 'Node 5' },
position: { x: 800, y: 1000 },
},
{
id: '12',
data: { label: 'Node 6' },
position: { x: 1000, y: 800 },
},
];
const initialEdges: Edge[] = [];
const defaultEdgeOptions = { zIndex: 0 };
const BasicFlow = () => {
const instance = useReactFlow();
const [inverse, setInverse] = useState(false);
const updatePos = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
})
);
};
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleInverse = () => setInverse(!inverse);
const toggleClassnames = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.className = node.className === 'light' ? 'dark' : 'light';
return node;
})
);
};
const onMiniMapClick = useCallback((event: MouseEvent, pos: XYPosition) => {
console.log(pos);
}, []);
const onMiniMapNodeClick = useCallback((event: MouseEvent, node: Node) => {
console.log(node);
}, []);
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
fitView
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap onClick={onMiniMapClick} onNodeClick={onMiniMapNodeClick} pannable zoomable inversePan={inverse} />
<Controls />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button onClick={logToObject} style={{ marginRight: 5 }}>
toObject
</button>
<button onClick={toggleInverse} style={{ marginRight: 5 }}>
{inverse ? 'un-inverse pan' : 'inverse pan'}
</button>
</div>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<BasicFlow />
</ReactFlowProvider>
);
}
@@ -0,0 +1,105 @@
import { useCallback, MouseEvent } from 'react';
import {
ReactFlow,
MiniMap,
Background,
Controls,
ReactFlowProvider,
Node,
Edge,
useReactFlow,
useNodesState,
} from '@xyflow/react';
import './style.css';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
className: 'light',
style: {
width: 200,
height: 100,
},
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 0, y: 150 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 250, y: 0 },
className: 'light',
},
{
id: '4',
data: { label: 'Node' },
position: { x: 350, y: 150 },
style: {
width: 50,
height: 50,
},
className: 'light',
},
];
const initialEdges: Edge[] = [];
const defaultEdgeOptions = { zIndex: 0 };
const BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const { getIntersectingNodes, isNodeIntersecting } = useReactFlow();
const onNodeDrag = useCallback((_: MouseEvent, node: Node) => {
const intersections = getIntersectingNodes(node).map((n) => n.id);
const isIntersecting = isNodeIntersecting(node, { x: 0, y: 0, width: 100, height: 100 });
console.log(isIntersecting);
setNodes((ns) =>
ns.map((n) => ({
...n,
className: intersections.includes(n.id) ? 'highlight' : '',
}))
);
}, []);
return (
<ReactFlow
nodes={nodes}
edges={initialEdges}
onNodesChange={onNodesChange}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
fitView
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
>
<Background />
<MiniMap />
<Controls />
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<BasicFlow />
</ReactFlowProvider>
);
}
@@ -0,0 +1,4 @@
.react-flow__node.highlight {
background-color: #ff5050;
color: white;
}
@@ -0,0 +1,118 @@
import { useCallback } from 'react';
import dagre from 'dagre';
import {
ReactFlow,
Controls,
ReactFlowProvider,
addEdge,
Connection,
CoordinateExtent,
Position,
useNodesState,
useEdgesState,
MarkerType,
EdgeMarker,
Panel,
useReactFlow,
} from '@xyflow/react';
import initialItems from './initial-elements';
import styles from './layouting.module.css';
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeExtent: CoordinateExtent = [
[0, 0],
[1000, 1000],
];
const LayoutFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialItems.nodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialItems.edges);
const { fitView } = useReactFlow();
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
},
[setEdges]
);
const onLayout = (direction: string) => {
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: 150, height: 50 });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
node.position = {
x: nodeWithPosition.x + Math.random() / 1000,
y: nodeWithPosition.y,
};
return node;
});
setNodes(layoutedNodes);
};
const unselect = () => {
setNodes((nds) => nds.map((n) => ({ ...n, selected: false })));
};
const changeMarker = () => {
setEdges((eds) =>
eds.map((e) => ({
...e,
markerEnd: {
type: (e.markerEnd as EdgeMarker)?.type === MarkerType.Arrow ? MarkerType.ArrowClosed : MarkerType.Arrow,
},
}))
);
};
return (
<div className={styles.layoutflow}>
<ReactFlow
nodes={nodes}
edges={edges}
onConnect={onConnect}
nodeExtent={nodeExtent}
onInit={() => onLayout('TB')}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<Controls />
</ReactFlow>
<Panel position="top-right">
<button onClick={() => onLayout('TB')}>vertical layout</button>
<button onClick={() => onLayout('LR')}>horizontal layout</button>
<button onClick={() => unselect()}>unselect nodes</button>
<button onClick={() => changeMarker()}>change marker</button>
<button onClick={() => fitView()}>fitView</button>
<button onClick={() => fitView({ nodes: nodes.slice(0, 2) })}>fitView partially</button>
</Panel>
</div>
);
};
export default () => (
<ReactFlowProvider>
<LayoutFlow />
</ReactFlowProvider>
);
@@ -0,0 +1,135 @@
import { Node, Edge, XYPosition, MarkerType } from '@xyflow/react';
const position: XYPosition = { x: 0, y: 0 };
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position,
},
{
id: '2',
data: { label: 'node 2' },
position,
},
{
id: '2a',
data: { label: 'node 2a' },
position,
},
{
id: '2b',
data: { label: 'node 2b' },
position,
},
{
id: '2c',
data: { label: 'node 2c' },
position,
},
{
id: '2d',
data: { label: 'node 2d' },
position,
},
{
id: '3',
data: { label: 'node 3' },
position,
},
{
id: '4',
data: { label: 'node 4' },
position,
},
{
id: '5',
data: { label: 'node 5' },
position,
},
{
id: '6',
type: 'output',
data: { label: 'output' },
position,
},
{
id: '7',
type: 'output',
data: { label: 'output' },
position: { x: 400, y: 450 },
},
];
const edges: Edge[] = [
{
id: 'e12',
source: '1',
target: '2',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e13',
source: '1',
target: '3',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e22a',
source: '2',
target: '2a',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e22b',
source: '2',
target: '2b',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e22c',
source: '2',
target: '2c',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e2c2d',
source: '2c',
target: '2d',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e45',
source: '4',
target: '5',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e56',
source: '5',
target: '6',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e57',
source: '5',
target: '7',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
];
const nodesAndEdges = { nodes, edges };
export default nodesAndEdges;
@@ -0,0 +1,4 @@
.layoutflow {
flex-grow: 1;
position: relative;
}
@@ -0,0 +1,87 @@
import { FC } from 'react';
import {
ReactFlow,
Background,
addEdge,
Node,
Edge,
Connection,
ReactFlowProvider,
useNodesState,
useEdgesState,
MarkerType,
MiniMap,
} from '@xyflow/react';
import styles from './multiflows.module.css';
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,
markerEnd: { type: MarkerType.Arrow },
},
{ id: 'e1-3', source: '1', target: '3' },
];
const Flow: FC<{ id: string }> = ({ id }) => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
id={id}
>
<Background />
<MiniMap />
</ReactFlow>
</ReactFlowProvider>
);
};
const MultiFlows: FC = () => (
<div className={styles.multiflows}>
<Flow id="flow-a" />
<Flow id="flow-b" />
</div>
);
export default MultiFlows;
@@ -0,0 +1,13 @@
.multiflows {
display: flex;
height: 100%;
}
.multiflows :global .react-flow {
width: 100%;
height: 100%;
}
.multiflows :global .react-flow:first-child {
border-right: 2px solid #333;
}
@@ -0,0 +1,187 @@
import { useState, MouseEvent, useCallback } from 'react';
import {
ReactFlow,
Controls,
MiniMap,
Background,
addEdge,
useNodesState,
useEdgesState,
Node,
Edge,
ReactFlowInstance,
Connection,
} from '@xyflow/react';
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 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',
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
},
{
id: '2a',
data: { label: 'Node 2a' },
position: { x: 10, y: 50 },
parentNode: '2',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 320, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 320, y: 200 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 65 },
className: 'light',
parentNode: '4',
extent: 'parent',
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 15, y: 120 },
className: 'light',
style: {
backgroundColor: 'rgba(255, 0, 255, 0.7)',
height: 150,
width: 270,
},
parentNode: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 20, y: 40 },
className: 'light',
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 100, y: 100 },
className: 'light',
parentNode: '4b',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2a-4a', source: '2a', target: '4a' },
{ id: 'e3-4', source: '3', target: '4' },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
},
[setEdges]
);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
>
<MiniMap pannable />
<Controls />
<Background />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default NestedFlow;
@@ -0,0 +1,36 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps, NodeResizeControl } from '@xyflow/react';
import ResizeIcon from './ResizeIcon';
const controlStyle = {
background: 'transparent',
border: 'none',
};
const CustomResizerNode: FC<NodeProps> = ({ data }) => {
return (
<>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
style={controlStyle}
>
<ResizeIcon />
</NodeResizeControl>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomResizerNode);
@@ -0,0 +1,26 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps, NodeResizer } from '@xyflow/react';
const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
return (
<>
<NodeResizer
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
isVisible={data.isVisible ?? selected}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
/>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(DefaultResizerNode);
@@ -0,0 +1,40 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps, NodeResizeControl } from '@xyflow/react';
const HorizontalResizerNode: FC<NodeProps> = ({ data }) => {
return (
<>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Left}
/>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Right}
/>
<Handle type="target" position={Position.Top} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Bottom} />
</>
);
};
export default memo(HorizontalResizerNode);
@@ -0,0 +1,24 @@
function ResizeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="8"
height="8"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={{ position: 'absolute', right: 2, bottom: 2 }}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="16 20 20 20 20 16" />
<line x1="14" y1="14" x2="20" y2="20" />
<polyline points="8 4 4 4 4 8" />
<line x1="4" y1="4" x2="10" y2="10" />
</svg>
);
}
export default ResizeIcon;
@@ -0,0 +1,40 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps, NodeResizeControl } from '@xyflow/react';
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Top}
/>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Bottom}
/>
<Handle type="target" position={Position.Left} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomNode);
@@ -0,0 +1,154 @@
import { useCallback, useState } from 'react';
import {
ReactFlow,
Controls,
addEdge,
Connection,
useNodesState,
useEdgesState,
Panel,
Node,
Edge,
} from '@xyflow/react';
import DefaultResizer from './DefaultResizer';
import CustomResizer from './CustomResizer';
import VerticalResizer from './VerticalResizer';
import HorizontalResizer from './HorizontalResizer';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
defaultResizer: DefaultResizer,
customResizer: CustomResizer,
verticalResizer: VerticalResizer,
horizontalResizer: HorizontalResizer,
};
const nodeStyle = {
border: '1px solid #222',
fontSize: 10,
backgroundColor: '#ddd',
};
const initialEdges: Edge[] = [];
const initialNodes: Node[] = [
{
id: '1',
type: 'defaultResizer',
data: { label: 'default resizer' },
position: { x: 0, y: 0 },
style: { ...nodeStyle },
},
{
id: '1a',
type: 'defaultResizer',
data: {
label: 'default resizer with min and max dimensions',
minWidth: 100,
minHeight: 80,
maxWidth: 200,
maxHeight: 200,
},
position: { x: 0, y: 60 },
style: { ...nodeStyle, width: 100, height: 80 },
},
{
id: '1b',
type: 'defaultResizer',
data: {
label: 'default resizer with initial size and aspect ratio',
keepAspectRatio: true,
minWidth: 100,
minHeight: 60,
maxWidth: 400,
maxHeight: 400,
},
position: { x: 250, y: 0 },
style: {
width: 174,
height: 123,
...nodeStyle,
},
},
{
id: '2',
type: 'customResizer',
data: { label: 'custom resize icon' },
position: { x: 0, y: 200 },
style: { width: 100, height: 60, ...nodeStyle },
},
{
id: '3',
type: 'verticalResizer',
data: { label: 'vertical resizer' },
position: { x: 250, y: 200 },
style: { ...nodeStyle },
},
{
id: '3a',
type: 'verticalResizer',
data: {
label: 'vertical resizer with min/maxHeight and aspect ratio',
minHeight: 50,
maxHeight: 200,
keepAspectRatio: true,
},
position: { x: 400, y: 200 },
style: { ...nodeStyle, height: 50 },
},
{
id: '4',
type: 'horizontalResizer',
data: {
label: 'horizontal resizer with aspect ratio',
keepAspectRatio: true,
minHeight: 20,
maxHeight: 80,
maxWidth: 300,
},
position: { x: 250, y: 300 },
style: { ...nodeStyle },
},
{
id: '4a',
type: 'horizontalResizer',
data: { label: 'horizontal resizer with maxWidth', maxWidth: 300 },
position: { x: 250, y: 400 },
style: { ...nodeStyle },
},
];
const CustomNodeFlow = () => {
const [snapToGrid, setSnapToGrid] = useState(false);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge({ ...connection }, eds)),
[setEdges]
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
minZoom={0.2}
maxZoom={5}
snapToGrid={snapToGrid}
fitView
>
<Controls />
<Panel position="bottom-right">
<button onClick={() => setSnapToGrid((s) => !s)}>snapToGrid: {snapToGrid ? 'on' : 'off'}</button>
</Panel>
</ReactFlow>
);
};
export default CustomNodeFlow;
@@ -0,0 +1,19 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps, NodeToolbar } from '@xyflow/react';
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition} align={data.toolbarAlign}>
<button>delete</button>
<button>copy</button>
<button>expand</button>
</NodeToolbar>
<div>{data.label}</div>
<Handle type="target" position={Position.Left} />
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomNode);
@@ -0,0 +1,15 @@
import { NodeToolbar, ReactFlowState, useStore } from '@xyflow/react';
const selectedNodesSelector = (state: ReactFlowState) =>
state.nodes.filter((node) => node.selected).map((node) => node.id);
export default function SelectedNodesToolbar() {
const selectedNodeIds = useStore(selectedNodesSelector);
const isVisible = selectedNodeIds.length > 1;
return (
<NodeToolbar nodeId={selectedNodeIds} isVisible={isVisible}>
<button>Selection action</button>
</NodeToolbar>
);
}
@@ -0,0 +1,76 @@
import {
ReactFlow,
MiniMap,
Background,
BackgroundVariant,
Controls,
Node,
Edge,
NodeTypes,
Position,
NodeOrigin,
} from '@xyflow/react';
import CustomNode from './CustomNode';
import SelectedNodesToolbar from './SelectedNodesToolbar';
const nodeTypes: NodeTypes = {
custom: CustomNode,
};
const positions = ['top', 'right', 'bottom', 'left'];
const alignments = ['start', 'center', 'end'];
const initialNodes: Node[] = [
{
id: '4',
type: 'custom',
data: { label: 'toolbar top', toolbarPosition: Position.Top },
position: { x: 0, y: -200 },
className: 'react-flow__node-default',
},
];
positions.forEach((position, posIndex) => {
alignments.forEach((align, alignIndex) => {
const id = `node-${align}-${position}`;
initialNodes.push({
id,
type: 'custom',
data: {
label: `toolbar ${position} ${align}`,
toolbarPosition: position as Position,
toolbarAlign: align,
toolbarVisible: true,
},
className: 'react-flow__node-default',
position: { x: posIndex * 300, y: alignIndex * 100 },
});
});
});
const initialEdges: Edge[] = [];
const defaultEdgeOptions = { zIndex: 0 };
const nodeOrigin: NodeOrigin = [0.5, 0.5];
export default function NodeToolbarExample() {
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
className="react-flow-node-toolbar-example"
minZoom={0.2}
maxZoom={4}
fitView
defaultEdgeOptions={defaultEdgeOptions}
nodeTypes={nodeTypes}
nodeOrigin={nodeOrigin}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<SelectedNodesToolbar />
</ReactFlow>
);
}
@@ -0,0 +1,66 @@
import { CSSProperties, useCallback } from 'react';
import { ReactFlow, addEdge, Node, Position, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
sourcePosition: Position.Right,
type: 'input',
data: { label: 'Input' },
position: { x: 0, y: 80 },
},
{
id: '2',
type: 'output',
sourcePosition: Position.Right,
targetPosition: Position.Left,
data: { label: 'A Node' },
position: { x: 250, y: 0 },
},
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }];
const buttonStyle: CSSProperties = {
position: 'absolute',
right: 10,
top: 30,
zIndex: 4,
};
const NodeTypeChangeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const changeType = () => {
setNodes((nds) =>
nds.map((node) => {
if (node.type === 'input') {
return node;
}
return {
...node,
type: node.type === 'default' ? 'output' : 'default',
};
})
);
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<button onClick={changeType} style={buttonStyle}>
change type
</button>
</ReactFlow>
);
};
export default NodeTypeChangeFlow;
@@ -0,0 +1,89 @@
import { useState, CSSProperties, FC, useCallback } from 'react';
import {
ReactFlow,
addEdge,
Node,
Position,
Connection,
Edge,
NodeProps,
NodeTypes,
useNodesState,
useEdgesState,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
sourcePosition: Position.Right,
type: 'input',
data: { label: 'Input' },
position: { x: 0, y: 80 },
},
{
id: '2',
type: 'a',
sourcePosition: Position.Right,
targetPosition: Position.Left,
data: { label: 'A Node' },
position: { x: 250, y: 0 },
},
];
const buttonStyle: CSSProperties = {
position: 'absolute',
right: 10,
top: 30,
zIndex: 4,
};
const nodeStyles: CSSProperties = {
padding: '10px 15px',
border: '1px solid #ddd',
};
const NodeA: FC<NodeProps> = () => {
return <div style={nodeStyles}>A</div>;
};
const NodeB: FC<NodeProps> = () => {
return <div style={nodeStyles}>B</div>;
};
type NodeTypesObject = {
[key: string]: NodeTypes;
};
const nodeTypesObjects: NodeTypesObject = {
a: {
a: NodeA,
},
b: {
b: NodeB,
},
};
const NodeTypeChangeFlow = () => {
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypesObjects[nodeTypesId]}
>
<button onClick={changeType} style={buttonStyle}>
change type
</button>
</ReactFlow>
);
};
export default NodeTypeChangeFlow;
@@ -0,0 +1,238 @@
import { MouseEvent as ReactMouseEvent, CSSProperties, useCallback } from 'react';
import {
ReactFlow,
addEdge,
Node,
Viewport,
SnapGrid,
Connection,
Edge,
ReactFlowInstance,
useNodesState,
useEdgesState,
OnSelectionChangeParams,
Controls,
Background,
MiniMap,
} from '@xyflow/react';
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDrag = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
const onSelectionContextMenu = (event: ReactMouseEvent, nodes: Node[]) => {
event.preventDefault();
console.log('selection context menu', nodes);
};
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('node click:', node);
const onSelectionChange = ({ nodes, edges }: OnSelectionChangeParams) => console.log('selection change', nodes, edges);
const onInit = (reactFlowInstance: ReactFlowInstance) => {
console.log('pane ready:', reactFlowInstance);
};
const onMoveStart = (_: MouseEvent | TouchEvent | null, viewport: Viewport) => console.log('zoom/move start', viewport);
const onMoveEnd = (_: MouseEvent | TouchEvent | null, viewport: Viewport) => console.log('zoom/move end', viewport);
const onEdgeContextMenu = (_: ReactMouseEvent, edge: Edge) => console.log('edge context menu', edge);
const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse enter', edge);
const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY);
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: {
label: (
<>
Welcome to <strong>React Flow!</strong>
</>
),
},
position: { x: 250, y: 0 },
},
{
id: '2',
data: {
label: (
<>
This is a <strong>default node</strong>
</>
),
},
position: { x: 100, y: 100 },
},
{
id: '3',
data: {
label: (
<>
This one has a <strong>custom style</strong>
</>
),
},
position: { x: 400, y: 100 },
style: {
background: '#D6D5E6',
color: '#333',
border: '1px solid #222138',
width: 180,
},
},
{
id: '4',
position: { x: 250, y: 200 },
data: {
label: (
<>
You can find the docs on{' '}
<a href="https://github.com/wbkd/react-flow" target="_blank" rel="noopener noreferrer">
Github
</a>
</>
),
},
},
{
id: '5',
data: {
label: (
<>
Or check out the other <strong>examples</strong>
</>
),
},
position: { x: 250, y: 325 },
},
{
id: '6',
type: 'output',
data: {
label: (
<>
An <strong>output node (not deletable)</strong>
</>
),
},
position: { x: 100, y: 480 },
deletable: false,
},
{
id: '7',
type: 'output',
data: { label: 'Another output node' },
position: { x: 400, y: 450 },
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' },
{
id: 'e3-4',
source: '3',
target: '4',
animated: true,
label: 'animated edge',
},
{ id: 'e4-5', source: '4', target: '5', label: 'edge with arrow head' },
{
id: 'e5-6',
source: '5',
target: '6',
type: 'smoothstep',
deletable: false,
label: 'smooth step edge (not deletable)',
},
{
id: 'e5-7',
source: '5',
target: '7',
type: 'step',
style: { stroke: '#f6ab6c' },
label: 'a step edge',
animated: true,
labelStyle: { fill: '#f6ab6c', fontWeight: 700 },
},
];
const connectionLineStyle: CSSProperties = { stroke: '#ddd' };
const snapGrid: SnapGrid = [25, 25];
const nodeStrokeColor = (n: Node): string => {
if (n.style?.background) return n.style.background as string;
if (n.type === 'input') return '#0041d0';
if (n.type === 'output') return '#ff0072';
if (n.type === 'default') return '#1a192b';
return '#eee';
};
const nodeColor = (n: Node): string => {
if (n.style?.background) return n.style.background as string;
return '#fff';
};
const OverviewFlow = () => {
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}
onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
onNodeDoubleClick={onNodeDoubleClick}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
onSelectionChange={onSelectionChange}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
onInit={onInit}
connectionLineStyle={connectionLineStyle}
snapToGrid={true}
snapGrid={snapGrid}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeDoubleClick={onEdgeDoubleClick}
fitView
fitViewOptions={{ padding: 0.2 }}
attributionPosition="top-right"
maxZoom={Infinity}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
onPaneMouseMove={onPaneMouseMove}
>
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
<Controls />
<Background color="#aaa" gap={25} />
</ReactFlow>
);
};
export default OverviewFlow;
@@ -0,0 +1,37 @@
import { useReactFlow, useStore } from '@xyflow/react';
import styles from './provider.module.css';
const Sidebar = () => {
const { setNodes } = useReactFlow();
const nodeInfos = useStore((store) =>
store.nodes.map((n) => `Node ${n.id} - x: ${n.position.x.toFixed(2)}, y: ${n.position.y.toFixed(2)}`)
);
const transform = useStore((store) => store.transform);
const selectAll = () => {
setNodes((nodes) => nodes.map((n) => ({ ...n, selected: true })));
};
return (
<aside className={styles.aside}>
<div className={styles.description}>
This is an example of how you can access the internal state outside of the ReactFlow component.
</div>
<div className={styles.title}>Zoom & pan transform</div>
<div className={styles.transform}>
[{transform[0].toFixed(2)}, {transform[1].toFixed(2)}, {transform[2].toFixed(2)}]
</div>
<div className={styles.title}>Nodes</div>
{nodeInfos.map((info, index) => (
<div key={index}>{info}</div>
))}
<div className={styles.selectall}>
<button onClick={selectAll}>select all nodes</button>
</div>
</aside>
);
};
export default Sidebar;
@@ -0,0 +1,68 @@
import { MouseEvent, useCallback } from 'react';
import {
ReactFlow,
Controls,
ReactFlowProvider,
addEdge,
Node,
Connection,
Edge,
ConnectionMode,
useNodesState,
useEdgesState,
ReactFlowInstance,
} from '@xyflow/react';
import Sidebar from './Sidebar';
import styles from './provider.module.css';
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('pane ready:', reactFlowInstance);
const initialNodes: 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 initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const ProviderFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
return (
<div className={styles.providerflow}>
<ReactFlowProvider>
<Sidebar />
<div className={styles.wrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onInit={onInit}
connectionMode={ConnectionMode.Loose}
>
<Controls />
</ReactFlow>
</div>
</ReactFlowProvider>
</div>
);
};
export default ProviderFlow;
@@ -0,0 +1,45 @@
.providerflow {
flex-direction: column;
display: flex;
height: 100%;
}
.aside {
border-right: 1px solid #eee;
padding: 15px 10px;
font-size: 12px;
background: #fcfcfc;
}
.description {
margin-bottom: 10px;
}
.title {
font-weight: 700;
margin-bottom: 5px;
}
.transform {
margin-bottom: 20px;
}
.wrapper {
flex-grow: 1;
height: 100%;
}
.selectall {
margin-top: 10px;
}
@media screen and (min-width: 768px) {
.providerflow {
flex-direction: row;
}
.aside {
width: 20%;
max-width: 250px;
}
}

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