diff --git a/README.md b/README.md index 078e245f..589d5532 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,40 @@ nodeTypes={{ You can now use type `special` for a node. The `default`, `input` and `output` types will be still available except you overwrite one of them. -You can find an example of how to implement a custom node in the [custom node example](example/src/CustomNode). +You can find an example of how to implement a custom node in the [custom node example](/example/src/CustomNode). + +## Handle + +We export a `Handle` component as a helper for your custom nodes: + +```javascript +import { Handle } from 'react-flow-renderer'; + +const targetHandleWithValidation = ( + connection.source === 'some-id'} + onConnect={params => console.log('handle onConnect', params)} + style={{ background: '#fff' }} + /> +); + +``` + +### Props + +- `type`: 'source' | 'target' +- `id`: string - you only need this when you have multiple source or target handles otherwise the node id is used +- `position`: 'left' | 'right' | 'top' | 'bottom' handle position - default: 'top' for type target, 'bottom' for type source +- `onConnect`: function that gets triggered on connect +- `isValidConnection`: function receives a connection `{ target: 'some-id', source: 'another-id' }` as param, returns a boolean - true by default +- `style`: css properties + +### Multiple Handles + +If you need multiple source our target handles you can achieve this by creating a custom node. Normally you just use the id of a node for the `source` or `target` of an edge. If you have multiple source or target handles you need to pass an id to these handles. These ids get then added to the node id, so that you can connect to a specific handle. If you have a node with an id = `1` and a handle with an id = `a` you can connect to this handle by using the id = `1__a`. +You can find an example of how to implement a custom node with multiple handles in the [custom node example](/example/src/CustomNode/ColorSelectorNode.js#L18-L29). ## Edges @@ -281,7 +314,7 @@ const FlowWithControls = () => ( You can find all examples in the [example](example) folder or check out the live versions: -- [rich](https://react-flow.netlify.app/) +- [overview](https://react-flow.netlify.app/) - [basic](https://react-flow.netlify.app/basic) - [custom node](https://react-flow.netlify.app/custom-node) - [horizontal](https://react-flow.netlify.app/horizontal) diff --git a/cypress/integration/flow/empty.spec.js b/cypress/integration/flow/empty.spec.js index d378ef85..4d0b48cf 100644 --- a/cypress/integration/flow/empty.spec.js +++ b/cypress/integration/flow/empty.spec.js @@ -33,7 +33,7 @@ describe('Empty Flow Rendering', () => { }); it('uses lock view control', () => { - cy.get('.react-flow__controls-interactive').click(); + cy.get('.react-flow__controls-interactive'); }); it('renders an empty mini map', () => { diff --git a/cypress/integration/flow/inactive.spec.js b/cypress/integration/flow/inactive.spec.js index 0fdcf741..44209137 100644 --- a/cypress/integration/flow/inactive.spec.js +++ b/cypress/integration/flow/inactive.spec.js @@ -9,11 +9,13 @@ describe('Inactive Graph Rendering', () => { }); it('tries to select a node by click', () => { - cy.get('.react-flow__node:first').click().should('have.not.class', 'selected'); + const pointerEvents = Cypress.$('.react-flow__node:first').css('pointer-events'); + expect(pointerEvents).to.equal('none'); }); it('tries to select an edge by click', () => { - cy.get('.react-flow__edge:first').click().should('have.not.class', 'selected'); + const pointerEvents = Cypress.$('.react-flow__edge:first').css('pointer-events'); + expect(pointerEvents).to.equal('none'); }); it('tries to do a selection', () => { @@ -22,30 +24,6 @@ describe('Inactive Graph Rendering', () => { cy.get('body').type('{shift}', { release: true }); }); - it('tries to drag a node', () => { - const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform'); - - cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el) => { - const styleAfterDrag = $el.css('transform'); - expect(styleBeforeDrag).to.equal(styleAfterDrag); - }); - }); - - it('tries to connect 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', 2); - }); - it('toggles interactive mode', () => { cy.get('.react-flow__interactive').click(); }); diff --git a/example/src/Rich/index.js b/example/src/Rich/index.js index 162d2f99..ab545a51 100644 --- a/example/src/Rich/index.js +++ b/example/src/Rich/index.js @@ -18,7 +18,7 @@ const initialElements = [ }, { id: '4', position: { x: 250, y: 200 }, - data: { label: <>You can find the docs on Github } + data: { label: <>You can find the docs on Github } }, { id: '5', data: { label: <>Or check out the other examples }, position: { x: 250, y: 300 } }, { id: '6', type: 'output', data: { label: <>An output node }, position: { x: 100, y: 450 } }, diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index c1c9e51b..cb845358 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -37,6 +37,9 @@ export default (EdgeComponent: ComponentType) => { ...rest }: EdgeWrapperProps) => { const edgeClasses = cx('react-flow__edge', { selected, animated }); + const edgeGroupStyle: CSSProperties = { + pointerEvents: isInteractive ? 'all' : 'none', + }; const onEdgeClick = (): void => { if (!isInteractive) { return; @@ -47,7 +50,7 @@ export default (EdgeComponent: ComponentType) => { }; return ( - + ) => { const [isDragging, setDragging] = useState(false); const position = { x: xPos, y: yPos }; const nodeClasses = cx('react-flow__node', { selected }); - const nodeStyle = { + const nodeStyle: CSSProperties = { zIndex: selected ? 10 : 3, transform: `translate(${xPos}px,${yPos}px)`, + pointerEvents: isInteractive ? 'all' : 'none', }; const updateNode = (): void => { diff --git a/src/style.css b/src/style.css index 9bd3d521..5232f445 100644 --- a/src/style.css +++ b/src/style.css @@ -100,20 +100,6 @@ transform-origin: 0 0; } -.is-interactive { - .react-flow__node { - cursor: grab; - - &:hover > * { - box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08); - } - } - - .react-flow__handle { - cursor: crosshair; - } -} - .react-flow__node { position: absolute; color: #222; @@ -123,10 +109,20 @@ user-select: none; pointer-events: all; transform-origin: 0 0; + cursor: grab; - &.selected > * { + &.selected > *, + &.selected:hover > * { box-shadow: 0 0 0 2px #555; } + + &:hover > * { + box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08); + } + + .react-flow__handle { + cursor: crosshair; + } } .react-flow__handle {