diff --git a/README.md b/README.md
index e87a9af2..3f9e9347 100644
--- a/README.md
+++ b/README.md
@@ -542,6 +542,7 @@ You can find all examples in the [example](example) folder or check out the live
- [interaction](https://reactflow.dev/interaction)
- [provider](https://reactflow.dev/provider)
- [hidden](https://reactflow.dev/hidden)
+- [edge types](https://reactflow.dev/edge-types)
# Development
diff --git a/cypress/integration/flow/basic.spec.js b/cypress/integration/flow/basic.spec.js
index a073a03a..121896d3 100644
--- a/cypress/integration/flow/basic.spec.js
+++ b/cypress/integration/flow/basic.spec.js
@@ -1,7 +1,9 @@
describe('Basic Flow Rendering', () => {
- it('renders a flow with three nodes', () => {
+ before(() => {
cy.visit('/basic');
+ });
+ 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);
@@ -115,16 +117,10 @@ describe('Basic Flow Rendering', () => {
});
it('drags the pane', () => {
- // for d3 we have to pass the window to the event
- // https://github.com/cypress-io/cypress/issues/3441
-
- const newPosition = {
- clientX: Cypress.config('viewportWidth') * 0.6,
- clientY: Cypress.config('viewportHeight') * 0.7,
- };
-
const styleBeforeDrag = Cypress.$('.react-flow__nodes').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__zoompane')
.trigger('mousedown', 'topLeft', { which: 1, view: win })
diff --git a/cypress/integration/flow/controls.spec.js b/cypress/integration/flow/controls.spec.js
new file mode 100644
index 00000000..3975fadb
--- /dev/null
+++ b/cypress/integration/flow/controls.spec.js
@@ -0,0 +1,80 @@
+describe('Controls Testing', () => {
+ before(() => {
+ cy.visit('/');
+ });
+
+ it('renders the control panel', () => {
+ cy.get('.react-flow__controls');
+ });
+
+ it('zooms in', () => {
+ const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
+
+ cy.get('.react-flow__controls-zoomin')
+ .click()
+ .then(() => {
+ const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
+ expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
+ });
+ });
+
+ it('zooms out', () => {
+ const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
+
+ cy.get('.react-flow__controls-zoomout')
+ .click()
+ .then(() => {
+ const styleAfterZoom = Cypress.$('.react-flow__nodes').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__nodes').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__zoompane')
+ .trigger('mousedown', 'topLeft', { which: 1, view: win })
+ .trigger('mousemove', 'bottomLeft')
+ .trigger('mouseup', { force: true, view: win })
+ .then(() => {
+ const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
+ expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
+ });
+ });
+ });
+
+ it('fits view', () => {
+ const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
+
+ cy.get('.react-flow__controls-fitview')
+ .click()
+ .then(() => {
+ const styleAfterZoom = Cypress.$('.react-flow__nodes').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__renderer').click('bottomRight');
+ 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().should('have.class', 'selected');
+ });
+ });
+});
diff --git a/cypress/integration/flow/custom-node.js b/cypress/integration/flow/custom-node.js
deleted file mode 100644
index 4cecdcec..00000000
--- a/cypress/integration/flow/custom-node.js
+++ /dev/null
@@ -1,10 +0,0 @@
-describe('Custom Node Flow Rendering', () => {
- it('renders a flow', () => {
- cy.visit('/custom-node');
-
- cy.get('.react-flow__renderer');
-
- cy.get('.react-flow__node').should('have.length', 4);
- cy.get('.react-flow__edge').should('have.length', 3);
- });
-});
diff --git a/cypress/integration/flow/empty.spec.js b/cypress/integration/flow/empty.spec.js
index d4029107..28853dfd 100644
--- a/cypress/integration/flow/empty.spec.js
+++ b/cypress/integration/flow/empty.spec.js
@@ -1,7 +1,9 @@
describe('Empty Flow Rendering', () => {
- it('renders an empty flow', () => {
+ 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');
@@ -16,26 +18,6 @@ describe('Empty Flow Rendering', () => {
.trigger('mouseup', 'bottomRight', { force: true });
});
- it('renders a control panel', () => {
- cy.get('.react-flow__controls');
- });
-
- it('uses zoom in control', () => {
- cy.get('.react-flow__controls-zoomin').click();
- });
-
- it('uses zoom out control', () => {
- cy.get('.react-flow__controls-zoomout').click();
- });
-
- it('uses fit view control', () => {
- cy.get('.react-flow__controls-fitview').click();
- });
-
- it('uses lock view control', () => {
- cy.get('.react-flow__controls-interactive');
- });
-
it('renders an empty mini map', () => {
cy.get('.react-flow__minimap');
cy.get('.react-flow__minimap-node').should('not.exist');
diff --git a/cypress/integration/flow/graph-utils.spec.js b/cypress/integration/flow/graph-utils.spec.js
new file mode 100644
index 00000000..128f3e6d
--- /dev/null
+++ b/cypress/integration/flow/graph-utils.spec.js
@@ -0,0 +1,130 @@
+import { isNode, isEdge, getOutgoers, removeElements, addEdge } from '../../../src/utils/graph.ts';
+
+const nodes = [
+ { 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 = [
+ { id: 'e1-2', source: '1', target: '2', animated: true },
+ { id: 'e1-3', source: '1', target: '3' },
+];
+
+const elements = [...nodes, ...edges];
+
+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], elements);
+ expect(outgoers.length).to.be.equal(2);
+
+ const noOutgoers = getOutgoers(nodes[1], elements);
+ expect(noOutgoers.length).to.be.equal(0);
+ });
+
+ describe('tests addEdge function', () => {
+ it('adds edge', () => {
+ const newEdge = { source: '2', target: '3' };
+ const nextElements = addEdge(newEdge, elements);
+
+ expect(nextElements.length).to.be.equal(elements.length + 1);
+ });
+
+ it('tries to add invalid edge', () => {
+ const newEdge = { nosource: '1', notarget: '3' };
+
+ try {
+ addEdge(newEdge, elements);
+ } catch (e) {
+ console.log(e.message);
+
+ expect(e.message).to.be.equal("Can't create edge. An edge needs a source and a target.");
+ }
+ });
+
+ it('tries to add edge with id that does not exist', () => {
+ const notExistingId = 'does-not-exist';
+ const newEdge = { source: notExistingId, target: '3' };
+
+ try {
+ addEdge(newEdge, elements);
+ } catch (e) {
+ console.log(e.message);
+ expect(e.message).to.be.equal(`Can't create edge. Node with id=${notExistingId} does not exist.`);
+ }
+ });
+ });
+
+ describe('tests removeElements function', () => {
+ it('removes a node', () => {
+ const nextElements = removeElements([nodes[0]], elements);
+
+ const nextNodes = nextElements.filter((e) => isNode(e));
+ const nextEdges = nextElements.filter((e) => isEdge(e));
+
+ expect(nextNodes.length).to.be.equal(nodes.length - 1);
+ expect(nextEdges.length).to.be.equal(edges.length - 2);
+ });
+
+ it('removes multiple nodes', () => {
+ const elementsToRemove = [nodes[0], nodes[1]];
+ const nextElements = removeElements(elementsToRemove, elements);
+ const nextNodes = nextElements.filter((e) => isNode(e));
+ const nextEdges = nextElements.filter((e) => isEdge(e));
+
+ expect(nextNodes.length).to.be.equal(nodes.length - 2);
+ expect(nextEdges.length).to.be.equal(0);
+ });
+
+ it('removes no node', () => {
+ const nextElementsNoRemove = removeElements([], elements);
+ expect(nextElementsNoRemove.length).to.be.equal(elements.length);
+ });
+
+ it('tries to removes node that does not exist', () => {
+ const nextElementsNoRemove = removeElements([{ id: 'id-that-does-not-exist' }], elements);
+ expect(nextElementsNoRemove.length).to.be.equal(elements.length);
+ });
+
+ it('removes an edge', () => {
+ const nextElements = removeElements([edges[0]], elements);
+
+ const nextNodes = nextElements.filter((e) => isNode(e));
+ const nextEdges = nextElements.filter((e) => isEdge(e));
+
+ expect(nextNodes.length).to.be.equal(nodes.length);
+ expect(nextEdges.length).to.be.equal(edges.length - 1);
+ });
+
+ it('removes multiple edges', () => {
+ const nextElements = removeElements([edges[0], edges[1]], elements);
+
+ const nextNodes = nextElements.filter((e) => isNode(e));
+ const nextEdges = nextElements.filter((e) => isEdge(e));
+
+ expect(nextNodes.length).to.be.equal(nodes.length);
+ expect(nextEdges.length).to.be.equal(edges.length - 2);
+ });
+
+ it('removes node and edge', () => {
+ const nextElements = removeElements([nodes[0], edges[0]], elements);
+
+ const nextNodes = nextElements.filter((e) => isNode(e));
+ const nextEdges = nextElements.filter((e) => isEdge(e));
+
+ expect(nextNodes.length).to.be.equal(nodes.length - 1);
+ expect(nextEdges.length).to.be.equal(edges.length - 2);
+ });
+ });
+});
diff --git a/cypress/integration/flow/hidden.spec.js b/cypress/integration/flow/hidden.spec.js
index ff534735..fdb0827f 100644
--- a/cypress/integration/flow/hidden.spec.js
+++ b/cypress/integration/flow/hidden.spec.js
@@ -1,7 +1,9 @@
describe('Hidden Flow Rendering', () => {
- it('renders the initial flow', () => {
+ before(() => {
cy.visit('/hidden');
+ });
+ 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);
diff --git a/cypress/integration/flow/interaction.spec.js b/cypress/integration/flow/interaction.spec.js
index 33e672ce..055a4214 100644
--- a/cypress/integration/flow/interaction.spec.js
+++ b/cypress/integration/flow/interaction.spec.js
@@ -1,7 +1,9 @@
describe('Interaction Flow Rendering', () => {
- it('renders initial flow', () => {
+ 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);
@@ -23,6 +25,38 @@ describe('Interaction Flow Rendering', () => {
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 scroll', () => {
+ const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
+
+ cy.get('.react-flow__zoompane')
+ .dblclick()
+ .then(() => {
+ const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
+ expect(styleBeforeZoom).to.equal(styleAfterZoom);
+ });
+ });
+
+ it('tries to zoom by double click', () => {
+ const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
+
+ cy.get('.react-flow__zoompane')
+ .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();
});
@@ -47,4 +81,53 @@ describe('Interaction Flow Rendering', () => {
it('selects an edge by click', () => {
cy.get('.react-flow__edge:first').click().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', { which: 1 });
+
+ 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__nodes').css('transform');
+
+ cy.get('.react-flow__zoompane')
+ .trigger('wheel', 'topLeft', { deltaY: 200 })
+ .then(() => {
+ const styleAfterZoom = Cypress.$('.react-flow__nodes').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', () => {
+ const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
+
+ cy.get('.react-flow__zoompane')
+ .dblclick()
+ .then(() => {
+ const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
+ expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
+ });
+ });
});
diff --git a/cypress/integration/flow/minimap.spec.js b/cypress/integration/flow/minimap.spec.js
new file mode 100644
index 00000000..1ce5bb5a
--- /dev/null
+++ b/cypress/integration/flow/minimap.spec.js
@@ -0,0 +1,69 @@
+describe('Minimap Testing', () => {
+ 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', () => {
+ 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').attr('viewBox');
+ const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
+
+ cy.get('.react-flow__zoompane')
+ .trigger('wheel', 'topLeft', { deltaY: -200 })
+ .then(() => {
+ const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').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 xPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
+ const yPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
+ const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
+
+ cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el) => {
+ const xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
+ const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
+ const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
+
+ expect(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
+ expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag);
+ expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
+ });
+ });
+
+ it('changes node positions via pane drag', () => {
+ const viewBoxBeforeDrag = Cypress.$('.react-flow__minimap').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__zoompane')
+ .trigger('mousedown', 'topLeft', { which: 1, view: win })
+ .trigger('mousemove', 'bottomLeft')
+ .trigger('mouseup', { force: true, view: win })
+ .then(() => {
+ const viewBoxAfterDrag = Cypress.$('.react-flow__minimap').attr('viewBox');
+ const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
+
+ expect(viewBoxBeforeDrag).to.not.equal(viewBoxAfterDrag);
+ expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
+ });
+ });
+ });
+});
diff --git a/cypress/integration/flow/overview.spec.js b/cypress/integration/flow/overview.spec.js
deleted file mode 100644
index d0153db2..00000000
--- a/cypress/integration/flow/overview.spec.js
+++ /dev/null
@@ -1,17 +0,0 @@
-describe('Overview Flow Rendering', () => {
- it('renders a flow', () => {
- cy.visit('/');
-
- cy.get('.react-flow__renderer');
-
- cy.get('.react-flow__node').should('have.length', 7);
- cy.get('.react-flow__edge').should('have.length', 6);
- });
-
- it('renders a grid', () => {
- cy.get('.react-flow__background');
-
- const gridStroke = Cypress.$('.react-flow__background path').attr('fill');
- expect(gridStroke).to.equal('#888');
- });
-});
diff --git a/cypress/integration/flow/stress.spec.js b/cypress/integration/flow/stress.spec.js
deleted file mode 100644
index d70b5847..00000000
--- a/cypress/integration/flow/stress.spec.js
+++ /dev/null
@@ -1,10 +0,0 @@
-describe('Stress Flow Rendering', () => {
- it('renders initial flow', () => {
- cy.visit('/stress');
-
- cy.get('.react-flow__renderer');
-
- cy.get('.react-flow__node').should('have.length', 100);
- cy.get('.react-flow__edge').should('have.length', 99);
- });
-});
diff --git a/example/src/CustomNode/index.js b/example/src/CustomNode/index.js
index c346808f..9dc682c3 100644
--- a/example/src/CustomNode/index.js
+++ b/example/src/CustomNode/index.js
@@ -70,7 +70,7 @@ const CustomNodeFlow = () => {
nodeTypes={{
selectorNode: ColorSelectorNode,
}}
- connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
+ connectionLineStyle={{ stroke: '#ddd' }}
snapToGrid={true}
snapGrid={[16, 16]}
>
diff --git a/example/src/EdgeTypes/index.js b/example/src/EdgeTypes/index.js
new file mode 100644
index 00000000..c6bae629
--- /dev/null
+++ b/example/src/EdgeTypes/index.js
@@ -0,0 +1,36 @@
+/**
+ * Example for checking the different edge types and source and target positions
+ */
+import React, { useState } from 'react';
+
+import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
+import { getElements } from './utils';
+
+const onLoad = (reactFlowInstance) => {
+ reactFlowInstance.fitView();
+ console.log(reactFlowInstance.getElements());
+};
+
+const initialElements = getElements();
+
+const EdgeTypesFlow = () => {
+ const [elements, setElements] = useState(initialElements);
+ const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
+ const onConnect = (params) => setElements((els) => addEdge(params, els));
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default EdgeTypesFlow;
diff --git a/example/src/EdgeTypes/utils.js b/example/src/EdgeTypes/utils.js
new file mode 100644
index 00000000..baf832a3
--- /dev/null
+++ b/example/src/EdgeTypes/utils.js
@@ -0,0 +1,104 @@
+const nodeWidth = 80;
+const nodeGapWidth = nodeWidth * 2;
+const nodeStyle = { width: nodeWidth, fontSize: 11, color: 'white' };
+
+const sourceTargetPositions = [
+ { source: 'bottom', target: 'top' },
+ { source: 'right', target: 'left' },
+];
+const nodeColors = [
+ ['#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
+ ['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8'],
+];
+const edgeTypes = ['default', 'step', 'smoothstep', 'straight'];
+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 = () => (id++).toString();
+
+export function getElements() {
+ const initialElements = [];
+
+ 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 = {
+ 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 = {
+ id: targetId,
+ style,
+ data: targetData,
+ position: targetPosition,
+ sourcePosition: currSourceTargetPos.source,
+ targetPosition: currSourceTargetPos.target,
+ };
+
+ initialElements.push(sourceNode);
+ initialElements.push(targetNode);
+
+ initialElements.push({ id: `${sourceId}-${targetId}`, source: sourceId, target: targetId, type: currEdgeType });
+ }
+ }
+ }
+
+ return initialElements;
+}
diff --git a/example/src/Interaction/index.js b/example/src/Interaction/index.js
index f2fabcdd..3599ab7b 100644
--- a/example/src/Interaction/index.js
+++ b/example/src/Interaction/index.js
@@ -22,8 +22,8 @@ const InteractionFlow = () => {
const [isSelectable, setIsSelectable] = useState(false);
const [isDraggable, setIsDraggable] = useState(false);
const [isConnectable, setIsConnectable] = useState(false);
- const [zoomOnScroll, setZoomOnScroll] = useState(true);
- const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(true);
+ const [zoomOnScroll, setZoomOnScroll] = useState(false);
+ const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false);
return (
{
onSelectionChange={onSelectionChange}
style={{ width: '100%', height: '100%' }}
onLoad={onLoad}
- connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
+ connectionLineStyle={{ stroke: '#ddd' }}
snapToGrid={true}
snapGrid={[16, 16]}
>
{
- if (n.type === 'input') return 'blue';
- if (n.type === 'output') return 'green';
- if (n.type === 'default') return 'red';
+ if (n.style?.background) return n.style.background;
+ if (n.type === 'input') return '#9999ff';
+ if (n.type === 'output') return '#79c9b7';
+ if (n.type === 'default') return '#ff6060';
- return '#FFCC00';
+ return '#eee';
}}
/>
diff --git a/example/src/index.js b/example/src/index.js
index 5eb71cfa..51b9c00f 100644
--- a/example/src/index.js
+++ b/example/src/index.js
@@ -13,6 +13,7 @@ import Validation from './Validation';
import Horizontal from './Horizontal';
import Provider from './Provider';
import Hidden from './Hidden';
+import EdgeTypes from './EdgeTypes';
import './index.css';
@@ -69,6 +70,10 @@ const routes = [
path: '/hidden',
component: Hidden,
},
+ {
+ path: '/edge-types',
+ component: EdgeTypes,
+ },
];
const navLinks = routes.filter((route) => route.label);
diff --git a/package.json b/package.json
index 5bc59d3d..b7565ab5 100644
--- a/package.json
+++ b/package.json
@@ -16,10 +16,12 @@
"start:examples": "npm run build && cd example && npm start",
"dev:wait": "start-server-and-test start:examples http-get://localhost:3000",
"build:example": "npm install && npm run build && cd example && npm install && npm run build",
- "cy:run": "cypress run",
"cy:open": "cypress open",
"cypress": "npm run dev:wait cy:open",
- "test": "npm run dev:wait cy:run",
+ "test:chrome": "cypress run --browser chrome",
+ "test:firefox": "cypress run --browser firefox",
+ "test:all": "npm run test:chrome && npm run test:firefox",
+ "test": "npm run dev:wait test:chrome",
"release": "release-it"
},
"dependencies": {
diff --git a/src/additional-components/MiniMap/MiniMapNode.tsx b/src/additional-components/MiniMap/MiniMapNode.tsx
index e85ba788..24b1eee0 100644
--- a/src/additional-components/MiniMap/MiniMapNode.tsx
+++ b/src/additional-components/MiniMap/MiniMapNode.tsx
@@ -15,7 +15,7 @@ const MiniMapNode = ({ node, color, borderRadius }: MiniMapNodeProps) => {
height,
} = node.__rf;
const { background, backgroundColor } = node.style || {};
- const fill = (background || backgroundColor || color) as string;
+ const fill = (color || background || backgroundColor) as string;
return (
{
+ borderRadius = 5,
+ }: EdgeSmoothStepProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
@@ -159,6 +162,7 @@ export default memo(
targetX,
targetY,
targetPosition,
+ borderRadius,
});
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
diff --git a/src/components/Edges/StepEdge.tsx b/src/components/Edges/StepEdge.tsx
index 9475685b..a03e3052 100644
--- a/src/components/Edges/StepEdge.tsx
+++ b/src/components/Edges/StepEdge.tsx
@@ -1,61 +1,8 @@
import React, { memo } from 'react';
-import EdgeText from './EdgeText';
-import { getMarkerEnd } from './utils';
-import { EdgeProps } from '../../types';
+import { EdgeSmoothStepProps } from '../../types';
+import SmoothStepEdge from './SmoothStepEdge';
-interface GetStepPathParams {
- centerY: number;
- sourceX: number;
- sourceY: number;
- targetX: number;
- targetY: number;
-}
-
-export function getStepPath({ centerY, sourceX, sourceY, targetX, targetY }: GetStepPathParams): string {
- return `M ${sourceX},${sourceY}L ${sourceX},${centerY}L ${targetX},${centerY}L ${targetX},${targetY}`;
-}
-
-export default memo(
- ({
- sourceX,
- sourceY,
- targetX,
- targetY,
- label,
- labelStyle,
- labelShowBg,
- labelBgStyle,
- style,
- arrowHeadType,
- markerEndId,
- }: EdgeProps) => {
- const yOffset = Math.abs(targetY - sourceY) / 2;
- const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
-
- const xOffset = Math.abs(targetX - sourceX) / 2;
- const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
-
- const path = getStepPath({ centerY, sourceX, sourceY, targetX, targetY });
-
- const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
-
- const text = label ? (
-
- ) : null;
-
- return (
- <>
-
- {text}
- >
- );
- }
-);
+export default memo((props: EdgeSmoothStepProps) => {
+ return ;
+});
diff --git a/src/style.css b/src/style.css
index e2057f62..7fdaef30 100644
--- a/src/style.css
+++ b/src/style.css
@@ -63,7 +63,7 @@
.react-flow__edge-path {
fill: none;
stroke: #bbb;
- stroke-width: 2;
+ stroke-width: 1.5;
}
.react-flow__edge-text {
@@ -83,7 +83,7 @@
.react-flow__connection-path {
fill: none;
stroke: #ddd;
- stroke-width: 2;
+ stroke-width: 1.5;
}
.react-flow__nodes {
@@ -107,7 +107,7 @@
.react-flow__node-input,
.react-flow__node-output {
padding: 10px;
- border-radius: 5px;
+ border-radius: 3px;
width: 150px;
font-size: 12px;
color: #222;
@@ -127,16 +127,16 @@
}
}
-.react-flow__node-default {
- background: #ff6060;
-}
-
.react-flow__node-input {
background: #9999ff;
}
+.react-flow__node-default {
+ background: #ff6060;
+}
+
.react-flow__node-output {
- background: #55dd99;
+ background: #79c9b7;
}
.react-flow__nodesselection {
diff --git a/src/types/index.ts b/src/types/index.ts
index 6bbf076d..8b9e3e83 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -99,6 +99,10 @@ export interface EdgeBezierProps extends EdgeProps {
targetPosition: Position;
}
+export interface EdgeSmoothStepProps extends EdgeBezierProps {
+ borderRadius?: number;
+}
+
export interface NodeProps {
id: ElementId;
type: string;
diff --git a/src/utils/graph.ts b/src/utils/graph.ts
index 8b899913..79ebfc04 100644
--- a/src/utils/graph.ts
+++ b/src/utils/graph.ts
@@ -33,9 +33,16 @@ const getEdgeId = ({ source, target }: Connection): ElementId => `reactflow__edg
export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => {
if (!edgeParams.source || !edgeParams.target) {
- throw new Error('Can not create edge. An edge needs a source and a target');
+ throw new Error("Can't create edge. An edge needs a source and a target.");
}
+ // make sure that there is node with the target and one with the source id
+ [edgeParams.source, edgeParams.target].forEach((id) => {
+ if (!elements.find((e) => isNode(e) && e.id === id)) {
+ throw new Error(`Can't create edge. Node with id=${id} does not exist.`);
+ }
+ });
+
if (isEdge(edgeParams)) {
return elements.concat({ ...edgeParams });
}