Merge pull request #3993 from xyflow/next

* fix(react): ReactFlow comp ref type

* chore(changelog): update

* Refactor(types): unify node and edge handling (#3978)

* refactor(types): unify node and edge type behaviour

* chore(changelogs): update

* chore(examples): cleanup

* chore(svelte): update deps

* refactor(useNodesData): improve selector equality function (#3976)

* improved selector equality function for useNodesData

* fixed selector & useNodesData example

* added shallow-node-data as system function

* fix(safari): prevent selection of viewport (#3973)

* prevent selection of viewport

* added user-select=none to viewport-portal and moved to be over nodes

* chore(changelogs): update

* fix(react): allow edges to start from 0,0 (#3980)

* test(playwright): fix edge selection and auto pan tests

* fix(react): select nodes with selection in uncontrolled flows

* text(react): cleanup tests, add onNodesChange tests

* react(pane): position fixed

* chore(test): cleanup

* chore(react): dont use position fixed

---------

Co-authored-by: peterkogo <7165378+peterkogo@users.noreply.github.com>
This commit is contained in:
Moritz Klack
2024-03-07 13:54:58 +01:00
committed by GitHub
co-authored by peterkogo
64 changed files with 1484 additions and 705 deletions
@@ -6,7 +6,7 @@ import * as simpleflow from '../../fixtures/simpleflow';
describe('<ReactFlow />: Basic Props', () => { describe('<ReactFlow />: Basic Props', () => {
describe('uses defaultNodes and defaultEdges', () => { describe('uses defaultNodes and defaultEdges', () => {
beforeEach(() => { beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} />); cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} nodeDragThreshold={0} />);
}); });
it('mounts nodes and edges', () => { it('mounts nodes and edges', () => {
@@ -0,0 +1,138 @@
import { useCallback, useState } from 'react';
import { OnNodesChange, Panel, ReactFlow, ReactFlowProps, applyNodeChanges, useReactFlow, Node } from '@xyflow/react';
const nodes = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
style: { width: 100, height: 40 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 200, y: 200 },
style: { width: 80, height: 50 },
},
];
describe('<ReactFlow />: onNodesChange', () => {
beforeEach(() => {
const onNodesChange = cy.spy().as('onNodesChange');
cy.mount(<Comp nodes={nodes} onNodesChange={onNodesChange} />).wait(500);
});
it('receive initial dimension change', () => {
const expectedChanges = nodes.map((n) => ({
type: 'dimensions',
id: n.id,
dimensions: n.style,
}));
cy.get('@onNodesChange').should('have.callCount', 1);
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
it('receive replace and dimension change', () => {
const expectedDimensionChanges = [
{
type: 'dimensions',
id: '1',
dimensions: { width: 200, height: 100 },
},
];
const expectedReplaceChanges = [
{
type: 'replace',
id: '1',
item: {
...nodes[0],
computed: { positionAbsolute: nodes[0].position, width: 200, height: 100 },
style: { width: 200, height: 100 },
},
},
];
cy.get('[data-id=update-btn]').click();
cy.get('@onNodesChange').should('have.callCount', 3);
cy.get('@onNodesChange').then((s) => {
// dimension change (already checked above)
// const firstCall = s.getCall(0);
// replace change
// @ts-ignore
const secondChangeCallArgs = s.getCall(1).args[0];
// dimension change
// @ts-ignore
const thirdChangeCallArgs = s.getCall(2).args[0];
expect(secondChangeCallArgs).to.be.deep.eq(expectedReplaceChanges);
expect(thirdChangeCallArgs).to.be.deep.eq(expectedDimensionChanges);
});
});
it('receive select and unselect change', () => {
const expectedSelectChanges = [{ id: '1', type: 'select', selected: true }];
const expectedUnselectChanges = [{ id: '1', type: 'select', selected: false }];
cy.get('.react-flow__node').first().click();
cy.get('@onNodesChange').should('have.calledWith', expectedSelectChanges);
cy.get('.react-flow__pane').first().click({ force: true });
cy.get('@onNodesChange').should('have.calledWith', expectedUnselectChanges);
});
it('receive position change', () => {
const endPosition = { x: 200, y: 25 };
const expectedChanges = [
{ id: '1', type: 'position', dragging: false, position: endPosition, positionAbsolute: endPosition },
];
cy.drag('.react-flow__node:first', endPosition).then(() => {
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
it('receive remove change', () => {
const expectedChanges = [{ id: '1', type: 'remove' }];
cy.get('.react-flow__node').first().click();
cy.realPress('Backspace');
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
// test specific helpers
function UpdateButton() {
const { updateNode } = useReactFlow();
const updateNodeDimensions = () => {
updateNode('1', { style: { width: 200, height: 100 } });
};
return (
<Panel position="top-right">
<button data-id="update-btn" onClick={updateNodeDimensions} id="btn">
update
</button>
</Panel>
);
}
type CompProps = ReactFlowProps & { onNodesChange: (changes: any) => void };
function Comp(props: CompProps) {
const [nodes, setNodes] = useState(props.nodes || []);
const onNodesChange: OnNodesChange<Node> = useCallback((changes) => {
props.onNodesChange(changes);
setNodes((nds) => applyNodeChanges(changes, nds));
}, []);
return (
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} nodeDragThreshold={0}>
<UpdateButton />
</ReactFlow>
);
}
+18 -24
View File
@@ -49,21 +49,16 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
it('selects one node with a selection', () => { it('selects one node with a selection', () => {
cy.get('body') cy.get('body')
.type('{shift}', { release: false }) .type('{Shift}', { release: false })
.wait(50)
.get('.react-flow__pane') .get('.react-flow__pane')
.trigger('mousedown', 1, 10, { button: 0, force: true }) .trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 450, { button: 0 }) .trigger('mousemove', 1000, 200, { button: 0 })
.wait(50) .trigger('mouseup', 1000, 200, { button: 0 });
.trigger('mouseup', 1000, 450, { button: 0, force: true });
cy.wait(200); cy.get('.react-flow__node').eq(0).should('have.class', 'selected');
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__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect'); cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true }); cy.get('body').type('{shift}', { release: true, force: true });
}); });
@@ -97,19 +92,19 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
it('drags a node', () => { it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform'); const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el: any) => { cy.drag('.react-flow__node:first', { x: 10, y: 10 }).then(($el: any) => {
const styleAfterDrag = $el.css('transform'); const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag); expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
}); });
}); });
// @TODO: why does this fail since react18? it('removes a node', () => {
// it('removes a node', () => { cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected'); cy.get('html').realPress('Backspace');
// cy.get('html').type('{backspace}').wait(100);
// cy.get('.react-flow__node').should('have.length', 3); cy.get('.react-flow__node').should('have.length', 3);
// cy.get('.react-flow__edge').should('have.length', 1); cy.get('.react-flow__edge').should('have.length', 0);
// }); });
it('connects nodes', () => { it('connects nodes', () => {
cy.get('.react-flow__node') cy.get('.react-flow__node')
@@ -125,16 +120,15 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
.trigger('mouseup', { force: true, button: 0 }); .trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').as('edge'); cy.get('.react-flow__edge').as('edge');
cy.get('@edge').should('have.length', 3); cy.get('@edge').should('have.length', 1);
}); });
// @TODO: why does this fail since react18? it('removes an edge', () => {
// it('removes an edge', () => { cy.get('.react-flow__edge:first').click();
// cy.get('.react-flow__edge:first').click(); cy.get('html').realPress('Backspace');
// cy.get('body').type('{backspace}');
// cy.get('.react-flow__edge').should('have.length', 1); cy.get('.react-flow__edge').should('have.length', 0);
// }); });
it('drags the pane', () => { it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform'); const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
@@ -59,7 +59,7 @@ function ControlledFlow({
handlers.onConnect = onConnect; handlers.onConnect = onConnect;
} }
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />; return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} nodeDragThreshold={0} />;
} }
export default ControlledFlow; export default ControlledFlow;
+1 -1
View File
@@ -9,7 +9,7 @@ Cypress.Commands.add('drag', (selector, { x, y }) =>
const nextY: number = centerY + y; const nextY: number = centerY + y;
return elementToDrag return elementToDrag
.trigger('mousedown', { view: window }) .trigger('mousedown', { view: window, force: true })
.trigger('mousemove', nextX, nextY, { force: true }) .trigger('mousemove', nextX, nextY, { force: true })
.wait(50) .wait(50)
.trigger('mouseup', { view: window, force: true }); .trigger('mouseup', { view: window, force: true });
+1
View File
@@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands'; import './commands';
import 'cypress-real-events/support';
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/; const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
+2 -2
View File
@@ -30,8 +30,8 @@
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "4.1.1", "@vitejs/plugin-react": "4.1.1",
"@vitejs/plugin-react-swc": "^3.4.1", "@vitejs/plugin-react-swc": "^3.4.1",
"cypress": "12.17.3", "cypress": "13.6.6",
"cypress-real-events": "1.11.0", "cypress-real-events": "1.12.0",
"start-server-and-test": "^2.0.2", "start-server-and-test": "^2.0.2",
"typescript": "5.2.2", "typescript": "5.2.2",
"vite": "4.5.0" "vite": "4.5.0"
+1 -1
View File
@@ -45,7 +45,6 @@ const initialNodes: Node[] = [
data: { label: 'Node 4' }, data: { label: 'Node 4' },
position: { x: 400, y: 200 }, position: { x: 400, y: 200 },
className: 'light', className: 'light',
connectable: false,
}, },
]; ];
@@ -129,6 +128,7 @@ const BasicFlow = () => {
selectNodesOnDrag={false} selectNodesOnDrag={false}
elevateEdgesOnSelect elevateEdgesOnSelect
elevateNodesOnSelect={false} elevateNodesOnSelect={false}
nodeDragThreshold={0}
> >
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -1,4 +1,4 @@
import React, { memo, FC, CSSProperties, useCallback } from 'react'; import React, { memo, CSSProperties, useCallback } from 'react';
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react'; import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
import type { ColorSelectorNode } from '.'; import type { ColorSelectorNode } from '.';
@@ -13,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = {
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const ColorSelectorNode: FC<NodeProps<ColorSelectorNode['data']>> = ({ data, isConnectable }) => { function ColorSelectorNode({ data, isConnectable }: NodeProps<ColorSelectorNode>) {
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []); const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []); const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []); const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
@@ -44,6 +44,6 @@ const ColorSelectorNode: FC<NodeProps<ColorSelectorNode['data']>> = ({ data, isC
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} /> <Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
</> </>
); );
}; }
export default memo(ColorSelectorNode); export default memo(ColorSelectorNode);
@@ -1,4 +1,4 @@
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react'; import { useState, useEffect, MouseEvent, ChangeEvent, useCallback, useRef } from 'react';
import { import {
ReactFlow, ReactFlow,
MiniMap, MiniMap,
@@ -17,6 +17,8 @@ import {
OnBeforeDelete, OnBeforeDelete,
BuiltInNode, BuiltInNode,
BuiltInEdge, BuiltInEdge,
NodeTypes,
ReactFlowProvider,
} from '@xyflow/react'; } from '@xyflow/react';
import ColorSelectorNode from './ColorSelectorNode'; import ColorSelectorNode from './ColorSelectorNode';
@@ -40,11 +42,12 @@ const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' }; const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16]; const snapGrid: SnapGrid = [16, 16];
const nodeTypes = { const nodeTypes: NodeTypes = {
selectorNode: ColorSelectorNode, selectorNode: ColorSelectorNode,
}; };
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
const ref = useRef(null);
const [nodes, setNodes] = useState<MyNode[]>([]); const [nodes, setNodes] = useState<MyNode[]>([]);
const onNodesChange: OnNodesChange<MyNode> = useCallback( const onNodesChange: OnNodesChange<MyNode> = useCallback(
(changes) => (changes) =>
@@ -165,6 +168,7 @@ const CustomNodeFlow = () => {
minZoom={0.3} minZoom={0.3}
maxZoom={2} maxZoom={2}
onBeforeDelete={onBeforeDelete} onBeforeDelete={onBeforeDelete}
ref={ref}
> >
<MiniMap<MyNode> <MiniMap<MyNode>
nodeStrokeColor={(n: MyNode): string => { nodeStrokeColor={(n: MyNode): string => {
@@ -186,4 +190,8 @@ const CustomNodeFlow = () => {
); );
}; };
export default CustomNodeFlow; export default () => (
<ReactFlowProvider>
<CustomNodeFlow />
</ReactFlowProvider>
);
@@ -33,6 +33,7 @@ const DragHandleFlow = () => {
edges={edges} edges={edges}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
nodeDragThreshold={0}
/> />
); );
}; };
@@ -36,7 +36,7 @@ const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', nod
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge); const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event); const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event); const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event); const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport); const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport);
const InteractionFlow = () => { const InteractionFlow = () => {
@@ -80,6 +80,7 @@ const InteractionFlow = () => {
onPaneClick={captureZoomClick ? onPaneClick : undefined} onPaneClick={captureZoomClick ? onPaneClick : undefined}
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined} onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined} onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
nodeDragThreshold={0}
onMoveEnd={onMoveEnd} onMoveEnd={onMoveEnd}
> >
<MiniMap /> <MiniMap />
@@ -20,8 +20,8 @@ export type ResultNode = Node<{}, 'result'>;
export type UppercaseNode = Node<{ text: string }, 'uppercase'>; export type UppercaseNode = Node<{ text: string }, 'uppercase'>;
export type MyNode = TextNode | ResultNode | UppercaseNode; export type MyNode = TextNode | ResultNode | UppercaseNode;
export function isTextNode(node: any): node is TextNode { export function isTextNode(node: any): node is TextNode | UppercaseNode {
return node.type === 'text'; return node.type === 'text' || node.type === 'uppercase';
} }
const nodeTypes = { const nodeTypes = {
@@ -27,18 +27,21 @@ const initialNodes: Node[] = [
data: { label: 'Node 2' }, data: { label: 'Node 2' },
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
className: 'light', className: 'light',
type: 'default',
}, },
{ {
id: '3', id: '3',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
className: 'light', className: 'light',
type: 'default',
}, },
{ {
id: '4', id: '4',
data: { label: 'Node 4' }, data: { label: 'Node 4' },
position: { x: 400, y: 200 }, position: { x: 400, y: 200 },
className: 'light', className: 'light',
type: 'default',
}, },
]; ];
@@ -85,6 +88,7 @@ const UseZoomPanHelperFlow = () => {
data: { data: {
label: `${projectedPosition.x}-${projectedPosition.y}`, label: `${projectedPosition.x}-${projectedPosition.y}`,
}, },
type: 'default',
}) })
); );
}, },
@@ -106,6 +110,7 @@ const UseZoomPanHelperFlow = () => {
data: { data: {
label: 'New Node', label: 'New Node',
}, },
type: 'default',
}; };
addNodes(newNode); addNodes(newNode);
@@ -139,8 +144,8 @@ const UseZoomPanHelperFlow = () => {
const onSetNodes = () => { const onSetNodes = () => {
setNodes([ setNodes([
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node a' } }, { id: 'a', type: 'default', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Node b' } }, { id: 'b', type: 'default', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
]); ]);
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]); setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
@@ -3,6 +3,7 @@ import { MarkerType } from '@xyflow/react';
export default { export default {
flowProps: { flowProps: {
fitView: true, fitView: true,
multiSelectionKeyCode: 's',
nodes: [ nodes: [
{ {
id: '1', id: '1',
+1 -1
View File
@@ -15,7 +15,7 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"types": ["cypress", "cypress-real-events"] "types": ["cypress", "node", "cypress-real-events"]
}, },
"include": ["**/*.ts", "**/*.tsx"], "include": ["**/*.ts", "**/*.tsx"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
+5 -4
View File
@@ -12,8 +12,8 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^2.1.1", "@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^1.27.3", "@sveltejs/kit": "^2.5.2",
"@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0", "eslint": "^8.53.0",
@@ -21,8 +21,8 @@
"eslint-plugin-svelte": "^2.35.0", "eslint-plugin-svelte": "^2.35.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3", "prettier-plugin-svelte": "^3.0.3",
"svelte": "^4.2.2", "svelte": "^4.2.12",
"svelte-check": "^3.5.2", "svelte-check": "^3.6.6",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0" "vite": "^4.5.0"
@@ -30,6 +30,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.0.4", "@dagrejs/dagre": "^1.0.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@xyflow/svelte": "workspace:^" "@xyflow/svelte": "workspace:^"
} }
} }
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { ChangeEventHandler } from 'svelte/elements';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import { Handle, Position, type NodeProps } from '@xyflow/svelte'; import { Handle, Position, type NodeProps, type Node } from '@xyflow/svelte';
type $$Props = NodeProps; type $$Props = NodeProps<Node<{ colorStore: Writable<string> }>>;
export let data: { colorStore: Writable<string> }; export let data: $$Props['data'];
const { colorStore } = data; const { colorStore } = data;
</script> </script>
@@ -5,8 +5,8 @@
type UppercaseNodeType = Node<{ text: string }, 'uppercase'>; type UppercaseNodeType = Node<{ text: string }, 'uppercase'>;
type ResultNodeType = Node<{}, 'result'>; type ResultNodeType = Node<{}, 'result'>;
export function isTextNode(node: any): node is TextNodeType { export function isTextNode(node: any): node is TextNodeType | UppercaseNode {
return node.type === 'text'; return node.type === 'text' || node.type === 'uppercase';
} }
export type MyNode = TextNodeType | UppercaseNodeType | ResultNodeType; export type MyNode = TextNodeType | UppercaseNodeType | ResultNodeType;
+1 -1
View File
@@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
+10
View File
@@ -1,5 +1,15 @@
# @xyflow/react # @xyflow/react
## 12.0.0-next.11
## Patch changes
- fix `ref` prop for `ReactFlow` and `Handle` component
- unify `Edge` and `Node` type handling
- fix safari: prevent selection of viewport
- fix `useNodesData` hook to prevent re-renderings
- fix edges: allow start at 0,0
## 12.0.0-next.10 ## 12.0.0-next.10
## ⚠️ Breaking changes ## ⚠️ Breaking changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.0.0-next.10", "version": "12.0.0-next.11",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [ "keywords": [
"react", "react",
@@ -111,7 +111,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
[edge.markerEnd, rfId] [edge.markerEnd, rfId]
); );
if (edge.hidden || !sourceX || !sourceY || !targetX || !targetY) { if (edge.hidden || sourceX === null || sourceY === null || targetX === null || targetY === null) {
return null; return null;
} }
@@ -212,6 +212,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
id={id} id={id}
source={edge.source} source={edge.source}
target={edge.target} target={edge.target}
type={edge.type}
selected={edge.selected} selected={edge.selected}
animated={edge.animated} animated={edge.animated}
label={edge.label} label={edge.label}
@@ -1,5 +1,4 @@
import type { ComponentType } from 'react'; import type { EdgeTypes } from '../../types';
import type { EdgeProps, EdgeTypes } from '../../types';
import { import {
BezierEdgeInternal, BezierEdgeInternal,
StraightEdgeInternal, StraightEdgeInternal,
@@ -9,11 +8,11 @@ import {
} from '../Edges'; } from '../Edges';
export const builtinEdgeTypes: EdgeTypes = { export const builtinEdgeTypes: EdgeTypes = {
default: BezierEdgeInternal as ComponentType<EdgeProps>, default: BezierEdgeInternal,
straight: StraightEdgeInternal as ComponentType<EdgeProps>, straight: StraightEdgeInternal,
step: StepEdgeInternal as ComponentType<EdgeProps>, step: StepEdgeInternal,
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>, smoothstep: SmoothStepEdgeInternal,
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>, simplebezier: SimpleBezierEdgeInternal,
}; };
export const nullPosition = { export const nullPosition = {
+175 -177
View File
@@ -1,9 +1,11 @@
/* import {
* The Handle component is used to connect nodes. When the user mousedowns a handle, we start the connection process. type HTMLAttributes,
* The user can then drag the connection to another handle or node. When the user releases the mouse, we check if the type MouseEvent as ReactMouseEvent,
* connection is valid and if so, we call the onConnect callback. type TouchEvent as ReactTouchEvent,
*/ type ForwardedRef,
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react'; memo,
forwardRef,
} from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { import {
@@ -58,190 +60,186 @@ const connectingSelector =
}; };
}; };
const HandleComponent = forwardRef<HTMLDivElement, HandleComponentProps>( function HandleComponent(
( {
{ type = 'source',
type = 'source', position = Position.Top,
position = Position.Top, isValidConnection,
isValidConnection, isConnectable = true,
isConnectable = true, isConnectableStart = true,
isConnectableStart = true, isConnectableEnd = true,
isConnectableEnd = true, id,
id, onConnect,
onConnect, children,
children, className,
className, onMouseDown,
onMouseDown, onTouchStart,
onTouchStart, ...rest
...rest }: HandleComponentProps,
}, ref: ForwardedRef<HTMLDivElement>
ref ) {
) => { const handleId = id || null;
const handleId = id || null; const isTarget = type === 'target';
const isTarget = type === 'target'; const store = useStoreApi();
const store = useStoreApi(); const nodeId = useNodeId();
const nodeId = useNodeId(); const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow); const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore( connectingSelector(nodeId, handleId, type),
connectingSelector(nodeId, handleId, type), shallow
shallow );
);
if (!nodeId) { if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']()); store.getState().onError?.('010', errorMessages['error010']());
}
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges, setEdges } = store.getState();
setEdges(addEdge(edgeParams, edges));
} }
const onConnectExtended = (params: Connection) => { onConnectAction?.(edgeParams);
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState(); onConnect?.(edgeParams);
};
const edgeParams = { const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
...defaultEdgeOptions, if (!nodeId) {
...params, return;
}; }
if (hasDefaultEdges) {
const { edges, setEdges } = store.getState();
setEdges(addEdge(edgeParams, edges));
}
onConnectAction?.(edgeParams); const isMouseTriggered = isMouseEvent(event.nativeEvent);
onConnect?.(edgeParams);
};
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => { if (
if (!nodeId) { isConnectableStart &&
return; ((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
} ) {
const currentStore = store.getState();
const isMouseTriggered = isMouseEvent(event.nativeEvent); XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect: currentStore.autoPanOnConnect,
if ( connectionMode: currentStore.connectionMode,
isConnectableStart && connectionRadius: currentStore.connectionRadius,
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered) domNode: currentStore.domNode,
) { nodes: currentStore.nodes,
const currentStore = store.getState(); lib: currentStore.lib,
isTarget,
XYHandle.onPointerDown(event.nativeEvent, { handleId,
autoPanOnConnect: currentStore.autoPanOnConnect, nodeId,
connectionMode: currentStore.connectionMode, flowId: currentStore.rfId,
connectionRadius: currentStore.connectionRadius, panBy: currentStore.panBy,
domNode: currentStore.domNode, cancelConnection: currentStore.cancelConnection,
nodes: currentStore.nodes, onConnectStart: currentStore.onConnectStart,
lib: currentStore.lib, onConnectEnd: currentStore.onConnectEnd,
isTarget, updateConnection: currentStore.updateConnection,
handleId, onConnect: onConnectExtended,
nodeId, isValidConnection: isValidConnection || currentStore.isValidConnection,
flowId: currentStore.rfId, getTransform: () => store.getState().transform,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform,
});
}
if (isMouseTriggered) {
onMouseDown?.(event as ReactMouseEvent<HTMLDivElement>);
} else {
onTouchStart?.(event as ReactTouchEvent<HTMLDivElement>);
}
};
const onClick = (event: ReactMouseEvent) => {
const {
onClickConnectStart,
onClickConnectEnd,
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
} = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
return;
}
if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib,
}); });
}
if (isValid && connection) { if (isMouseTriggered) {
onConnectExtended(connection); onMouseDown?.(event as ReactMouseEvent<HTMLDivElement>);
} } else {
onTouchStart?.(event as ReactTouchEvent<HTMLDivElement>);
}
};
onClickConnectEnd?.(event as unknown as MouseEvent); const onClick = (event: ReactMouseEvent) => {
const {
onClickConnectStart,
onClickConnectEnd,
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
} = store.getState();
store.setState({ connectionClickStartHandle: null }); if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
}; return;
}
return ( if (!connectionClickStartHandle) {
<div onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
data-handleid={handleId} store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
data-nodeid={nodeId} return;
data-handlepos={position} }
data-id={`${rfId}-${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
noPanClassName,
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connectablestart: isConnectableStart,
connectableend: isConnectableEnd,
clickconnecting: clickConnecting,
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
);
HandleComponent.displayName = 'Handle'; const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib,
});
if (isValid && connection) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionClickStartHandle: null });
};
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${rfId}-${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
noPanClassName,
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connectablestart: isConnectableStart,
connectableend: isConnectableEnd,
clickconnecting: clickConnecting,
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
/** /**
* The Handle component is the part of a node that can be used to connect nodes. * The Handle component is a UI element that is used to connect nodes.
*/ */
export const Handle = memo(HandleComponent); export const Handle = memo(forwardRef(HandleComponent));
@@ -1,5 +1,4 @@
import type { ComponentType } from 'react'; import type { XYPosition } from '@xyflow/system';
import type { NodeProps, XYPosition } from '@xyflow/system';
import { InputNode } from '../Nodes/InputNode'; import { InputNode } from '../Nodes/InputNode';
import { DefaultNode } from '../Nodes/DefaultNode'; import { DefaultNode } from '../Nodes/DefaultNode';
@@ -15,10 +14,10 @@ export const arrowKeyDiffs: Record<string, XYPosition> = {
}; };
export const builtinNodeTypes: NodeTypes = { export const builtinNodeTypes: NodeTypes = {
input: InputNode as ComponentType<NodeProps>, input: InputNode,
default: DefaultNode as ComponentType<NodeProps>, default: DefaultNode,
output: OutputNode as ComponentType<NodeProps>, output: OutputNode,
group: GroupNode as ComponentType<NodeProps>, group: GroupNode,
}; };
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>( export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
@@ -1,13 +1,14 @@
import { Position, type NodeProps } from '@xyflow/system'; import { Position } from '@xyflow/system';
import { Handle } from '../../components/Handle'; import { Handle } from '../../components/Handle';
import type { BuiltInNode, NodeProps } from '../../types/nodes';
export function DefaultNode({ export function DefaultNode({
data, data,
isConnectable, isConnectable,
targetPosition = Position.Top, targetPosition = Position.Top,
sourcePosition = Position.Bottom, sourcePosition = Position.Bottom,
}: NodeProps) { }: NodeProps<BuiltInNode>) {
return ( return (
<> <>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} /> <Handle type="target" position={targetPosition} isConnectable={isConnectable} />
@@ -1,8 +1,9 @@
import { Position, type NodeProps } from '@xyflow/system'; import { Position } from '@xyflow/system';
import { Handle } from '../../components/Handle'; import { Handle } from '../../components/Handle';
import type { BuiltInNode, NodeProps } from '../../types/nodes';
export function InputNode({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) { export function InputNode({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps<BuiltInNode>) {
return ( return (
<> <>
{data?.label} {data?.label}
@@ -1,8 +1,9 @@
import { Position, type NodeProps } from '@xyflow/system'; import { Position } from '@xyflow/system';
import { Handle } from '../../components/Handle'; import { Handle } from '../../components/Handle';
import type { BuiltInNode, NodeProps } from '../../types/nodes';
export function OutputNode({ data, isConnectable, targetPosition = Position.Top }: NodeProps) { export function OutputNode({ data, isConnectable, targetPosition = Position.Top }: NodeProps<BuiltInNode>) {
return ( return (
<> <>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} /> <Handle type="target" position={targetPosition} isConnectable={isConnectable} />
@@ -172,7 +172,6 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
containerStyle={connectionLineContainerStyle} containerStyle={connectionLineContainerStyle}
/> />
<div className="react-flow__edgelabel-renderer" /> <div className="react-flow__edgelabel-renderer" />
<div className="react-flow__viewport-portal" />
<NodeRenderer<NodeType> <NodeRenderer<NodeType>
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
@@ -189,6 +188,7 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
rfId={rfId} rfId={rfId}
/> />
<div className="react-flow__viewport-portal" />
</Viewport> </Viewport>
</FlowRenderer> </FlowRenderer>
); );
+4 -7
View File
@@ -128,7 +128,8 @@ export function Pane({
}; };
const onMouseMove = (event: ReactMouseEvent): void => { const onMouseMove = (event: ReactMouseEvent): void => {
const { userSelectionRect, edges, transform, nodeOrigin, nodes, onNodesChange, onEdgesChange } = store.getState(); const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } =
store.getState();
if (!isSelecting || !containerBounds.current || !userSelectionRect) { if (!isSelecting || !containerBounds.current || !userSelectionRect) {
return; return;
} }
@@ -172,17 +173,13 @@ export function Pane({
if (prevSelectedNodesCount.current !== selectedNodeIds.size) { if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
prevSelectedNodesCount.current = selectedNodeIds.size; prevSelectedNodesCount.current = selectedNodeIds.size;
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[]; const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
if (changes.length) { triggerNodeChanges(changes);
onNodesChange?.(changes);
}
} }
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) { if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.size; prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) { triggerEdgeChanges(changes);
onEdgesChange?.(changes);
}
} }
store.setState({ store.setState({
@@ -9,7 +9,7 @@ import { StoreUpdater } from '../../components/StoreUpdater';
import { useColorModeClass } from '../../hooks/useColorModeClass'; import { useColorModeClass } from '../../hooks/useColorModeClass';
import { GraphView } from '../GraphView'; import { GraphView } from '../GraphView';
import { Wrapper } from './Wrapper'; import { Wrapper } from './Wrapper';
import type { Edge, Node, ReactFlowProps, ReactFlowRefType } from '../../types'; import type { Edge, Node, ReactFlowProps } from '../../types';
import { defaultViewport as initViewport, defaultNodeOrigin } from './init-values'; import { defaultViewport as initViewport, defaultNodeOrigin } from './init-values';
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
@@ -142,7 +142,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
debug, debug,
...rest ...rest
}: ReactFlowProps<NodeType, EdgeType>, }: ReactFlowProps<NodeType, EdgeType>,
ref: ForwardedRef<ReactFlowRefType> ref: ForwardedRef<HTMLDivElement>
) { ) {
const rfId = id || '1'; const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode); const colorModeClassName = useColorModeClass(colorMode);
@@ -286,4 +286,4 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
); );
} }
export default forwardRef(ReactFlow) as typeof ReactFlow; export default forwardRef(ReactFlow);
+2 -8
View File
@@ -1,15 +1,9 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { shallow } from 'zustand/shallow'; import { shallowNodeData } from '@xyflow/system';
import { useStore } from '../hooks/useStore'; import { useStore } from '../hooks/useStore';
import type { Node } from '../types'; import type { Node } from '../types';
export interface NodeDataReturn<NodeType extends Node> {
id: string;
type: NodeType['type'];
data: NodeType['data'];
}
/** /**
* Hook for receiving data of one or multiple nodes * Hook for receiving data of one or multiple nodes
* *
@@ -46,7 +40,7 @@ export function useNodesData(nodeIds: any): any {
}, },
[nodeIds] [nodeIds]
), ),
shallow shallowNodeData
); );
return nodesData; return nodesData;
-1
View File
@@ -64,7 +64,6 @@ export {
SelectionMode, SelectionMode,
type SelectionRect, type SelectionRect,
type OnError, type OnError,
type NodeProps,
type NodeOrigin, type NodeOrigin,
type OnSelectionDrag, type OnSelectionDrag,
Position, Position,
@@ -509,5 +509,3 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
*/ */
debug?: boolean; debug?: boolean;
} }
export type ReactFlowRefType = HTMLDivElement;
+4 -4
View File
@@ -98,10 +98,10 @@ export type EdgeTextProps = HTMLAttributes<SVGElement> &
* Custom edge component props * Custom edge component props
* @public * @public
*/ */
export type EdgeProps< export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
EdgeData extends Record<string, unknown> = Record<string, unknown>, EdgeType,
EdgeType extends string | undefined = string | undefined 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> = Pick<Edge<EdgeData, EdgeType>, 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'> & > &
EdgePosition & EdgePosition &
EdgeLabelOptions & { EdgeLabelOptions & {
sourceHandleId?: string | null; sourceHandleId?: string | null;
+32 -5
View File
@@ -1,3 +1,4 @@
import { ComponentType } from 'react';
import { import {
FitViewParamsBase, FitViewParamsBase,
FitViewOptionsBase, FitViewOptionsBase,
@@ -9,13 +10,19 @@ import {
SetCenter, SetCenter,
FitBounds, FitBounds,
XYPosition, XYPosition,
NodeProps,
OnBeforeDeleteBase, OnBeforeDeleteBase,
Connection, Connection,
} from '@xyflow/system'; } from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.'; import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
import { ComponentType } from 'react';
// this is needed, to use generics + forwardRef
declare module 'react' {
// eslint-disable-next-line @typescript-eslint/ban-types
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void; export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void; export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
@@ -27,8 +34,28 @@ export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge>
edges: EdgeType[]; edges: EdgeType[];
}) => void; }) => void;
export type NodeTypes = { [key: string]: ComponentType<NodeProps> }; export type NodeTypes = Record<
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> }; string,
ComponentType<
NodeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>;
export type EdgeTypes = Record<
string,
ComponentType<
EdgeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>;
export type UnselectNodesAndEdgesParams = { export type UnselectNodesAndEdgesParams = {
nodes?: Node[]; nodes?: Node[];
+4 -2
View File
@@ -1,5 +1,5 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system'; import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system';
import { NodeTypes } from './general'; import { NodeTypes } from './general';
@@ -9,7 +9,7 @@ import { NodeTypes } from './general';
*/ */
export type Node< export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>, NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string | undefined = string | undefined NodeType extends string = string
> = NodeBase<NodeData, NodeType> & { > = NodeBase<NodeData, NodeType> & {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
@@ -49,3 +49,5 @@ export type NodeWrapperProps<NodeType extends Node> = {
}; };
export type BuiltInNode = Node<{ label: string }, 'input' | 'output' | 'default'>; export type BuiltInNode = Node<{ label: string }, 'input' | 'output' | 'default'>;
export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType>;
+11
View File
@@ -1,5 +1,16 @@
# @xyflow/svelte # @xyflow/svelte
## 0.0.38
## ⚠️ Breaking changes
- `NodeProps` generic is a node and not only node data. `type $$Props = NodeProps<AppNode>`
## Patch changes
- unify `Edge` and `Node` type handling
- fix safari: prevent selection of viewport
## 0.0.37 ## 0.0.37
## ⚠️ Breaking changes ## ⚠️ Breaking changes
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "0.0.37", "version": "0.0.38",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [ "keywords": [
"svelte", "svelte",
@@ -46,9 +46,9 @@
"classcat": "^5.0.4" "classcat": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^2.1.0", "@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^1.22.6", "@sveltejs/kit": "^2.5.2",
"@sveltejs/package": "^2.2.1", "@sveltejs/package": "^2.2.7",
"@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0", "@typescript-eslint/parser": "^5.60.0",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
@@ -65,8 +65,8 @@
"postcss-rename": "^0.6.1", "postcss-rename": "^0.6.1",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.2.0", "svelte": "^4.2.12",
"svelte-check": "^3.5.0", "svelte-check": "^3.6.6",
"svelte-eslint-parser": "^0.32.2", "svelte-eslint-parser": "^0.32.2",
"tslib": "^2.5.3", "tslib": "^2.5.3",
"typescript": "5.1.3" "typescript": "5.1.3"
@@ -51,7 +51,8 @@
edgecontextmenu: { edge: Edge; event: MouseEvent }; edgecontextmenu: { edge: Edge; event: MouseEvent };
}>(); }>();
$: edgeComponent = $edgeTypes[type!] || BezierEdgeInternal; const edgeType = type || 'default';
$: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal;
$: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined; $: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined;
$: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined; $: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined;
$: isSelectable = selectable || ($elementsSelectable && typeof selectable === 'undefined'); $: isSelectable = selectable || ($elementsSelectable && typeof selectable === 'undefined');
@@ -113,6 +114,7 @@
{data} {data}
{style} {style}
{interactionWidth} {interactionWidth}
type={edgeType}
sourceHandleId={sourceHandle} sourceHandleId={sourceHandle}
targetHandleId={targetHandle} targetHandleId={targetHandle}
markerStart={markerStartUrl} markerStart={markerStartUrl}
@@ -1,16 +1,10 @@
<svelte:options immutable /> <svelte:options immutable />
<script lang="ts"> <script lang="ts">
import { import { createEventDispatcher, setContext, onDestroy } from 'svelte';
createEventDispatcher,
setContext,
SvelteComponent,
type ComponentType,
onDestroy
} from 'svelte';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import cc from 'classcat'; import cc from 'classcat';
import { errorMessages, Position, type NodeProps } from '@xyflow/system'; import { errorMessages, Position } from '@xyflow/system';
import drag from '$lib/actions/drag'; import drag from '$lib/actions/drag';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -71,8 +65,7 @@
console.warn('003', errorMessages['error003'](type!)); console.warn('003', errorMessages['error003'](type!));
} }
const nodeComponent: ComponentType<SvelteComponent<NodeProps>> = const nodeComponent = $nodeTypes[nodeType] || DefaultNode;
$nodeTypes[nodeType] || DefaultNode;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
nodeclick: { node: Node; event: MouseEvent | TouchEvent }; nodeclick: { node: Node; event: MouseEvent | TouchEvent };
nodecontextmenu: { node: Node; event: MouseEvent | TouchEvent }; nodecontextmenu: { node: Node; event: MouseEvent | TouchEvent };
@@ -211,11 +204,11 @@
{selected} {selected}
{sourcePosition} {sourcePosition}
{targetPosition} {targetPosition}
{type}
{zIndex} {zIndex}
{dragging} {dragging}
{dragHandle} {dragHandle}
isConnectable={connectable} type={nodeType}
isConnectable={$connectableStore}
positionAbsoluteX={positionX} positionAbsoluteX={positionX}
positionAbsoluteY={positionY} positionAbsoluteY={positionY}
{width} {width}
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = ''; export let id: $$Props['id'] = '';
export let source: $$Props['source'] = ''; export let source: $$Props['source'] = '';
export let target: $$Props['target'] = ''; export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'default';
export let animated: $$Props['animated'] = undefined; export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined; export let selected: $$Props['selected'] = undefined;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
@@ -43,6 +44,7 @@
id; id;
source; source;
target; target;
type;
animated; animated;
selected; selected;
data; data;
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = ''; export let id: $$Props['id'] = '';
export let source: $$Props['source'] = ''; export let source: $$Props['source'] = '';
export let target: $$Props['target'] = ''; export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'smoothstep';
export let animated: $$Props['animated'] = undefined; export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined; export let selected: $$Props['selected'] = undefined;
@@ -43,6 +44,7 @@
id; id;
source; source;
target; target;
type;
animated; animated;
selected; selected;
data; data;
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = ''; export let id: $$Props['id'] = '';
export let source: $$Props['source'] = ''; export let source: $$Props['source'] = '';
export let target: $$Props['target'] = ''; export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'step';
export let animated: $$Props['animated'] = undefined; export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined; export let selected: $$Props['selected'] = undefined;
@@ -44,6 +45,7 @@
id; id;
source; source;
target; target;
type;
animated; animated;
selected; selected;
data; data;
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = ''; export let id: $$Props['id'] = '';
export let source: $$Props['source'] = ''; export let source: $$Props['source'] = '';
export let target: $$Props['target'] = ''; export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'straight';
export let animated: $$Props['animated'] = undefined; export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined; export let selected: $$Props['selected'] = undefined;
@@ -41,6 +42,7 @@
id; id;
source; source;
target; target;
type;
animated; animated;
selected; selected;
data; data;
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Position, type NodeProps } from '@xyflow/system'; import { Position } from '@xyflow/system';
import { Handle } from '$lib/components/Handle'; import { Handle } from '$lib/components/Handle';
import type { NodeProps } from '$lib/types';
interface $$Props extends NodeProps<{ label: string }> {} interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' }; export let data: $$Props['data'] = { label: 'Node' };
export let targetPosition: $$Props['targetPosition'] = Position.Top; export let targetPosition: $$Props['targetPosition'] = Position.Top;
@@ -15,12 +16,12 @@
export let height: $$Props['height'] = undefined; export let height: $$Props['height'] = undefined;
export let selected: $$Props['selected'] = undefined; export let selected: $$Props['selected'] = undefined;
export let type: $$Props['type'] = undefined; export let type: $$Props['type'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0; export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0; export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
id; id;
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { NodeProps } from '@xyflow/system'; import type { NodeProps } from '$lib/types';
interface $$Props extends NodeProps<{}> {} interface $$Props extends NodeProps {}
// unused props - we need to list them here in order to prevent warnings // unused props - we need to list them here in order to prevent warnings
export let id: $$Props['id'] = ''; export let id: $$Props['id'] = '';
@@ -12,12 +12,12 @@
export let sourcePosition: $$Props['sourcePosition'] = undefined; export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined;
export let type: $$Props['type'] = undefined; export let type: $$Props['type'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0; export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0; export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
id; id;
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Position, type NodeProps } from '@xyflow/system'; import { Position } from '@xyflow/system';
import type { NodeProps } from '$lib/types';
import { Handle } from '$lib/components/Handle'; import { Handle } from '$lib/components/Handle';
interface $$Props extends NodeProps<{ label: string }> {} interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' }; export let data: $$Props['data'] = { label: 'Node' };
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom; export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
@@ -15,12 +16,12 @@
export let selected: $$Props['selected'] = undefined; export let selected: $$Props['selected'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined;
export let type: $$Props['type'] = undefined; export let type: $$Props['type'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0; export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0; export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
id; id;
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Position, type NodeProps } from '@xyflow/system'; import { Position } from '@xyflow/system';
import type { NodeProps } from '$lib/types';
import { Handle } from '$lib/components/Handle'; import { Handle } from '$lib/components/Handle';
interface $$Props extends NodeProps<{ label: string }> {} interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' }; export let data: $$Props['data'] = { label: 'Node' };
export let targetPosition: $$Props['targetPosition'] = Position.Top; export let targetPosition: $$Props['targetPosition'] = Position.Top;
@@ -15,12 +16,12 @@
export let selected: $$Props['selected'] = undefined; export let selected: $$Props['selected'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = undefined; export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let type: $$Props['type'] = undefined; export let type: $$Props['type'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0; export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0; export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
id; id;
+2 -15
View File
@@ -1,22 +1,9 @@
import { derived, type Readable } from 'svelte/store'; import { derived, type Readable } from 'svelte/store';
import { shallowNodeData } from '@xyflow/system';
import type { Node } from '$lib/types'; import type { Node } from '$lib/types';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
function areNodesDataEqual(a: any[], b: any[]) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i].data !== b[i].data) {
return false;
}
}
return true;
}
/** /**
* Hook for receiving data of one or multiple nodes * Hook for receiving data of one or multiple nodes
* *
@@ -51,7 +38,7 @@ export function useNodesData(nodeIds: any): any {
} }
} }
if (!areNodesDataEqual(nextNodesData, prevNodesData)) { if (!shallowNodeData(nextNodesData, prevNodesData)) {
prevNodesData = nextNodesData; prevNodesData = nextNodesData;
set(isArrayOfIds ? nextNodesData : nextNodesData[0] ?? null); set(isArrayOfIds ? nextNodesData : nextNodesData[0] ?? null);
} }
+1 -2
View File
@@ -45,7 +45,7 @@ export type {
DefaultEdgeOptions DefaultEdgeOptions
} from '$lib/types/edges'; } from '$lib/types/edges';
export type { HandleComponentProps, FitViewOptions } from '$lib/types/general'; export type { HandleComponentProps, FitViewOptions } from '$lib/types/general';
export type { Node, NodeTypes, DefaultNodeOptions, BuiltInNode } from '$lib/types/nodes'; export type { Node, NodeTypes, DefaultNodeOptions, BuiltInNode, NodeProps } from '$lib/types/nodes';
export type { SvelteFlowStore } from '$lib/store/types'; export type { SvelteFlowStore } from '$lib/store/types';
// system types // system types
@@ -77,7 +77,6 @@ export {
SelectionMode, SelectionMode,
type SelectionRect, type SelectionRect,
type OnError, type OnError,
type NodeProps,
type NodeOrigin, type NodeOrigin,
type OnSelectionDrag, type OnSelectionDrag,
Position, Position,
+20 -5
View File
@@ -49,11 +49,14 @@ export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge;
/** /**
* Custom edge component props. * Custom edge component props.
*/ */
export type EdgeProps< export type EdgeProps<EdgeType extends Edge = Edge> = Omit<
EdgeData extends Record<string, unknown> = Record<string, unknown>, EdgeType,
EdgeType extends string | undefined = string | undefined 'sourceHandle' | 'targetHandle'
> = Omit<Edge<EdgeData, EdgeType>, 'sourceHandle' | 'targetHandle' | 'type'> & > &
EdgePosition & { EdgePosition & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
type: string;
markerStart?: string; markerStart?: string;
markerEnd?: string; markerEnd?: string;
sourceHandleId?: string | null; sourceHandleId?: string | null;
@@ -103,7 +106,19 @@ export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
*/ */
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>; export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>; export type EdgeTypes = Record<
string,
ComponentType<
SvelteComponent<
EdgeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>
>;
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>; export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
+22 -3
View File
@@ -1,5 +1,5 @@
import type { ComponentType, SvelteComponent } from 'svelte'; import type { ComponentType, SvelteComponent } from 'svelte';
import type { NodeBase, NodeProps } from '@xyflow/system'; import type { NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
/** /**
* The node data structure that gets used for the nodes prop. * The node data structure that gets used for the nodes prop.
@@ -7,13 +7,32 @@ import type { NodeBase, NodeProps } from '@xyflow/system';
*/ */
export type Node< export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>, NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string | undefined = string | undefined NodeType extends string = string
> = NodeBase<NodeData, NodeType> & { > = NodeBase<NodeData, NodeType> & {
class?: string; class?: string;
style?: string; style?: string;
}; };
export type NodeTypes = Record<string, ComponentType<SvelteComponent<NodeProps>>>; // @todo: currently generics for nodes are not really supported
// let's fix `type: any` when we migrate to Svelte 5
export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType> & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
};
export type NodeTypes = Record<
string,
ComponentType<
SvelteComponent<
NodeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>
>;
export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>; export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.18", "version": "0.0.19",
"description": "xyflow core system that powers React Flow and Svelte Flow.", "description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [ "keywords": [
"node-based UI", "node-based UI",
+1
View File
@@ -324,6 +324,7 @@ svg.xy-flow__connectionline {
height: 100%; height: 100%;
left: 0; left: 0;
top: 0; top: 0;
user-select: none;
} }
.xy-flow__minimap { .xy-flow__minimap {
+13 -19
View File
@@ -10,7 +10,7 @@ import { Optional } from '../utils/types';
*/ */
export type NodeBase< export type NodeBase<
NodeData extends Record<string, unknown> = Record<string, unknown>, NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string | undefined = string | undefined NodeType extends string = string
> = { > = {
/** Unique id of a node */ /** Unique id of a node */
id: string; id: string;
@@ -82,25 +82,19 @@ export type NodeBase<
* The node data structure that gets used for the nodes prop. * The node data structure that gets used for the nodes prop.
* *
* @public * @public
* @param id - The id of the node.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any export type NodeProps<NodeType extends NodeBase> = Pick<
export type NodeProps<NodeData = any> = { NodeType,
id: NodeBase['id']; 'id' | 'data' | 'width' | 'height' | 'sourcePosition' | 'targetPosition' | 'selected' | 'dragHandle'
data: NodeData; > &
dragHandle: NodeBase['dragHandle']; Required<Pick<NodeType, 'type' | 'dragging' | 'zIndex'>> & {
type: NodeBase['type']; /** whether a node is connectable or not */
selected: NodeBase['selected']; isConnectable: boolean;
isConnectable: NodeBase['connectable']; /** position absolute x value */
zIndex: NodeBase['zIndex']; positionAbsoluteX: number;
positionAbsoluteX: number; /** position absolute x value */
positionAbsoluteY: number; positionAbsoluteY: number;
width?: number; };
height?: number;
dragging: NodeBase['dragging'];
sourcePosition?: NodeBase['sourcePosition'];
targetPosition?: NodeBase['targetPosition'];
};
export type NodeHandleBounds = { export type NodeHandleBounds = {
source: HandleElement[] | null; source: HandleElement[] | null;
+1
View File
@@ -7,3 +7,4 @@ export * from './marker';
export * from './node-toolbar'; export * from './node-toolbar';
export * from './store'; export * from './store';
export * from './types'; export * from './types';
export * from './shallow-node-data';
@@ -0,0 +1,20 @@
import { NodeBase } from '../types';
type NodeData = Pick<NodeBase, 'id' | 'type' | 'data'>;
export function shallowNodeData(a: NodeData | NodeData[], b: NodeData | NodeData[]) {
const _a = Array.isArray(a) ? a : [a];
const _b = Array.isArray(b) ? b : [b];
if (_a.length !== _b.length) {
return false;
}
for (let i = 0; i < _a.length; i++) {
if (_a[i].id !== _b[i].id || _a[i].type !== _b[i].type || !Object.is(_a[i].data, _b[i].data)) {
return false;
}
}
return true;
}
+876 -326
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -30,11 +30,12 @@ test.describe('Edges', () => {
await edge1.click(); await edge1.click();
await expect(edge1).toHaveClass(/selected/); await expect(edge1).toHaveClass(/selected/);
// FIXME: I cannot get this to work // we are using "s" here because Meta doesn't work for some reason
await page.keyboard.down('MetaLeft'); await page.keyboard.down('s');
await edge2.click(); await edge2.click();
await expect(edge2).toHaveClass(/selected/); await page.keyboard.up('s');
await expect(edge2).toHaveClass(/selected/);
await expect(edge1).toHaveClass(/selected/); await expect(edge1).toHaveClass(/selected/);
}); });
}); });
@@ -88,8 +89,6 @@ test.describe('Edges', () => {
await expect(edge).toBeAttached(); await expect(edge).toBeAttached();
const edgeBox = await edge.boundingBox();
await edge.click(); await edge.click();
await expect(edge).toHaveClass(/selected/); await expect(edge).toHaveClass(/selected/);
@@ -154,8 +153,8 @@ test.describe('Edges', () => {
await expect(edge).toBeAttached(); await expect(edge).toBeAttached();
await expect(edge).toHaveAttribute('marker-start', 'url(#1__type=arrowclosed)'); await expect(edge).toHaveAttribute('marker-start', "url('#1__type=arrowclosed')");
await expect(edge).toHaveAttribute('marker-end', 'url(#1__type=arrow)'); await expect(edge).toHaveAttribute('marker-end', "url('#1__type=arrow')");
}); });
}); });
}); });
+7 -6
View File
@@ -52,7 +52,7 @@ test.describe('Pane default', () => {
}); });
test.describe('minZoom & maxZoom', () => { test.describe('minZoom & maxZoom', () => {
test('minZoom works as intended', async ({ page }) => { test('minZoom', async ({ page }) => {
const pane = page.locator(`.${FRAMEWORK}-flow__pane`); const pane = page.locator(`.${FRAMEWORK}-flow__pane`);
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`); const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
@@ -67,7 +67,7 @@ test.describe('Pane default', () => {
expect(transformsMinZoom.scale).toBe(0.25); expect(transformsMinZoom.scale).toBe(0.25);
}); });
test('maxZoom works as intended', async ({ page }) => { test('maxZoom', async ({ page }) => {
const pane = page.locator(`.${FRAMEWORK}-flow__pane`); const pane = page.locator(`.${FRAMEWORK}-flow__pane`);
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`); const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
@@ -84,7 +84,7 @@ test.describe('Pane default', () => {
}); });
test.describe('autoPan', () => { test.describe('autoPan', () => {
test('autoPanOnNodeDrag works as intended', async ({ page }) => { test('autoPanOnNodeDrag', async ({ page }) => {
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`); const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
const node = page.locator('[data-id="1"]'); const node = page.locator('[data-id="1"]');
@@ -96,7 +96,8 @@ test.describe('Pane default', () => {
await page.mouse.down(); await page.mouse.down();
await page.mouse.move(0, 0); await page.mouse.move(0, 0);
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page.mouse.move(100, 100); await page.mouse.move(1000, 100, { steps: 100 });
await page.mouse.up();
const transformAfter = await getTransform(viewport); const transformAfter = await getTransform(viewport);
@@ -104,7 +105,7 @@ test.describe('Pane default', () => {
await expect(transformAfter.translateY).toBeGreaterThan(transformBefore.translateY); await expect(transformAfter.translateY).toBeGreaterThan(transformBefore.translateY);
}); });
test('autoPanOnConnect works as intended', async ({ page }) => { test('autoPanOnConnect', async ({ page }) => {
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`); const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
const handle = page.locator(`[data-id="1"] .${FRAMEWORK}-flow__handle`); const handle = page.locator(`[data-id="1"] .${FRAMEWORK}-flow__handle`);
@@ -116,7 +117,7 @@ test.describe('Pane default', () => {
await page.mouse.down(); await page.mouse.down();
await page.mouse.move(0, 0); await page.mouse.move(0, 0);
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page.mouse.move(100, 100); await page.mouse.move(100, 100, { steps: 100 });
const transformAfter = await getTransform(viewport); const transformAfter = await getTransform(viewport);
+2 -2
View File
@@ -13,11 +13,11 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.40.1", "@playwright/test": "^1.42.1",
"@types/node": "^18.7.16" "@types/node": "^18.7.16"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-react": "^1.40.1", "@playwright/experimental-ct-react": "^1.42.1",
"@types/react": "^18.2.31", "@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.14",
"react": "^18.2.0", "react": "^18.2.0",