diff --git a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx index 4f8ef0ab..863458fb 100644 --- a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx +++ b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx @@ -81,6 +81,8 @@ const initialNodes: Node[] = [ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, + width: 200, + height: 50, type: 'custom', }, ]; diff --git a/examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx b/examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx new file mode 100644 index 00000000..c7fae3d8 --- /dev/null +++ b/examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx @@ -0,0 +1,21 @@ +import { memo, useState } from 'react'; +import { Handle, Position } from '@xyflow/react'; + +function CustomNode() { + const [text, setText] = useState('this is a pretty long text'); + + return ( + <> + +
+
+ setText(e.target.value)} /> +
text: {text}
+
+
+ + + ); +} + +export default memo(CustomNode); diff --git a/examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx b/examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx new file mode 100644 index 00000000..b3f99a6b --- /dev/null +++ b/examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx @@ -0,0 +1,69 @@ +import { useCallback } from 'react'; +import { + ReactFlow, + addEdge, + useEdgesState, + useNodesState, + Background, + Controls, + type Connection, + type Edge, + type Node, +} from '@xyflow/react'; + +import CustomNode from './CustomNode'; + +import '@xyflow/react/dist/style.css'; + +const initialNodes: Node[] = [ + { + id: '1', + data: {}, + position: { x: 0, y: 0 }, + initialWidth: 200, + initialHeight: 50, + type: 'custom', + }, + { + id: '2', + data: {}, + position: { x: 0, y: 200 }, + width: 200, + initialHeight: 50, + type: 'custom', + }, +]; + +const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }]; + +const nodeTypes = { + custom: CustomNode, +}; + +function Flow() { + const [nodes, , onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + + return ( +
+ + + + +
+ ); +} + +export default Flow; diff --git a/examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte new file mode 100644 index 00000000..5bef87c9 --- /dev/null +++ b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte @@ -0,0 +1,25 @@ + + + +
+
+ text: {text} +
+ +
+ + + diff --git a/examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte new file mode 100644 index 00000000..8fbe4905 --- /dev/null +++ b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte @@ -0,0 +1,39 @@ + + +
+ + + + +
diff --git a/examples/astro-xyflow/src/pages/index.astro b/examples/astro-xyflow/src/pages/index.astro index be9d1885..d7b14558 100644 --- a/examples/astro-xyflow/src/pages/index.astro +++ b/examples/astro-xyflow/src/pages/index.astro @@ -1,6 +1,8 @@ --- -import ReactFlowApp from '../components/ReactFlowExample' -import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte' +import ReactFlowApp from '../components/ReactFlowExample'; +import ReactFlowInitialApp from '../components/ReactFlowInitialExample'; +import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'; +import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.svelte'; --- @@ -18,10 +20,24 @@ import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte' +

React Flow

+

no client hydration

+ + +

client hydration on load (client:load)

+ + +

client hydration on load (client:load) and initialWidth / initialHeight

+ +

Svelte Flow

-

React Flow

- +

client hydration on load (client:load)

+ + +

client hydration on load (client:load) and initialWidth / initialHeight

+ + diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 444586ca..7cd3eb13 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -49,6 +49,7 @@ import UseConnection from '../examples/UseConnection'; import UseNodesInitialized from '../examples/UseNodesInit'; import UseNodesData from '../examples/UseNodesData'; import UseHandleConnections from '../examples/UseHandleConnections'; +import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop'; export interface IRoute { name: string; @@ -57,6 +58,11 @@ export interface IRoute { } const routes: IRoute[] = [ + { + name: 'Add Node on edge Drop', + path: 'add-node-edge-drop', + component: AddNodeOnEdgeDrop, + }, { name: 'Basic', path: 'basic', diff --git a/examples/react/src/examples/AddNodeOnEdgeDrop/index.tsx b/examples/react/src/examples/AddNodeOnEdgeDrop/index.tsx new file mode 100644 index 00000000..e1204169 --- /dev/null +++ b/examples/react/src/examples/AddNodeOnEdgeDrop/index.tsx @@ -0,0 +1,99 @@ +import { useCallback, useRef } from 'react'; +import { + ReactFlow, + useNodesState, + useEdgesState, + addEdge, + useReactFlow, + ReactFlowProvider, + OnConnect, + OnConnectStart, + OnConnectEnd, + Node, + Edge, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; + +const initialNodes: Node[] = [ + { + id: '0', + type: 'input', + data: { label: 'Node' }, + position: { x: 0, y: 50 }, + }, +]; + +const initialEdges: Edge[] = []; + +let id = 1; +const getId = () => `${id++}`; + +const AddNodeOnEdgeDrop = () => { + const reactFlowWrapper = useRef(null); + const connectingNodeId = useRef(null); + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const { screenToFlowPosition } = useReactFlow(); + const onConnect: OnConnect = useCallback((params) => { + // reset the start node on connections + connectingNodeId.current = null; + setEdges((eds) => addEdge(params, eds)); + }, []); + + const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => { + connectingNodeId.current = nodeId; + }, []); + + const onConnectEnd: OnConnectEnd = useCallback( + (event) => { + if (!connectingNodeId.current) return; + + const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('react-flow__pane'); + + if (targetIsPane && 'clientX' in event && 'clientY' in event) { + // we need to remove the wrapper bounds, in order to get the correct position + const id = getId(); + const newNode: Node = { + id, + position: screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }), + data: { label: `Node ${id}` }, + origin: [0.5, 0.0], + }; + + const newEdge: Edge = { + id, + source: connectingNodeId.current, + target: id, + }; + + setNodes((nds) => nds.concat(newNode)); + setEdges((eds) => eds.concat(newEdge)); + } + }, + [screenToFlowPosition] + ); + + return ( +
+ +
+ ); +}; + +export default () => ( + + + +); diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index f454b313..a41f50db 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -13,10 +13,13 @@ import { OnNodeDrag, } from '@xyflow/react'; -const onNodeDrag: OnNodeDrag = (_, node) => console.log('drag', node); -const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes); +const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes); +const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); +const printSelectionEvent = (name: string) => (_: MouseEvent, nodes: Node[]) => console.log(name, nodes); + const initialNodes: Node[] = [ { id: '1', @@ -113,7 +116,11 @@ const BasicFlow = () => { defaultEdges={initialEdges} onNodeClick={onNodeClick} onNodeDragStop={onNodeDragStop} + onNodeDragStart={onNodeDragStart} onNodeDrag={onNodeDrag} + onSelectionDragStart={printSelectionEvent('selection drag start')} + onSelectionDrag={printSelectionEvent('selection drag')} + onSelectionDragStop={printSelectionEvent('selection drag stop')} className="react-flow-basic-example" minZoom={0.2} maxZoom={4} diff --git a/examples/react/src/examples/CustomNode/index.tsx b/examples/react/src/examples/CustomNode/index.tsx index a8c0f55d..16a99803 100644 --- a/examples/react/src/examples/CustomNode/index.tsx +++ b/examples/react/src/examples/CustomNode/index.tsx @@ -9,13 +9,14 @@ import { SnapGrid, useEdgesState, Background, - Edge, OnNodeDrag, OnInit, applyNodeChanges, OnNodesChange, OnConnect, OnBeforeDelete, + BuiltInNode, + BuiltInEdge, } from '@xyflow/react'; import ColorSelectorNode from './ColorSelectorNode'; @@ -24,9 +25,10 @@ export type ColorSelectorNode = Node< { color: string; onChange: (event: ChangeEvent) => void }, 'selectorNode' >; -export type MyNode = Node | ColorSelectorNode; +export type MyNode = BuiltInNode | ColorSelectorNode; +export type MyEdge = BuiltInEdge; -const onInit: OnInit = (reactFlowInstance) => { +const onInit: OnInit = (reactFlowInstance) => { console.log('flow loaded:', reactFlowInstance); }; @@ -44,7 +46,7 @@ const nodeTypes = { const CustomNodeFlow = () => { const [nodes, setNodes] = useState([]); - const onNodesChange: OnNodesChange = useCallback( + const onNodesChange: OnNodesChange = useCallback( (changes) => setNodes((nds) => { const nextNodes = applyNodeChanges(changes, nds); @@ -53,7 +55,7 @@ const CustomNodeFlow = () => { [setNodes] ); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [bgColor, setBgColor] = useState(initBgColor); @@ -61,7 +63,7 @@ const CustomNodeFlow = () => { const onChange = (event: ChangeEvent) => { setNodes((nds) => nds.map((node) => { - if (node.id !== '2') { + if (node.id !== '2' || node.type !== 'selectorNode') { return node; } @@ -143,7 +145,7 @@ const CustomNodeFlow = () => { [setEdges] ); - const onBeforeDelete: OnBeforeDelete = useCallback(async (params) => true, []); + const onBeforeDelete: OnBeforeDelete = useCallback(async (params) => true, []); return ( { maxZoom={2} onBeforeDelete={onBeforeDelete} > - nodeStrokeColor={(n: MyNode): string => { if (n.type === 'input') return '#0041d0'; if (n.type === 'selectorNode') return bgColor; diff --git a/examples/react/src/examples/DragHandle/index.tsx b/examples/react/src/examples/DragHandle/index.tsx index b76d8ae7..a7a75fcf 100644 --- a/examples/react/src/examples/DragHandle/index.tsx +++ b/examples/react/src/examples/DragHandle/index.tsx @@ -14,7 +14,7 @@ const initialNodes: Node[] = [ dragHandle: '.custom-drag-handle', style: { border: '1px solid #ddd', padding: '20px 40px' }, position: { x: 200, y: 200 }, - data: null, + data: {}, }, ]; diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index 42e83523..b0a272f2 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -50,7 +50,9 @@ const initialNodes: Node[] = [ maxHeight: 200, }, position: { x: 0, y: 60 }, - style: { ...nodeStyle, width: 100, height: 80 }, + width: 100, + height: 80, + style: { ...nodeStyle }, }, { id: '1b', @@ -64,9 +66,9 @@ const initialNodes: Node[] = [ maxHeight: 400, }, position: { x: 250, y: 0 }, + width: 174, + height: 123, style: { - width: 174, - height: 123, ...nodeStyle, }, }, @@ -75,7 +77,9 @@ const initialNodes: Node[] = [ type: 'customResizer', data: { label: 'custom resize icon' }, position: { x: 0, y: 200 }, - style: { width: 100, height: 60, ...nodeStyle }, + width: 100, + height: 60, + style: { ...nodeStyle }, }, { id: '3', @@ -94,7 +98,8 @@ const initialNodes: Node[] = [ keepAspectRatio: true, }, position: { x: 400, y: 200 }, - style: { ...nodeStyle, height: 50 }, + height: 50, + style: { ...nodeStyle }, }, { id: '4', @@ -121,7 +126,9 @@ const initialNodes: Node[] = [ type: 'defaultResizer', data: { label: 'Parent', keepAspectRatio: true }, position: { x: 700, y: 0 }, - style: { ...nodeStyle, width: 300, height: 400 }, + width: 300, + height: 400, + style: { ...nodeStyle }, }, { id: '5a', @@ -132,7 +139,9 @@ const initialNodes: Node[] = [ position: { x: 50, y: 50 }, parentNode: '5', extent: 'parent', - style: { ...nodeStyle, width: 50, height: 100 }, + width: 50, + height: 100, + style: { ...nodeStyle }, }, { id: '5b', diff --git a/examples/react/src/examples/TouchDevice/index.tsx b/examples/react/src/examples/TouchDevice/index.tsx index 636a36a4..e412bee5 100644 --- a/examples/react/src/examples/TouchDevice/index.tsx +++ b/examples/react/src/examples/TouchDevice/index.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { ReactFlow, Node, Edge, useNodesState, useEdgesState, Position, Connection, addEdge } from '@xyflow/react'; -import styles from './touch-device.module.css'; +import './touch-device.css'; const initialNodes: Node[] = [ { @@ -42,7 +42,7 @@ const TouchDeviceFlow = () => { onConnectEnd={onConnectEnd} onClickConnectStart={onClickConnectStart} onClickConnectEnd={onClickConnectEnd} - className={styles.flow} + className="touch-flow" /> ); }; diff --git a/examples/react/src/examples/TouchDevice/touch-device.css b/examples/react/src/examples/TouchDevice/touch-device.css new file mode 100644 index 00000000..92f85c27 --- /dev/null +++ b/examples/react/src/examples/TouchDevice/touch-device.css @@ -0,0 +1,27 @@ +.react-flow.touch-flow .react-flow__handle { + width: 20px; + height: 20px; + border-radius: 3px; + background-color: #9f7aea; +} + +.touch-flow .react-flow__handle-right { + --translate: translate(50%, -50%); +} + +.touch-flow .react-flow__handle-left { + --translate: translate(-50%, -50%); +} + +@keyframes bounce { + 0% { + transform: var(--translate) scale(1); + } + 50% { + transform: var(--translate) scale(1.1); + } +} + +.react-flow.touch-flow .react-flow__handle.clickconnecting { + animation: bounce 1600ms infinite ease-in; +} diff --git a/examples/react/src/examples/TouchDevice/touch-device.module.css b/examples/react/src/examples/TouchDevice/touch-device.module.css deleted file mode 100644 index b5f65209..00000000 --- a/examples/react/src/examples/TouchDevice/touch-device.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.flow :global .react-flow__handle { - width: 20px; - height: 20px; - border-radius: 3px; - background-color: #9f7aea; -} - -.flow :global .react-flow__handle.connecting { - animation: bounce 1600ms infinite ease-out; -} - -@keyframes bounce { - 0% { - transform: translate(0, -50%) scale(1); - } - 50% { - transform: translate(0, -50%) scale(1.1); - } -} diff --git a/examples/react/src/examples/UseNodesData/ResultNode.tsx b/examples/react/src/examples/UseNodesData/ResultNode.tsx index ac9777dd..bf8f7a35 100644 --- a/examples/react/src/examples/UseNodesData/ResultNode.tsx +++ b/examples/react/src/examples/UseNodesData/ResultNode.tsx @@ -1,20 +1,18 @@ import { memo } from 'react'; import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react'; +import { isTextNode, type MyNode } from '.'; function ResultNode() { const connections = useHandleConnections({ type: 'target', }); - const nodesData = useNodesData(connections.map((connection) => connection.source)); + const nodesData = useNodesData(connections.map((connection) => connection.source)); + const textNodes = nodesData.filter(isTextNode); return (
-
- incoming texts:{' '} - {nodesData?.filter((nodeData) => nodeData.text !== undefined).map(({ text }, i) =>
{text}
) || - 'none'} -
+
incoming texts: {textNodes.map(({ data }, i) =>
{data.text}
) || 'none'}
); } diff --git a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx index 699dc37e..70c6079d 100644 --- a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx +++ b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx @@ -1,16 +1,18 @@ import { memo, useEffect } from 'react'; import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react'; +import { isTextNode, type TextNode, type MyNode } from '.'; function UppercaseNode({ id }: NodeProps) { const { updateNodeData } = useReactFlow(); const connections = useHandleConnections({ type: 'target', }); - const nodeData = useNodesData(connections[0]?.source); + const nodesData = useNodesData(connections[0]?.source); + const textNode = isTextNode(nodesData) ? nodesData : null; useEffect(() => { - updateNodeData(id, { text: nodeData?.text.toUpperCase() }); - }, [nodeData]); + updateNodeData(id, { text: textNode?.data.text.toUpperCase() }); + }, [textNode]); return (
diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx index cbeb2478..91cc1f94 100644 --- a/examples/react/src/examples/UseNodesData/index.tsx +++ b/examples/react/src/examples/UseNodesData/index.tsx @@ -17,8 +17,12 @@ import UppercaseNode from './UppercaseNode'; export type TextNode = Node<{ text: string }, 'text'>; export type ResultNode = Node<{}, 'result'>; -export type UppercaseNode = Node<{}, 'uppercase'>; -export type MyNode = Node | TextNode | ResultNode | UppercaseNode; +export type UppercaseNode = Node<{ text: string }, 'uppercase'>; +export type MyNode = TextNode | ResultNode | UppercaseNode; + +export function isTextNode(node: any): node is TextNode { + return node.type === 'text'; +} const nodeTypes = { text: TextNode, @@ -38,7 +42,7 @@ const initNodes: MyNode[] = [ { id: '1a', type: 'uppercase', - data: {}, + data: { text: '' }, position: { x: 100, y: 0 }, }, { diff --git a/examples/react/src/examples/Validation/validation.module.css b/examples/react/src/examples/Validation/validation.module.css index c19a8723..3fbe2f94 100644 --- a/examples/react/src/examples/Validation/validation.module.css +++ b/examples/react/src/examples/Validation/validation.module.css @@ -24,10 +24,14 @@ background: #fff; } -.validationflow :global .connecting { +.validationflow :global .connectingto { background: #ff6060; } +.validationflow :global .react-flow__node-custominput .connectingfrom { + background: #55dd99; +} + .validationflow :global .valid { background: #55dd99; } diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 90cdc62b..007d8ded 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -23,7 +23,8 @@ 'usenodesdata', 'usesvelteflow', 'useupdatenodeinternals', - 'validation' + 'validation', + 'reset' ]; const onChange = (event: Event) => { diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte index 39778156..b7479b03 100644 --- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte @@ -111,9 +111,10 @@ { id: '5a', type: 'defaultResizer', - data: { label: 'Child' }, + data: { label: 'Child with extent parent' }, position: { x: 50, y: 50 }, parentNode: '5', + extent: 'parent', style: nodeStyle }, { @@ -137,6 +138,7 @@ maxZoom={5} snapGrid={snapToGrid ? [10, 10] : undefined} fitView + nodeOrigin={[0.5, 0.5]} > diff --git a/examples/svelte/src/routes/examples/reset/+page.svelte b/examples/svelte/src/routes/examples/reset/+page.svelte new file mode 100644 index 00000000..78b1b245 --- /dev/null +++ b/examples/svelte/src/routes/examples/reset/+page.svelte @@ -0,0 +1,43 @@ + + + + +
+ + + + + +
diff --git a/examples/svelte/src/routes/examples/usenodesdata/+page.svelte b/examples/svelte/src/routes/examples/usenodesdata/+page.svelte index 7b2d2204..fc85ee26 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/+page.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/+page.svelte @@ -1,3 +1,17 @@ + +
incoming texts:
- {#each $nodeData as data} -
{data.text}
+ {#each textNodes as textNode} +
{textNode.data.text}
{/each}
diff --git a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte index 2dcf3bde..be15ac9a 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte @@ -7,6 +7,7 @@ useSvelteFlow, type NodeProps } from '@xyflow/svelte'; + import { isTextNode, type MyNode } from './+page.svelte'; type $$Props = NodeProps; @@ -18,10 +19,11 @@ type: 'target' }); - $: nodeData = useNodesData($connections[0]?.source); + $: nodeData = useNodesData($connections[0]?.source); + $: textNode = isTextNode($nodeData) ? $nodeData : null; $: { - updateNodeData(id, { text: $nodeData?.text?.toUpperCase() || '' }); + updateNodeData(id, { text: textNode?.data.text.toUpperCase() || '' }); } diff --git a/examples/svelte/src/routes/examples/validation/style.css b/examples/svelte/src/routes/examples/validation/style.css index 6839e129..a6bbf75a 100644 --- a/examples/svelte/src/routes/examples/validation/style.css +++ b/examples/svelte/src/routes/examples/validation/style.css @@ -1,7 +1,11 @@ -.svelte-flow__handle.connecting { +.svelte-flow__handle.connectingto { background: #ff6060; } +.svelte-flow__handle.connectingfrom { + background: #55dd99; +} + .svelte-flow__handle.valid { background: #55dd99; } diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index e2e23ca4..051fdbd9 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,20 @@ # @xyflow/react +## 12.0.0-next.10 + +## ⚠️ Breaking changes + +- `useNodesData` not only returns data objects but also the type and the id of the node +- status class names for Handle components are slightly different. It's now "connectingfrom" and "connectingto" instead of "connecting" + +## Patch changes + +- better cursor defaults for the pane, nodes and edges +- `disableKeyboardA11y` now also disables Enter and Escape for selecting/deselecting nodes and edges +- fix bug where users couldn't drag a node after toggle nodes `hidden` attribute +- add `initialWidth` and `initialHeight` node attributes for specifying initial dimensions for ssr +- fix `NodeResizer` when used in combination with `nodeOrigin` + ## 12.0.0-next.9 ### Patch changes diff --git a/packages/react/package.json b/packages/react/package.json index ddb6d327..d9105c4f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/react", - "version": "12.0.0-next.9", + "version": "12.0.0-next.10", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "keywords": [ "react", diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index f841c8b6..17a3f7f2 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -7,7 +7,7 @@ import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapI import { useStore, useStoreApi } from '../../hooks/useStore'; import { Panel } from '../../components/Panel'; -import type { ReactFlowState } from '../../types'; +import type { ReactFlowState, Node } from '../../types'; import MiniMapNodes from './MiniMapNodes'; import type { MiniMapProps } from './types'; @@ -38,7 +38,7 @@ const selector = (s: ReactFlowState) => { const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; -function MiniMapComponent({ +function MiniMapComponent({ style, className, nodeStrokeColor, @@ -62,8 +62,8 @@ function MiniMapComponent({ inversePan, zoomStep = 10, offsetScale = 5, -}: MiniMapProps) { - const store = useStoreApi(); +}: MiniMapProps) { + const store = useStoreApi(); const svg = useRef(null); const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight } = useStore(selector, shallow); const elementWidth = (style?.width as number) ?? defaultWidth; @@ -155,7 +155,7 @@ function MiniMapComponent({ onClick={onSvgClick} > {ariaLabel && {ariaLabel}} - onClick={onSvgNodeClick} nodeColor={nodeColor} nodeStrokeColor={nodeStrokeColor} @@ -178,4 +178,4 @@ function MiniMapComponent({ MiniMapComponent.displayName = 'MiniMap'; -export const MiniMap = memo(MiniMapComponent); +export const MiniMap = memo(MiniMapComponent) as typeof MiniMapComponent; diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx index 63851aad..0abe3177 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx @@ -1,21 +1,22 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComponentType, memo } from 'react'; -import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system'; +import { NodeOrigin, getNodeDimensions, getNodePositionWithOrigin, nodeHasDimensions } from '@xyflow/system'; import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; import { MiniMapNode } from './MiniMapNode'; -import type { ReactFlowState } from '../../types'; +import type { ReactFlowState, Node } from '../../types'; import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types'; declare const window: any; const selector = (s: ReactFlowState) => s.nodeOrigin; const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id); -const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); +const getAttrFunction = (func: any): GetMiniMapNodeAttribute => + func instanceof Function ? func : () => func; -function MiniMapNodes({ +function MiniMapNodes({ nodeStrokeColor, nodeColor, nodeClassName = '', @@ -25,12 +26,12 @@ function MiniMapNodes({ // a component properly. nodeComponent: NodeComponent = MiniMapNode, onClick, -}: MiniMapNodesProps) { +}: MiniMapNodesProps) { const nodeIds = useStore(selectorNodeIds, shallow); const nodeOrigin = useStore(selector); - const nodeColorFunc = getAttrFunction(nodeColor); - const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); - const nodeClassNameFunc = getAttrFunction(nodeClassName); + const nodeColorFunc = getAttrFunction(nodeColor); + const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); + const nodeClassNameFunc = getAttrFunction(nodeClassName); const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; @@ -42,7 +43,7 @@ function MiniMapNodes({ // minimize the cost of updates when individual nodes change. // // For more details, see a similar commit in `NodeRenderer/index.tsx`. - key={nodeId} id={nodeId} nodeOrigin={nodeOrigin} @@ -60,7 +61,7 @@ function MiniMapNodes({ ); } -const NodeComponentWrapper = memo(function NodeComponentWrapper({ +function NodeComponentWrapperInner({ id, nodeOrigin, nodeColorFunc, @@ -74,9 +75,9 @@ const NodeComponentWrapper = memo(function NodeComponentWrapper({ }: { id: string; nodeOrigin: NodeOrigin; - nodeColorFunc: GetMiniMapNodeAttribute; - nodeStrokeColorFunc: GetMiniMapNodeAttribute; - nodeClassNameFunc: GetMiniMapNodeAttribute; + nodeColorFunc: GetMiniMapNodeAttribute; + nodeStrokeColorFunc: GetMiniMapNodeAttribute; + nodeClassNameFunc: GetMiniMapNodeAttribute; nodeBorderRadius: number; nodeStrokeWidth?: number; NodeComponent: ComponentType; @@ -84,7 +85,7 @@ const NodeComponentWrapper = memo(function NodeComponentWrapper({ shapeRendering: string; }) { const { node, x, y } = useStore((s) => { - const node = s.nodeLookup.get(id); + const node = s.nodeLookup.get(id) as NodeType; const { x, y } = getNodePositionWithOrigin(node, node?.origin || nodeOrigin).positionAbsolute; return { @@ -93,16 +94,19 @@ const NodeComponentWrapper = memo(function NodeComponentWrapper({ y, }; }, shallow); - if (!node || node.hidden || !(node.computed?.width || node.width) || !(node.computed?.height || node.height)) { + + if (!node || node.hidden || !nodeHasDimensions(node)) { return null; } + const { width, height } = getNodeDimensions(node); + return ( ); -}); +} -export default memo(MiniMapNodes); +const NodeComponentWrapper = memo(NodeComponentWrapperInner) as typeof NodeComponentWrapperInner; + +export default memo(MiniMapNodes) as typeof MiniMapNodes; diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index c03c023e..1a1107b1 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -50,12 +50,13 @@ function ResizeControl({ domNode: resizeControlRef.current, nodeId: id, getStoreItems: () => { - const { nodeLookup, transform, snapGrid, snapToGrid } = store.getState(); + const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin } = store.getState(); return { nodeLookup, transform, snapGrid, snapToGrid, + nodeOrigin, }; }, onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index db56017c..c84c2d3b 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -5,20 +5,20 @@ import { EdgeAnchor } from '../Edges/EdgeAnchor'; import type { EdgeWrapperProps, Edge } from '../../types/edges'; import { useStoreApi } from '../../hooks/useStore'; -type EdgeUpdateAnchorsProps = { - edge: Edge; +type EdgeUpdateAnchorsProps = { + edge: EdgeType; isUpdatable: boolean | 'source' | 'target'; edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius']; sourceHandleId: Edge['sourceHandle']; targetHandleId: Edge['targetHandle']; - onEdgeUpdate: EdgeWrapperProps['onEdgeUpdate']; - onEdgeUpdateStart: EdgeWrapperProps['onEdgeUpdateStart']; - onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd']; + onEdgeUpdate: EdgeWrapperProps['onEdgeUpdate']; + onEdgeUpdateStart: EdgeWrapperProps['onEdgeUpdateStart']; + onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd']; setUpdateHover: (hover: boolean) => void; setUpdating: (updating: boolean) => void; } & EdgePosition; -export function EdgeUpdateAnchors({ +export function EdgeUpdateAnchors({ isUpdatable, edgeUpdaterRadius, edge, @@ -35,7 +35,7 @@ export function EdgeUpdateAnchors({ onEdgeUpdateEnd, setUpdating, setUpdateHover, -}: EdgeUpdateAnchorsProps) { +}: EdgeUpdateAnchorsProps) { const store = useStoreApi(); const handleEdgeUpdater = (event: React.MouseEvent, isSourceHandle: boolean) => { diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 5973751d..3baea708 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -13,9 +13,9 @@ import { useStoreApi, useStore } from '../../hooks/useStore'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { builtinEdgeTypes, nullPosition } from './utils'; import { EdgeUpdateAnchors } from './EdgeUpdateAnchors'; -import type { EdgeWrapperProps } from '../../types'; +import type { Edge, EdgeWrapperProps } from '../../types'; -export function EdgeWrapper({ +export function EdgeWrapper({ id, edgesFocusable, edgesUpdatable, @@ -34,8 +34,9 @@ export function EdgeWrapper({ edgeTypes, noPanClassName, onError, -}: EdgeWrapperProps): JSX.Element | null { - let edge = useStore((s) => s.edgeLookup.get(id)!); + disableKeyboardA11y, +}: EdgeWrapperProps): JSX.Element | null { + let edge = useStore((s) => s.edgeLookup.get(id)!) as EdgeType; const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions); edge = defaultEdgeOptions ? { ...defaultEdgeOptions, ...edge } : edge; @@ -160,7 +161,7 @@ export function EdgeWrapper({ : undefined; const onKeyDown = (event: KeyboardEvent) => { - if (elementSelectionKeys.includes(event.key) && isSelectable) { + if (!disableKeyboardA11y && elementSelectionKeys.includes(event.key) && isSelectable) { const { unselectNodesAndEdges, addSelectedEdges } = store.getState(); const unselect = event.key === 'Escape'; @@ -236,7 +237,7 @@ export function EdgeWrapper({ /> )} {isUpdatable && ( - edge={edge} isUpdatable={isUpdatable} edgeUpdaterRadius={edgeUpdaterRadius} diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 36778039..75c36b8c 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -16,6 +16,7 @@ import { type HandleProps, type Connection, type HandleType, + ConnectionMode, } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; @@ -36,14 +37,24 @@ const connectingSelector = connectionStartHandle: startHandle, connectionEndHandle: endHandle, connectionClickStartHandle: clickHandle, + connectionMode, + connectionStatus, } = state; + const connectingTo = endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type; + return { - connecting: - (startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type) || - (endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type), + connectingFrom: + startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type, + connectingTo, clickConnecting: clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type, + isPossibleEndHandle: + connectionMode === ConnectionMode.Strict + ? startHandle?.type !== type + : nodeId !== startHandle?.nodeId || handleId !== startHandle?.handleId, + connectionInProcess: !!startHandle, + valid: connectingTo && connectionStatus === 'valid', }; }; @@ -71,7 +82,10 @@ const HandleComponent = forwardRef( const store = useStoreApi(); const nodeId = useNodeId(); const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow); - const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type), shallow); + const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore( + connectingSelector(nodeId, handleId, type), + shallow + ); if (!nodeId) { store.getState().onError?.('010', errorMessages['error010']()); @@ -201,10 +215,16 @@ const HandleComponent = forwardRef( connectable: isConnectable, connectablestart: isConnectableStart, connectableend: isConnectableEnd, - connecting: clickConnecting, - // this class is used to style the handle when the user is connecting + 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 && ((isConnectableStart && !connecting) || (isConnectableEnd && connecting)), + isConnectable && + (!connectionInProcess || isPossibleEndHandle) && + (connectionInProcess ? isConnectableEnd : isConnectableStart), }, ])} onMouseDown={onPointerDown} diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 548a4aa4..8c207cd9 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -5,9 +5,11 @@ import { clampPosition, elementSelectionKeys, errorMessages, + getNodeDimensions, getPositionWithOrigin, internalsSymbol, isInputDOMNode, + nodeHasDimensions, } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; @@ -16,10 +18,10 @@ import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions'; import { useDrag } from '../../hooks/useDrag'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { handleNodeClick } from '../Nodes/utils'; -import { arrowKeyDiffs, builtinNodeTypes } from './utils'; -import type { NodeWrapperProps } from '../../types'; +import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; +import type { Node, NodeWrapperProps } from '../../types'; -export function NodeWrapper({ +export function NodeWrapper({ id, onClick, onMouseEnter, @@ -40,9 +42,9 @@ export function NodeWrapper({ nodeExtent, nodeOrigin, onError, -}: NodeWrapperProps) { +}: NodeWrapperProps) { const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => { - const node = s.nodeLookup.get(id)!; + const node = s.nodeLookup.get(id)! as NodeType; const positionAbsolute = nodeExtent ? clampPosition(node.computed?.positionAbsolute, nodeExtent) @@ -79,11 +81,9 @@ export function NodeWrapper({ const prevTargetPosition = useRef(node.targetPosition); const prevType = useRef(nodeType); - const width = node.width ?? undefined; - const height = node.height ?? undefined; - const computedWidth = node.computed?.width; - const computedHeight = node.computed?.height; - const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); + const nodeDimensions = getNodeDimensions(node); + const inlineDimensions = getNodeInlineStyleDimensions(node); + const initialized = nodeHasDimensions(node); const hasHandleBounds = !!node[internalsSymbol]?.handleBounds; const moveSelectedNodes = useMoveSelectedNodes(); @@ -143,8 +143,7 @@ export function NodeWrapper({ const positionAbsoluteOrigin = getPositionWithOrigin({ x: positionAbsoluteX, y: positionAbsoluteY, - width: computedWidth ?? width ?? 0, - height: computedHeight ?? height ?? 0, + ...nodeDimensions, origin: node.origin || nodeOrigin, }); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; @@ -174,7 +173,7 @@ export function NodeWrapper({ }; const onKeyDown = (event: KeyboardEvent) => { - if (isInputDOMNode(event.nativeEvent)) { + if (isInputDOMNode(event.nativeEvent) || disableKeyboardA11y) { return; } @@ -187,12 +186,7 @@ export function NodeWrapper({ unselect, nodeRef, }); - } else if ( - !disableKeyboardA11y && - isDraggable && - node.selected && - Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key) - ) { + } else if (isDraggable && node.selected && Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) { store.setState({ ariaLiveMessage: `Moved selected node ${event.key .replace('Arrow', '') @@ -220,6 +214,7 @@ export function NodeWrapper({ selected: node.selected, selectable: isSelectable, parent: isParent, + draggable: isDraggable, dragging, }, ])} @@ -230,8 +225,7 @@ export function NodeWrapper({ pointerEvents: hasPointerEvents ? 'all' : 'none', visibility: initialized ? 'visible' : 'hidden', ...node.style, - width: width ?? node.style?.width, - height: height ?? node.style?.height, + ...inlineDimensions, }} data-id={id} data-testid={`rf__node-${id}`} @@ -252,8 +246,6 @@ export function NodeWrapper({ id={id} data={node.data} type={nodeType} - width={computedWidth} - height={computedHeight} positionAbsoluteX={positionAbsoluteX} positionAbsoluteY={positionAbsoluteY} selected={node.selected} @@ -263,6 +255,7 @@ export function NodeWrapper({ dragging={dragging} dragHandle={node.dragHandle} zIndex={zIndex} + {...nodeDimensions} />
diff --git a/packages/react/src/components/NodeWrapper/utils.tsx b/packages/react/src/components/NodeWrapper/utils.tsx index 5dd73964..f98b6bb6 100644 --- a/packages/react/src/components/NodeWrapper/utils.tsx +++ b/packages/react/src/components/NodeWrapper/utils.tsx @@ -5,7 +5,7 @@ import { InputNode } from '../Nodes/InputNode'; import { DefaultNode } from '../Nodes/DefaultNode'; import { GroupNode } from '../Nodes/GroupNode'; import { OutputNode } from '../Nodes/OutputNode'; -import type { NodeTypes } from '../../types'; +import type { Node, NodeTypes } from '../../types'; export const arrowKeyDiffs: Record = { ArrowUp: { x: 0, y: -1 }, @@ -20,3 +20,22 @@ export const builtinNodeTypes: NodeTypes = { output: OutputNode as ComponentType, group: GroupNode as ComponentType, }; + +export function getNodeInlineStyleDimensions( + node: NodeType +): { + width: number | string | undefined; + height: number | string | undefined; +} { + if (!node.computed) { + return { + width: node.width ?? node.initialWidth ?? node.style?.width, + height: node.height ?? node.initialHeight ?? node.style?.height, + }; + } + + return { + width: node.width ?? node.style?.width, + height: node.height ?? node.style?.height, + }; +} diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index 9a88c80b..6c037ba5 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -2,7 +2,6 @@ * The nodes selection rectangle gets displayed when a user * made a selection with on or several nodes */ - import { useRef, useEffect, type MouseEvent, type KeyboardEvent } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; @@ -14,8 +13,8 @@ import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { arrowKeyDiffs } from '../NodeWrapper/utils'; import type { Node, ReactFlowState } from '../../types'; -export type NodesSelectionProps = { - onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; +export type NodesSelectionProps = { + onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void; noPanClassName?: string; disableKeyboardA11y: boolean; }; @@ -32,8 +31,12 @@ const selector = (s: ReactFlowState) => { }; }; -export function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y }: NodesSelectionProps) { - const store = useStoreApi(); +export function NodesSelection({ + onSelectionContextMenu, + noPanClassName, + disableKeyboardA11y, +}: NodesSelectionProps) { + const store = useStoreApi(); const { width, height, transformString, userSelectionActive } = useStore(selector, shallow); const moveSelectedNodes = useMoveSelectedNodes(); diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index f1d68e6c..6d92e8b4 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -65,10 +65,16 @@ const reactFlowFieldsToTrack = [ 'selectNodesOnDrag', 'nodeDragThreshold', 'onBeforeDelete', + 'debug', ] as const; type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number]; -type StoreUpdaterProps = Pick & { rfId: string }; +type StoreUpdaterProps = Pick< + ReactFlowProps, + ReactFlowFieldsToTrack +> & { + rfId: string; +}; // rfId doesn't exist in ReactFlowProps, but it's one of the fields we want to update const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const; @@ -97,7 +103,9 @@ const initPrevValues = { rfId: '1', }; -export function StoreUpdater(props: StoreUpdaterProps) { +export function StoreUpdater( + props: StoreUpdaterProps +) { const { setNodes, setEdges, @@ -108,7 +116,7 @@ export function StoreUpdater(props: StoreUpdaterProps) { reset, setDefaultNodesAndEdges, } = useStore(selector, shallow); - const store = useStoreApi(); + const store = useStoreApi(); useEffect(() => { setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges); @@ -120,7 +128,7 @@ export function StoreUpdater(props: StoreUpdaterProps) { }; }, []); - const previousFields = useRef>(initPrevValues); + const previousFields = useRef>>(initPrevValues); useEffect( () => { diff --git a/packages/react/src/container/EdgeRenderer/index.tsx b/packages/react/src/container/EdgeRenderer/index.tsx index 5fdaa1af..6636e833 100644 --- a/packages/react/src/container/EdgeRenderer/index.tsx +++ b/packages/react/src/container/EdgeRenderer/index.tsx @@ -6,10 +6,10 @@ import { useVisibleEdgeIds } from '../../hooks/useVisibleEdgeIds'; import MarkerDefinitions from './MarkerDefinitions'; import { GraphViewProps } from '../GraphView'; import { EdgeWrapper } from '../../components/EdgeWrapper'; -import type { ReactFlowState } from '../../types'; +import type { Edge, ReactFlowState, Node } from '../../types'; -type EdgeRendererProps = Pick< - GraphViewProps, +type EdgeRendererProps = Pick< + GraphViewProps, | 'onEdgeClick' | 'onEdgeDoubleClick' | 'defaultMarkerColor' @@ -40,7 +40,7 @@ const selector = (s: ReactFlowState) => ({ onError: s.onError, }); -function EdgeRendererComponent({ +function EdgeRendererComponent({ defaultMarkerColor, onlyRenderVisibleElements, rfId, @@ -56,7 +56,8 @@ function EdgeRendererComponent({ onEdgeDoubleClick, onEdgeUpdateStart, onEdgeUpdateEnd, -}: EdgeRendererProps) { + disableKeyboardA11y, +}: EdgeRendererProps) { const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements); @@ -66,7 +67,7 @@ function EdgeRendererComponent({ {edgeIds.map((id) => { return ( - key={id} id={id} edgesFocusable={edgesFocusable} @@ -86,6 +87,7 @@ function EdgeRendererComponent({ rfId={rfId} onError={onError} edgeTypes={edgeTypes} + disableKeyboardA11y={disableKeyboardA11y} /> ); })} @@ -95,4 +97,4 @@ function EdgeRendererComponent({ EdgeRendererComponent.displayName = 'EdgeRenderer'; -export const EdgeRenderer = memo(EdgeRendererComponent); +export const EdgeRenderer = memo(EdgeRendererComponent) as typeof EdgeRendererComponent; diff --git a/packages/react/src/container/FlowRenderer/index.tsx b/packages/react/src/container/FlowRenderer/index.tsx index 2670be70..14216ac2 100644 --- a/packages/react/src/container/FlowRenderer/index.tsx +++ b/packages/react/src/container/FlowRenderer/index.tsx @@ -7,10 +7,10 @@ import { GraphViewProps } from '../GraphView'; import { ZoomPane } from '../ZoomPane'; import { Pane } from '../Pane'; import { NodesSelection } from '../../components/NodesSelection'; -import type { ReactFlowState } from '../../types'; +import type { ReactFlowState, Node } from '../../types'; -export type FlowRendererProps = Omit< - GraphViewProps, +export type FlowRendererProps = Omit< + GraphViewProps, | 'snapToGrid' | 'nodeTypes' | 'edgeTypes' @@ -32,7 +32,7 @@ const selector = (s: ReactFlowState) => { return { nodesSelectionActive: s.nodesSelectionActive, userSelectionActive: s.userSelectionActive }; }; -const FlowRendererComponent = ({ +function FlowRendererComponent({ children, onPaneClick, onPaneMouseEnter, @@ -68,7 +68,7 @@ const FlowRendererComponent = ({ disableKeyboardA11y, onViewportChange, isControlledViewport, -}: FlowRendererProps) => { +}: FlowRendererProps) { const { nodesSelectionActive, userSelectionActive } = useStore(selector); const selectionKeyPressed = useKeyPress(selectionKeyCode); const panActivationKeyPressed = useKeyPress(panActivationKeyCode); @@ -125,8 +125,8 @@ const FlowRendererComponent = ({ ); -}; +} FlowRendererComponent.displayName = 'FlowRenderer'; -export const FlowRenderer = memo(FlowRendererComponent); +export const FlowRenderer = memo(FlowRendererComponent) as typeof FlowRendererComponent; diff --git a/packages/react/src/container/GraphView/index.tsx b/packages/react/src/container/GraphView/index.tsx index b95fa610..80c25dea 100644 --- a/packages/react/src/container/GraphView/index.tsx +++ b/packages/react/src/container/GraphView/index.tsx @@ -8,15 +8,15 @@ import { useOnInitHandler } from '../../hooks/useOnInitHandler'; import { useViewportSync } from '../../hooks/useViewportSync'; import { ConnectionLineWrapper } from '../../components/ConnectionLine'; import { useNodeOrEdgeTypesWarning } from './useNodeOrEdgeTypesWarning'; -import type { ReactFlowProps } from '../../types'; +import type { Edge, Node, ReactFlowProps } from '../../types'; -export type GraphViewProps = Omit< - ReactFlowProps, +export type GraphViewProps = Omit< + ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'onMove' | 'onMoveStart' | 'onMoveEnd' | 'elevateEdgesOnSelect' > & Required< Pick< - ReactFlowProps, + ReactFlowProps, | 'selectionKeyCode' | 'deleteKeyCode' | 'multiSelectionKeyCode' @@ -37,7 +37,7 @@ export type GraphViewProps = Omit< rfId: string; }; -function GraphViewComponent({ +function GraphViewComponent({ nodeTypes, edgeTypes, onInit, @@ -101,7 +101,7 @@ function GraphViewComponent({ rfId, viewport, onViewportChange, -}: GraphViewProps) { +}: GraphViewProps) { useNodeOrEdgeTypesWarning(nodeTypes); useNodeOrEdgeTypesWarning(edgeTypes); @@ -109,7 +109,7 @@ function GraphViewComponent({ useViewportSync(viewport); return ( - onPaneClick={onPaneClick} onPaneMouseEnter={onPaneMouseEnter} onPaneMouseMove={onPaneMouseMove} @@ -147,7 +147,7 @@ function GraphViewComponent({ isControlledViewport={!!viewport} > - edgeTypes={edgeTypes} onEdgeClick={onEdgeClick} onEdgeDoubleClick={onEdgeDoubleClick} @@ -173,7 +173,7 @@ function GraphViewComponent({ />
- nodeTypes={nodeTypes} onNodeClick={onNodeClick} onNodeDoubleClick={onNodeDoubleClick} @@ -196,4 +196,4 @@ function GraphViewComponent({ GraphViewComponent.displayName = 'GraphView'; -export const GraphView = memo(GraphViewComponent); +export const GraphView = memo(GraphViewComponent) as typeof GraphViewComponent; diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 11cab66f..39b20323 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -7,10 +7,10 @@ import { containerStyle } from '../../styles/utils'; import { GraphViewProps } from '../GraphView'; import { useResizeObserver } from './useResizeObserver'; import { NodeWrapper } from '../../components/NodeWrapper'; -import type { ReactFlowState } from '../../types'; +import type { Node, ReactFlowState } from '../../types'; -export type NodeRendererProps = Pick< - GraphViewProps, +export type NodeRendererProps = Pick< + GraphViewProps, | 'onNodeClick' | 'onNodeDoubleClick' | 'onNodeMouseEnter' @@ -35,7 +35,7 @@ const selector = (s: ReactFlowState) => ({ onError: s.onError, }); -const NodeRendererComponent = (props: NodeRendererProps) => { +function NodeRendererComponent(props: NodeRendererProps) { const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow); const nodeIds = useVisibleNodeIds(props.onlyRenderVisibleElements); const resizeObserver = useResizeObserver(); @@ -67,7 +67,7 @@ const NodeRendererComponent = (props: NodeRendererProps) => { // moved into `NodeComponentWrapper`. This ensures they are // memorized – so if `NodeRenderer` *has* to rerender, it only // needs to regenerate the list of nodes, nothing else. - key={nodeId} id={nodeId} nodeTypes={props.nodeTypes} @@ -94,8 +94,8 @@ const NodeRendererComponent = (props: NodeRendererProps) => { })}
); -}; +} NodeRendererComponent.displayName = 'NodeRenderer'; -export const NodeRenderer = memo(NodeRendererComponent); +export const NodeRenderer = memo(NodeRendererComponent) as typeof NodeRendererComponent; diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index af1ca28e..414a4745 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -220,7 +220,7 @@ export function Pane({ return (
( - ( - { - nodes, - edges, - defaultNodes, - defaultEdges, - className, - nodeTypes, - edgeTypes, - onNodeClick, - onEdgeClick, - onInit, - onMove, - onMoveStart, - onMoveEnd, - onConnect, - onConnectStart, - onConnectEnd, - onClickConnectStart, - onClickConnectEnd, - onNodeMouseEnter, - onNodeMouseMove, - onNodeMouseLeave, - onNodeContextMenu, - onNodeDoubleClick, - onNodeDragStart, - onNodeDrag, - onNodeDragStop, - onNodesDelete, - onEdgesDelete, - onDelete, - onSelectionChange, - onSelectionDragStart, - onSelectionDrag, - onSelectionDragStop, - onSelectionContextMenu, - onSelectionStart, - onSelectionEnd, - onBeforeDelete, - connectionMode, - connectionLineType = ConnectionLineType.Bezier, - connectionLineStyle, - connectionLineComponent, - connectionLineContainerStyle, - deleteKeyCode = 'Backspace', - selectionKeyCode = 'Shift', - selectionOnDrag = false, - selectionMode = SelectionMode.Full, - panActivationKeyCode = 'Space', - multiSelectionKeyCode = isMacOs() ? 'Meta' : 'Control', - zoomActivationKeyCode = isMacOs() ? 'Meta' : 'Control', - snapToGrid, - snapGrid, - onlyRenderVisibleElements = false, - selectNodesOnDrag, - nodesDraggable, - nodesConnectable, - nodesFocusable, - nodeOrigin = defaultNodeOrigin, - edgesFocusable, - edgesUpdatable, - elementsSelectable = true, - defaultViewport = initViewport, - minZoom = 0.5, - maxZoom = 2, - translateExtent = infiniteExtent, - preventScrolling = true, - nodeExtent, - defaultMarkerColor = '#b1b1b7', - zoomOnScroll = true, - zoomOnPinch = true, - panOnScroll = false, - panOnScrollSpeed = 0.5, - panOnScrollMode = PanOnScrollMode.Free, - zoomOnDoubleClick = true, - panOnDrag = true, - onPaneClick, - onPaneMouseEnter, - onPaneMouseMove, - onPaneMouseLeave, - onPaneScroll, - onPaneContextMenu, - children, - onEdgeUpdate, - onEdgeContextMenu, - onEdgeDoubleClick, - onEdgeMouseEnter, - onEdgeMouseMove, - onEdgeMouseLeave, - onEdgeUpdateStart, - onEdgeUpdateEnd, - edgeUpdaterRadius = 10, - onNodesChange, - onEdgesChange, - noDragClassName = 'nodrag', - noWheelClassName = 'nowheel', - noPanClassName = 'nopan', - fitView, - fitViewOptions, - connectOnClick, - attributionPosition, - proOptions, - defaultEdgeOptions, - elevateNodesOnSelect, - elevateEdgesOnSelect, - disableKeyboardA11y = false, - autoPanOnConnect, - autoPanOnNodeDrag, - connectionRadius, - isValidConnection, - onError, - style, - id, - nodeDragThreshold, - viewport, - onViewportChange, - width, - height, - colorMode = 'light', - ...rest - }, - ref - ) => { - const rfId = id || '1'; - const colorModeClassName = useColorModeClass(colorMode); +function ReactFlow( + { + nodes, + edges, + defaultNodes, + defaultEdges, + className, + nodeTypes, + edgeTypes, + onNodeClick, + onEdgeClick, + onInit, + onMove, + onMoveStart, + onMoveEnd, + onConnect, + onConnectStart, + onConnectEnd, + onClickConnectStart, + onClickConnectEnd, + onNodeMouseEnter, + onNodeMouseMove, + onNodeMouseLeave, + onNodeContextMenu, + onNodeDoubleClick, + onNodeDragStart, + onNodeDrag, + onNodeDragStop, + onNodesDelete, + onEdgesDelete, + onDelete, + onSelectionChange, + onSelectionDragStart, + onSelectionDrag, + onSelectionDragStop, + onSelectionContextMenu, + onSelectionStart, + onSelectionEnd, + onBeforeDelete, + connectionMode, + connectionLineType = ConnectionLineType.Bezier, + connectionLineStyle, + connectionLineComponent, + connectionLineContainerStyle, + deleteKeyCode = 'Backspace', + selectionKeyCode = 'Shift', + selectionOnDrag = false, + selectionMode = SelectionMode.Full, + panActivationKeyCode = 'Space', + multiSelectionKeyCode = isMacOs() ? 'Meta' : 'Control', + zoomActivationKeyCode = isMacOs() ? 'Meta' : 'Control', + snapToGrid, + snapGrid, + onlyRenderVisibleElements = false, + selectNodesOnDrag, + nodesDraggable, + nodesConnectable, + nodesFocusable, + nodeOrigin = defaultNodeOrigin, + edgesFocusable, + edgesUpdatable, + elementsSelectable = true, + defaultViewport = initViewport, + minZoom = 0.5, + maxZoom = 2, + translateExtent = infiniteExtent, + preventScrolling = true, + nodeExtent, + defaultMarkerColor = '#b1b1b7', + zoomOnScroll = true, + zoomOnPinch = true, + panOnScroll = false, + panOnScrollSpeed = 0.5, + panOnScrollMode = PanOnScrollMode.Free, + zoomOnDoubleClick = true, + panOnDrag = true, + onPaneClick, + onPaneMouseEnter, + onPaneMouseMove, + onPaneMouseLeave, + onPaneScroll, + onPaneContextMenu, + children, + onEdgeUpdate, + onEdgeContextMenu, + onEdgeDoubleClick, + onEdgeMouseEnter, + onEdgeMouseMove, + onEdgeMouseLeave, + onEdgeUpdateStart, + onEdgeUpdateEnd, + edgeUpdaterRadius = 10, + onNodesChange, + onEdgesChange, + noDragClassName = 'nodrag', + noWheelClassName = 'nowheel', + noPanClassName = 'nopan', + fitView, + fitViewOptions, + connectOnClick, + attributionPosition, + proOptions, + defaultEdgeOptions, + elevateNodesOnSelect, + elevateEdgesOnSelect, + disableKeyboardA11y = false, + autoPanOnConnect, + autoPanOnNodeDrag, + connectionRadius, + isValidConnection, + onError, + style, + id, + nodeDragThreshold, + viewport, + onViewportChange, + width, + height, + colorMode = 'light', + debug, + ...rest + }: ReactFlowProps, + ref: ForwardedRef +) { + const rfId = id || '1'; + const colorModeClassName = useColorModeClass(colorMode); - return ( -
- + + + onInit={onInit} + onNodeClick={onNodeClick} + onEdgeClick={onEdgeClick} + onNodeMouseEnter={onNodeMouseEnter} + onNodeMouseMove={onNodeMouseMove} + onNodeMouseLeave={onNodeMouseLeave} + onNodeContextMenu={onNodeContextMenu} + onNodeDoubleClick={onNodeDoubleClick} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + connectionLineType={connectionLineType} + connectionLineStyle={connectionLineStyle} + connectionLineComponent={connectionLineComponent} + connectionLineContainerStyle={connectionLineContainerStyle} + selectionKeyCode={selectionKeyCode} + selectionOnDrag={selectionOnDrag} + selectionMode={selectionMode} + deleteKeyCode={deleteKeyCode} + multiSelectionKeyCode={multiSelectionKeyCode} + panActivationKeyCode={panActivationKeyCode} + zoomActivationKeyCode={zoomActivationKeyCode} + onlyRenderVisibleElements={onlyRenderVisibleElements} + defaultViewport={defaultViewport} + translateExtent={translateExtent} + minZoom={minZoom} + maxZoom={maxZoom} + preventScrolling={preventScrolling} + zoomOnScroll={zoomOnScroll} + zoomOnPinch={zoomOnPinch} + zoomOnDoubleClick={zoomOnDoubleClick} + panOnScroll={panOnScroll} + panOnScrollSpeed={panOnScrollSpeed} + panOnScrollMode={panOnScrollMode} + panOnDrag={panOnDrag} + onPaneClick={onPaneClick} + onPaneMouseEnter={onPaneMouseEnter} + onPaneMouseMove={onPaneMouseMove} + onPaneMouseLeave={onPaneMouseLeave} + onPaneScroll={onPaneScroll} + onPaneContextMenu={onPaneContextMenu} + onSelectionContextMenu={onSelectionContextMenu} + onSelectionStart={onSelectionStart} + onSelectionEnd={onSelectionEnd} + onEdgeUpdate={onEdgeUpdate} + onEdgeContextMenu={onEdgeContextMenu} + onEdgeDoubleClick={onEdgeDoubleClick} + onEdgeMouseEnter={onEdgeMouseEnter} + onEdgeMouseMove={onEdgeMouseMove} + onEdgeMouseLeave={onEdgeMouseLeave} + onEdgeUpdateStart={onEdgeUpdateStart} + onEdgeUpdateEnd={onEdgeUpdateEnd} + edgeUpdaterRadius={edgeUpdaterRadius} + defaultMarkerColor={defaultMarkerColor} + noDragClassName={noDragClassName} + noWheelClassName={noWheelClassName} + noPanClassName={noPanClassName} + rfId={rfId} + disableKeyboardA11y={disableKeyboardA11y} + nodeOrigin={nodeOrigin} + nodeExtent={nodeExtent} + viewport={viewport} + onViewportChange={onViewportChange} + /> + nodes={nodes} edges={edges} defaultNodes={defaultNodes} defaultEdges={defaultEdges} - width={width} - height={height} + onConnect={onConnect} + onConnectStart={onConnectStart} + onConnectEnd={onConnectEnd} + onClickConnectStart={onClickConnectStart} + onClickConnectEnd={onClickConnectEnd} + nodesDraggable={nodesDraggable} + nodesConnectable={nodesConnectable} + nodesFocusable={nodesFocusable} + edgesFocusable={edgesFocusable} + edgesUpdatable={edgesUpdatable} + elementsSelectable={elementsSelectable} + elevateNodesOnSelect={elevateNodesOnSelect} + elevateEdgesOnSelect={elevateEdgesOnSelect} + minZoom={minZoom} + maxZoom={maxZoom} + nodeExtent={nodeExtent} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + snapToGrid={snapToGrid} + snapGrid={snapGrid} + connectionMode={connectionMode} + translateExtent={translateExtent} + connectOnClick={connectOnClick} + defaultEdgeOptions={defaultEdgeOptions} fitView={fitView} - > - - - - {children} - - - -
- ); - } -); + fitViewOptions={fitViewOptions} + onNodesDelete={onNodesDelete} + onEdgesDelete={onEdgesDelete} + onDelete={onDelete} + onNodeDragStart={onNodeDragStart} + onNodeDrag={onNodeDrag} + onNodeDragStop={onNodeDragStop} + onSelectionDrag={onSelectionDrag} + onSelectionDragStart={onSelectionDragStart} + onSelectionDragStop={onSelectionDragStop} + onMove={onMove} + onMoveStart={onMoveStart} + onMoveEnd={onMoveEnd} + noPanClassName={noPanClassName} + nodeOrigin={nodeOrigin} + rfId={rfId} + autoPanOnConnect={autoPanOnConnect} + autoPanOnNodeDrag={autoPanOnNodeDrag} + onError={onError} + connectionRadius={connectionRadius} + isValidConnection={isValidConnection} + selectNodesOnDrag={selectNodesOnDrag} + nodeDragThreshold={nodeDragThreshold} + onBeforeDelete={onBeforeDelete} + debug={debug} + /> + + {children} + + + +
+ ); +} -ReactFlow.displayName = 'ReactFlow'; - -export default ReactFlow; +export default forwardRef(ReactFlow) as typeof ReactFlow; diff --git a/packages/react/src/hooks/useDrag.ts b/packages/react/src/hooks/useDrag.ts index 8363d393..ee26cc57 100644 --- a/packages/react/src/hooks/useDrag.ts +++ b/packages/react/src/hooks/useDrag.ts @@ -31,35 +31,32 @@ export function useDrag({ const xyDrag = useRef(); useEffect(() => { - if (nodeRef?.current) { - xyDrag.current = XYDrag({ - domNode: nodeRef.current, - getStoreItems: () => store.getState(), - onNodeMouseDown: (id: string) => { - handleNodeClick({ - id, - store, - nodeRef, - }); - }, - onDragStart: () => { - setDragging(true); - }, - onDragStop: () => { - setDragging(false); - }, - }); - } + xyDrag.current = XYDrag({ + getStoreItems: () => store.getState(), + onNodeMouseDown: (id: string) => { + handleNodeClick({ + id, + store, + nodeRef, + }); + }, + onDragStart: () => { + setDragging(true); + }, + onDragStop: () => { + setDragging(false); + }, + }); }, []); useEffect(() => { if (disabled) { xyDrag.current?.destroy(); - } else { + } else if (nodeRef.current) { xyDrag.current?.update({ noDragClassName, handleSelector, - domNode: nodeRef.current as Element, + domNode: nodeRef.current, isSelectable, nodeId, }); diff --git a/packages/react/src/hooks/useEdges.ts b/packages/react/src/hooks/useEdges.ts index bd748abb..3ed9de39 100644 --- a/packages/react/src/hooks/useEdges.ts +++ b/packages/react/src/hooks/useEdges.ts @@ -11,8 +11,8 @@ const edgesSelector = (state: ReactFlowState) => state.edges; * @public * @returns An array of edges */ -export function useEdges(): Edge[] { - const edges = useStore(edgesSelector, shallow); +export function useEdges(): EdgeType[] { + const edges = useStore(edgesSelector, shallow) as EdgeType[]; return edges; } diff --git a/packages/react/src/hooks/useIsomorphicLayoutEffect.ts b/packages/react/src/hooks/useIsomorphicLayoutEffect.ts new file mode 100644 index 00000000..be54c9fe --- /dev/null +++ b/packages/react/src/hooks/useIsomorphicLayoutEffect.ts @@ -0,0 +1,4 @@ +import { useEffect, useLayoutEffect } from 'react'; + +// we need this hook to prevent a warning when using react-flow in SSR +export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; diff --git a/packages/react/src/hooks/useNodesData.ts b/packages/react/src/hooks/useNodesData.ts index 10480caa..c43a60cc 100644 --- a/packages/react/src/hooks/useNodesData.ts +++ b/packages/react/src/hooks/useNodesData.ts @@ -4,40 +4,45 @@ import { shallow } from 'zustand/shallow'; import { useStore } from '../hooks/useStore'; import type { Node } from '../types'; +export interface NodeDataReturn { + id: string; + type: NodeType['type']; + data: NodeType['data']; +} + /** * Hook for receiving data of one or multiple nodes * * @public * @param nodeId - The id (or ids) of the node to get the data from * @param guard - Optional guard function to narrow down the node type - * @returns An array od data objects + * @returns An object (or array of object) with {id, type, data} representing each node */ -export function useNodesData(nodeId: string): NodeType['data'] | null; -export function useNodesData(nodeIds: string[]): NodeType['data'][]; export function useNodesData( - nodeIds: string[], - guard: (node: Node) => node is NodeType -): NodeType['data'][]; + nodeId: string +): Pick | null; +export function useNodesData(nodeIds: string[]): Pick[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function useNodesData(nodeIds: any): any { const nodesData = useStore( useCallback( (s) => { - if (!Array.isArray(nodeIds)) { - return s.nodeLookup.get(nodeIds)?.data || null; - } - const data = []; + const isArrayOfIds = Array.isArray(nodeIds); + const _nodeIds = isArrayOfIds ? nodeIds : [nodeIds]; - for (const nodeId of nodeIds) { - const nodeData = s.nodeLookup.get(nodeId)?.data; - - if (nodeData) { - data.push(nodeData); + for (const nodeId of _nodeIds) { + const node = s.nodeLookup.get(nodeId); + if (node) { + data.push({ + id: node.id, + type: node.type, + data: node.data, + }); } } - return data; + return isArrayOfIds ? data : data[0] ?? null; }, [nodeIds] ), diff --git a/packages/react/src/hooks/useNodesEdgesState.ts b/packages/react/src/hooks/useNodesEdgesState.ts index 766ee049..fae87c02 100644 --- a/packages/react/src/hooks/useNodesEdgesState.ts +++ b/packages/react/src/hooks/useNodesEdgesState.ts @@ -1,7 +1,7 @@ import { useState, useCallback, type Dispatch, type SetStateAction } from 'react'; import { applyNodeChanges, applyEdgeChanges } from '../utils/changes'; -import type { Node, NodeChange, Edge, EdgeChange } from '../types'; +import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types'; /** * Hook for managing the state of nodes - should only be used for prototyping / simple use cases. @@ -10,12 +10,12 @@ import type { Node, NodeChange, Edge, EdgeChange } from '../types'; * @param initialNodes * @returns an array [nodes, setNodes, onNodesChange] */ -export function useNodesState( +export function useNodesState( initialNodes: NodeType[] -): [NodeType[], Dispatch>, (changes: NodeChange[]) => void] { +): [NodeType[], Dispatch>, OnNodesChange] { const [nodes, setNodes] = useState(initialNodes); - const onNodesChange = useCallback( - (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)), + const onNodesChange: OnNodesChange = useCallback( + (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [] ); @@ -31,10 +31,10 @@ export function useNodesState( */ export function useEdgesState( initialEdges: EdgeType[] -): [EdgeType[], Dispatch>, (changes: EdgeChange[]) => void] { +): [EdgeType[], Dispatch>, OnEdgesChange] { const [edges, setEdges] = useState(initialEdges); - const onEdgesChange = useCallback( - (changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)), + const onEdgesChange: OnEdgesChange = useCallback( + (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [] ); diff --git a/packages/react/src/hooks/useOnInitHandler.ts b/packages/react/src/hooks/useOnInitHandler.ts index 5b677c1a..5d43731b 100644 --- a/packages/react/src/hooks/useOnInitHandler.ts +++ b/packages/react/src/hooks/useOnInitHandler.ts @@ -1,15 +1,17 @@ import { useEffect, useRef } from 'react'; import { useReactFlow } from './useReactFlow'; -import type { OnInit } from '../types'; +import type { OnInit, Node, Edge } from '../types'; /** * Hook for calling onInit handler. * * @internal */ -export function useOnInitHandler(onInit: OnInit | undefined) { - const rfInstance = useReactFlow(); +export function useOnInitHandler( + onInit: OnInit | undefined +) { + const rfInstance = useReactFlow(); const isInitialized = useRef(false); useEffect(() => { diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 02080731..5fd605fd 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,10 +1,11 @@ -import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system'; import useViewportHelper from './useViewportHelper'; import { useStoreApi } from './useStore'; import type { ReactFlowInstance, Instance, Node, Edge } from '../types'; import { getElementsDiffChanges, isNode } from '../utils'; +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; /** * Hook for accessing the ReactFlow instance. @@ -54,7 +55,7 @@ export function useReactFlow { + useIsomorphicLayoutEffect(() => { // Because we need to flip the state back to false after flushing, this should // trigger the hook again (!). If the hook is being run again we know that any // updates should have been processed by now and we can safely clear the queue diff --git a/packages/react/src/hooks/useStore.ts b/packages/react/src/hooks/useStore.ts index 96587161..8a600706 100644 --- a/packages/react/src/hooks/useStore.ts +++ b/packages/react/src/hooks/useStore.ts @@ -1,15 +1,13 @@ import { useContext, useMemo } from 'react'; -import { useStoreWithEqualityFn as useZustandStore } from 'zustand/traditional'; -import type { StoreApi } from 'zustand'; +import { UseBoundStoreWithEqualityFn, useStoreWithEqualityFn as useZustandStore } from 'zustand/traditional'; import { errorMessages } from '@xyflow/system'; import StoreContext from '../contexts/RFStoreContext'; -import type { ReactFlowState } from '../types'; +import type { Edge, Node, ReactFlowState } from '../types'; +import { StoreApi } from 'zustand'; const zustandErrorMessage = errorMessages['error001'](); -type ExtractState = StoreApi extends { getState: () => infer T } ? T : never; - /** * Hook for accessing the internal store. Should only be used in rare cases. * @@ -17,8 +15,12 @@ type ExtractState = StoreApi extends { getState: () => infer T } * @param selector * @param equalityFn * @returns The selected state slice + * + * @example + * const nodes = useStore((state: ReactFlowState) => state.nodes); + * */ -function useStore( +function useStore( selector: (state: ReactFlowState) => StateSlice, equalityFn?: (a: StateSlice, b: StateSlice) => boolean ) { @@ -31,8 +33,10 @@ function useStore( return useZustandStore(store, selector, equalityFn); } -const useStoreApi = () => { - const store = useContext(StoreContext); +function useStoreApi() { + const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn< + StoreApi> + > | null; if (store === null) { throw new Error(zustandErrorMessage); @@ -47,6 +51,6 @@ const useStoreApi = () => { }), [store] ); -}; +} export { useStore, useStoreApi }; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index bcb12cdf..a757ec23 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -53,7 +53,6 @@ export { type OnConnectStart, type OnConnect, type OnConnectEnd, - type IsValidConnection, type Viewport, type SnapGrid, PanOnScrollMode, diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 71a51a1e..a2b98c1a 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -89,6 +89,7 @@ const createRFStore = ({ fitViewOnInitOptions, domNode, nodeOrigin, + debug, } = get(); const changes: NodeDimensionChange[] = []; @@ -130,6 +131,9 @@ const createRFStore = ({ set({ nodes: nextNodes, fitViewDone: nextFitViewDone }); if (changes?.length > 0) { + if (debug) { + console.log('React Flow: trigger node changes', changes); + } onNodesChange?.(changes); } }, @@ -149,7 +153,7 @@ const createRFStore = ({ get().triggerNodeChanges(changes); }, triggerNodeChanges: (changes) => { - const { onNodesChange, setNodes, nodes, hasDefaultNodes } = get(); + const { onNodesChange, setNodes, nodes, hasDefaultNodes, debug } = get(); if (changes?.length) { if (hasDefaultNodes) { @@ -157,11 +161,15 @@ const createRFStore = ({ setNodes(updatedNodes); } + if (debug) { + console.log('React Flow: trigger node changes', changes); + } + onNodesChange?.(changes); } }, triggerEdgeChanges: (changes) => { - const { onEdgesChange, setEdges, edges, hasDefaultEdges } = get(); + const { onEdgesChange, setEdges, edges, hasDefaultEdges, debug } = get(); if (changes?.length) { if (hasDefaultEdges) { @@ -169,6 +177,10 @@ const createRFStore = ({ setEdges(updatedEdges); } + if (debug) { + console.log('React Flow: trigger edge changes', changes); + } + onEdgesChange?.(changes); } }, @@ -291,13 +303,11 @@ const createRFStore = ({ connectionEndHandle: null, }), updateConnection: (params) => { - const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get(); + const { connectionPosition } = get(); const currentConnection = { + ...params, connectionPosition: params.connectionPosition ?? connectionPosition, - connectionStatus: params.connectionStatus ?? connectionStatus, - connectionStartHandle: params.connectionStartHandle ?? connectionStartHandle, - connectionEndHandle: params.connectionEndHandle ?? connectionEndHandle, }; set(currentConnection); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 5a1f9562..0cf3c7f0 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -43,7 +43,9 @@ const getInitialState = ({ let transform: Transform = [0, 0, 1]; if (fitView && width && height) { - const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); + const nodesWithDimensions = nextNodes.filter( + (node) => (node.width || node.initialWidth) && (node.height || node.initialHeight) + ); // @todo users nodeOrigin should be used here const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] }); const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); @@ -113,6 +115,7 @@ const getInitialState = ({ onSelectionChangeHandlers: [], lib: 'react', + debug: false, }; }; diff --git a/packages/react/src/types/changes.ts b/packages/react/src/types/changes.ts index 9a5e8fe5..adcb484d 100644 --- a/packages/react/src/types/changes.ts +++ b/packages/react/src/types/changes.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import type { XYPosition, Dimensions } from '@xyflow/system'; import type { Node, Edge } from '.'; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 25778476..e8d91ba4 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -18,7 +18,6 @@ import type { HandleType, SelectionMode, OnError, - IsValidConnection, ColorMode, SnapGrid, } from '@xyflow/system'; @@ -44,13 +43,15 @@ import type { EdgeMouseHandler, OnNodeDrag, OnBeforeDelete, + IsValidConnection, } from '.'; /** * ReactFlow component props. * @public */ -export interface ReactFlowProps extends Omit, 'onError'> { +export interface ReactFlowProps + extends Omit, 'onError'> { /** An array of nodes to render in a controlled flow. * @example * const nodes = [ @@ -62,7 +63,7 @@ export interface ReactFlowProps extends Omit, 'on * } * ]; */ - nodes?: Node[]; + nodes?: NodeType[]; /** An array of edges to render in a controlled flow. * @example * const edges = [ @@ -73,11 +74,11 @@ export interface ReactFlowProps extends Omit, 'on * } * ]; */ - edges?: Edge[]; + edges?: EdgeType[]; /** The initial nodes to render in an uncontrolled flow. */ - defaultNodes?: Node[]; + defaultNodes?: NodeType[]; /** The initial edges to render in an uncontrolled flow. */ - defaultEdges?: Edge[]; + defaultEdges?: EdgeType[]; /** Defaults to be applied to all new edges that are added to the flow. * * Properties on a new edge will override these defaults if they exist. @@ -99,38 +100,38 @@ export interface ReactFlowProps extends Omit, 'on */ defaultEdgeOptions?: DefaultEdgeOptions; /** This event handler is called when a user clicks on a node */ - onNodeClick?: NodeMouseHandler; + onNodeClick?: NodeMouseHandler; /** This event handler is called when a user double clicks on a node */ - onNodeDoubleClick?: NodeMouseHandler; + onNodeDoubleClick?: NodeMouseHandler; /** This event handler is called when mouse of a user enters a node */ - onNodeMouseEnter?: NodeMouseHandler; + onNodeMouseEnter?: NodeMouseHandler; /** This event handler is called when mouse of a user moves over a node */ - onNodeMouseMove?: NodeMouseHandler; + onNodeMouseMove?: NodeMouseHandler; /** This event handler is called when mouse of a user leaves a node */ - onNodeMouseLeave?: NodeMouseHandler; + onNodeMouseLeave?: NodeMouseHandler; /** This event handler is called when a user right clicks on a node */ - onNodeContextMenu?: NodeMouseHandler; + onNodeContextMenu?: NodeMouseHandler; /** This event handler is called when a user starts to drag a node */ - onNodeDragStart?: OnNodeDrag; + onNodeDragStart?: OnNodeDrag; /** This event handler is called when a user drags a node */ - onNodeDrag?: OnNodeDrag; + onNodeDrag?: OnNodeDrag; /** This event handler is called when a user stops dragging a node */ - onNodeDragStop?: OnNodeDrag; + onNodeDragStop?: OnNodeDrag; /** This event handler is called when a user clicks on an edge */ - onEdgeClick?: (event: ReactMouseEvent, edge: Edge) => void; + onEdgeClick?: (event: ReactMouseEvent, edge: EdgeType) => void; /** This event handler is called when a user right clicks on an edge */ - onEdgeContextMenu?: EdgeMouseHandler; + onEdgeContextMenu?: EdgeMouseHandler; /** This event handler is called when mouse of a user enters an edge */ - onEdgeMouseEnter?: EdgeMouseHandler; + onEdgeMouseEnter?: EdgeMouseHandler; /** This event handler is called when mouse of a user moves over an edge */ - onEdgeMouseMove?: EdgeMouseHandler; + onEdgeMouseMove?: EdgeMouseHandler; /** This event handler is called when mouse of a user leaves an edge */ - onEdgeMouseLeave?: EdgeMouseHandler; + onEdgeMouseLeave?: EdgeMouseHandler; /** This event handler is called when a user double clicks on an edge */ - onEdgeDoubleClick?: EdgeMouseHandler; - onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; - onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void; - onEdgeUpdate?: OnEdgeUpdateFunc; + onEdgeDoubleClick?: EdgeMouseHandler; + onEdgeUpdateStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; + onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; + onEdgeUpdate?: OnEdgeUpdateFunc; /** This event handler is called when a Node is updated * @example // Use NodesState hook to create edges and get onNodesChange handler * import ReactFlow, { useNodesState } from '@xyflow/react'; @@ -147,7 +148,7 @@ export interface ReactFlowProps extends Omit, 'on * * return () */ - onNodesChange?: OnNodesChange; + onNodesChange?: OnNodesChange; /** This event handler is called when a Edge is updated * @example // Use EdgesState hook to create edges and get onEdgesChange handler * import ReactFlow, { useEdgesState } from '@xyflow/react'; @@ -164,22 +165,22 @@ export interface ReactFlowProps extends Omit, 'on * * return () */ - onEdgesChange?: OnEdgesChange; + onEdgesChange?: OnEdgesChange; /** This event handler gets called when a Node is deleted */ - onNodesDelete?: OnNodesDelete; + onNodesDelete?: OnNodesDelete; /** This event handler gets called when a Edge is deleted */ - onEdgesDelete?: OnEdgesDelete; + onEdgesDelete?: OnEdgesDelete; /** This event handler gets called when a Node or Edge is deleted */ - onDelete?: OnDelete; + onDelete?: OnDelete; /** This event handler gets called when a user starts to drag a selection box */ - onSelectionDragStart?: SelectionDragHandler; + onSelectionDragStart?: SelectionDragHandler; /** This event handler gets called when a user drags a selection box */ - onSelectionDrag?: SelectionDragHandler; + onSelectionDrag?: SelectionDragHandler; /** This event handler gets called when a user stops dragging a selection box */ - onSelectionDragStop?: SelectionDragHandler; + onSelectionDragStop?: SelectionDragHandler; onSelectionStart?: (event: ReactMouseEvent) => void; onSelectionEnd?: (event: ReactMouseEvent) => void; - onSelectionContextMenu?: (event: ReactMouseEvent, nodes: Node[]) => void; + onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void; /** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection. * * You can use the addEdge utility to convert the connection to a complete edge. @@ -201,7 +202,7 @@ export interface ReactFlowProps extends Omit, 'on onClickConnectStart?: OnConnectStart; onClickConnectEnd?: OnConnectEnd; /** This event handler gets called when a flow has finished initializing */ - onInit?: OnInit; + onInit?: OnInit; /** This event handler is called while the user is either panning or zooming the viewport. */ onMove?: OnMove; /** This event handler gets called when a user starts to pan or zoom the viewport */ @@ -223,7 +224,7 @@ export interface ReactFlowProps extends Omit, 'on /** This event handler gets called when mouse leaves the pane */ onPaneMouseLeave?: (event: ReactMouseEvent) => void; /** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */ - onBeforeDelete?: OnBeforeDelete; + onBeforeDelete?: OnBeforeDelete; /** Custom node types to be available in a flow. * * React Flow matches a node's type to a component in the nodeTypes object. @@ -502,6 +503,11 @@ export interface ReactFlowProps extends Omit, 'on * @example 'system' | 'light' | 'dark' */ colorMode?: ColorMode; + /** If set true, some debug information will be logged to the console like which events are fired. + * + * @default undefined + */ + debug?: boolean; } export type ReactFlowRefType = HTMLDivElement; diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index 2b514c96..602f2c34 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -30,7 +30,14 @@ export type EdgeLabelOptions = { export type EdgeUpdatable = boolean | HandleType; -export type DefaultEdge = EdgeBase & +/** + * The Edge type is mainly used for the `edges` that get passed to the ReactFlow component + * @public + */ +export type Edge< + EdgeData extends Record = Record, + EdgeType extends string | undefined = string | undefined +> = EdgeBase & EdgeLabelOptions & { style?: CSSProperties; className?: string; @@ -38,48 +45,45 @@ export type DefaultEdge = EdgeBase & focusable?: boolean; }; -type SmoothStepEdgeType = DefaultEdge & { - type: 'smoothstep'; +type SmoothStepEdge = Record> = Edge< + EdgeData, + 'smoothstep' +> & { pathOptions?: SmoothStepPathOptions; }; -type BezierEdgeType = DefaultEdge & { - type: 'default'; +type BezierEdge = Record> = Edge & { pathOptions?: BezierPathOptions; }; -type StepEdgeType = DefaultEdge & { - type: 'step'; +type StepEdge = Record> = Edge & { pathOptions?: StepPathOptions; }; -/** - * The Edge type is mainly used for the `edges` that get passed to the ReactFlow component - * @public - */ -export type Edge = DefaultEdge | SmoothStepEdgeType | BezierEdgeType | StepEdgeType; +export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge; -export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void; +export type EdgeMouseHandler = (event: ReactMouseEvent, edge: EdgeType) => void; -export type EdgeWrapperProps = { +export type EdgeWrapperProps = { id: string; edgesFocusable: boolean; edgesUpdatable: boolean; elementsSelectable: boolean; noPanClassName: string; - onClick?: EdgeMouseHandler; - onDoubleClick?: EdgeMouseHandler; - onEdgeUpdate?: OnEdgeUpdateFunc; - onContextMenu?: EdgeMouseHandler; - onMouseEnter?: EdgeMouseHandler; - onMouseMove?: EdgeMouseHandler; - onMouseLeave?: EdgeMouseHandler; + onClick?: EdgeMouseHandler; + onDoubleClick?: EdgeMouseHandler; + onEdgeUpdate?: OnEdgeUpdateFunc; + onContextMenu?: EdgeMouseHandler; + onMouseEnter?: EdgeMouseHandler; + onMouseMove?: EdgeMouseHandler; + onMouseLeave?: EdgeMouseHandler; edgeUpdaterRadius?: number; - onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; - onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void; + onEdgeUpdateStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; + onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; rfId?: string; edgeTypes?: EdgeTypes; onError?: OnError; + disableKeyboardA11y?: boolean; }; export type DefaultEdgeOptions = DefaultEdgeOptionsBase; @@ -94,10 +98,10 @@ export type EdgeTextProps = HTMLAttributes & * Custom edge component props * @public */ -export type EdgeProps = Pick< - Edge, - 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' -> & +export type EdgeProps< + EdgeData extends Record = Record, + EdgeType extends string | undefined = string | undefined +> = Pick, 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'> & EdgePosition & EdgeLabelOptions & { sourceHandleId?: string | null; @@ -185,7 +189,7 @@ export type StraightEdgeProps = Omit = (oldEdge: Edge, newConnection: Connection) => void; +export type OnEdgeUpdateFunc = (oldEdge: EdgeType, newConnection: Connection) => void; export type ConnectionLineComponentProps = { connectionLineStyle?: CSSProperties; diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 4f3f127a..0e1e1dc4 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { FitViewParamsBase, FitViewOptionsBase, @@ -12,6 +11,7 @@ import { XYPosition, NodeProps, OnBeforeDeleteBase, + Connection, } from '@xyflow/system'; import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.'; @@ -21,8 +21,11 @@ export type OnNodesChange = (changes: NodeChange = (changes: EdgeChange[]) => void; export type OnNodesDelete = (nodes: NodeType[]) => void; -export type OnEdgesDelete = (edges: Edge[]) => void; -export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; +export type OnEdgesDelete = (edges: EdgeType[]) => void; +export type OnDelete = (params: { + nodes: NodeType[]; + edges: EdgeType[]; +}) => void; export type NodeTypes = { [key: string]: ComponentType }; export type EdgeTypes = { [key: string]: ComponentType }; @@ -139,3 +142,5 @@ export type OnBeforeDelete; + +export type IsValidConnection = (edge: EdgeType | Connection) => boolean; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index d34c23dc..1991580c 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-namespace */ import type { Rect, Viewport } from '@xyflow/system'; import type { Node, Edge, ViewportHelperFunctions } from '.'; diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 612c1d50..c3c0ea94 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -7,11 +7,10 @@ import { NodeTypes } from './general'; * The node data structure that gets used for the nodes prop. * @public */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type Node = NodeBase< - NodeData, - NodeType -> & { +export type Node< + NodeData extends Record = Record, + NodeType extends string | undefined = string | undefined +> = NodeBase & { style?: CSSProperties; className?: string; resizing?: boolean; @@ -26,18 +25,18 @@ export type OnNodeDrag = ( nodes: NodeType[] ) => void; -export type NodeWrapperProps = { +export type NodeWrapperProps = { id: string; nodesConnectable: boolean; elementsSelectable: boolean; nodesDraggable: boolean; nodesFocusable: boolean; - onClick?: NodeMouseHandler; - onDoubleClick?: NodeMouseHandler; - onMouseEnter?: NodeMouseHandler; - onMouseMove?: NodeMouseHandler; - onMouseLeave?: NodeMouseHandler; - onContextMenu?: NodeMouseHandler; + onClick?: NodeMouseHandler; + onDoubleClick?: NodeMouseHandler; + onMouseEnter?: NodeMouseHandler; + onMouseMove?: NodeMouseHandler; + onMouseLeave?: NodeMouseHandler; + onContextMenu?: NodeMouseHandler; resizeObserver: ResizeObserver | null; noDragClassName: string; noPanClassName: string; @@ -48,3 +47,5 @@ export type NodeWrapperProps = { nodeOrigin: NodeOrigin; onError?: OnError; }; + +export type BuiltInNode = Node<{ label: string }, 'input' | 'output' | 'default'>; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index f1c625ba..6c1cf9b0 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -21,7 +21,6 @@ import { type OnMoveStart, type OnMove, type OnMoveEnd, - type IsValidConnection, type UpdateConnection, type EdgeLookup, type ConnectionLookup, @@ -43,21 +42,22 @@ import type { OnDelete, OnNodeDrag, OnBeforeDelete, + IsValidConnection, EdgeChange, } from '.'; -export type ReactFlowStore = { +export type ReactFlowStore = { rfId: string; width: number; height: number; transform: Transform; - nodes: Node[]; - nodeLookup: NodeLookup; + nodes: NodeType[]; + nodeLookup: NodeLookup; edges: Edge[]; - edgeLookup: EdgeLookup; + edgeLookup: EdgeLookup; connectionLookup: ConnectionLookup; - onNodesChange: OnNodesChange | null; - onEdgesChange: OnEdgesChange | null; + onNodesChange: OnNodesChange | null; + onEdgesChange: OnEdgesChange | null; hasDefaultNodes: boolean; hasDefaultEdges: boolean; domNode: HTMLDivElement | null; @@ -99,9 +99,9 @@ export type ReactFlowStore = { connectionEndHandle: ConnectingHandle | null; connectionClickStartHandle: ConnectingHandle | null; - onNodeDragStart?: OnNodeDrag; - onNodeDrag?: OnNodeDrag; - onNodeDragStop?: OnNodeDrag; + onNodeDragStart?: OnNodeDrag; + onNodeDrag?: OnNodeDrag; + onNodeDragStop?: OnNodeDrag; onSelectionDragStart?: OnSelectionDrag; onSelectionDrag?: OnSelectionDrag; @@ -125,8 +125,8 @@ export type ReactFlowStore = { fitViewDone: boolean; fitViewOnInitOptions: FitViewOptions | undefined; - onNodesDelete?: OnNodesDelete; - onEdgesDelete?: OnEdgesDelete; + onNodesDelete?: OnNodesDelete; + onEdgesDelete?: OnEdgesDelete; onDelete?: OnDelete; onError?: OnError; @@ -134,7 +134,7 @@ export type ReactFlowStore = { onViewportChangeStart?: OnViewportChange; onViewportChange?: OnViewportChange; onViewportChangeEnd?: OnViewportChange; - onBeforeDelete?: OnBeforeDelete; + onBeforeDelete?: OnBeforeDelete; onSelectionChangeHandlers: OnSelectionChangeFunc[]; @@ -143,14 +143,16 @@ export type ReactFlowStore = { autoPanOnNodeDrag: boolean; connectionRadius: number; - isValidConnection?: IsValidConnection; + isValidConnection?: IsValidConnection; lib: string; + debug: boolean; }; -export type ReactFlowActions = { - setNodes: (nodes: Node[]) => void; - setEdges: (edges: Edge[]) => void; +export type ReactFlowActions = { + setNodes: (nodes: NodeType[]) => void; + setEdges: (edges: EdgeType[]) => void; + setDefaultNodesAndEdges: (nodes?: NodeType[], edges?: EdgeType[]) => void; updateNodeDimensions: (updates: Map) => void; updateNodePositions: UpdateNodePositions; resetSelectedElements: () => void; @@ -164,11 +166,14 @@ export type ReactFlowActions = { cancelConnection: () => void; updateConnection: UpdateConnection; reset: () => void; - triggerNodeChanges: (changes: NodeChange[] | null) => void; - triggerEdgeChanges: (changes: EdgeChange[] | null) => void; + triggerNodeChanges: (changes: NodeChange[]) => void; + triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; - fitView: (nodes: Node[], options?: FitViewOptions) => boolean; - setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; + fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean; }; -export type ReactFlowState = ReactFlowStore & ReactFlowActions; +export type ReactFlowState = ReactFlowStore< + NodeType, + EdgeType +> & + ReactFlowActions; diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2f34e1a3..4635387f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # @xyflow/svelte +## 0.0.37 + +## ⚠️ Breaking changes + +- `useNodesData` not only returns data objects but also the type and the id of the node +- status class names for Handle components are slightly different. It's now "connectingfrom" and "connectingto" instead of "connecting" + +## Patch changes + +- better cursor defaults for the pane, nodes and edges +- add `initialWidth` and `initialHeight` node attributes for specifying initial dimensions for ssr +- always re-measure nodes when new nodes get passed +- fix `NodeResizer` when used in combination with `nodeOrigin` + ## 0.0.36 ## Patch changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6fe2bec2..f73908cb 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/svelte", - "version": "0.0.36", + "version": "0.0.37", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "keywords": [ "svelte", diff --git a/packages/svelte/src/lib/actions/drag/index.ts b/packages/svelte/src/lib/actions/drag/index.ts index d08a17be..1405ccd9 100644 --- a/packages/svelte/src/lib/actions/drag/index.ts +++ b/packages/svelte/src/lib/actions/drag/index.ts @@ -19,7 +19,6 @@ export type UseDragParams = { export default function drag(domNode: Element, params: UseDragParams) { const { store, onDrag, onDragStart, onDragStop, onNodeMouseDown } = params; const dragInstance = XYDrag({ - domNode, onDrag, onDragStart, onDragStop, diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index 0d4c126b..1dff49e7 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -22,6 +22,7 @@ export let animated: $$Props['animated'] = false; export let selected: $$Props['selected'] = false; + export let selectable: $$Props['selectable'] = undefined; export let hidden: $$Props['hidden'] = false; export let label: $$Props['label'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined; @@ -44,7 +45,7 @@ setContext('svelteflow__edge_id', id); - const { edgeLookup, edgeTypes, flowId } = useStore(); + const { edgeLookup, edgeTypes, flowId, elementsSelectable } = useStore(); const dispatch = createEventDispatcher<{ edgeclick: { edge: Edge; event: MouseEvent | TouchEvent }; edgecontextmenu: { edge: Edge; event: MouseEvent }; @@ -53,6 +54,7 @@ $: edgeComponent = $edgeTypes[type!] || BezierEdgeInternal; $: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined; $: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined; + $: isSelectable = selectable || ($elementsSelectable && typeof selectable === 'undefined'); const handleEdgeSelect = useHandleEdgeSelect(); @@ -82,6 +84,7 @@ class={cc(['svelte-flow__edge', className])} class:animated class:selected + class:selectable={isSelectable} data-id={id} on:click={onClick} on:contextmenu={onContextMenu} diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index f8a43dd4..e35804fc 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -56,7 +56,8 @@ onconnect: onConnectAction, onconnectstart: onConnectStartAction, onconnectend: onConnectEndAction, - flowId + flowId, + connection } = store; function onPointerDown(event: MouseEvent | TouchEvent) { @@ -123,6 +124,16 @@ prevConnections = connections ?? new Map(); } + $: connectingFrom = + $connection.startHandle?.nodeId === nodeId && + $connection.startHandle?.type === type && + $connection.startHandle?.handleId === handleId; + $: connectingTo = + $connection.endHandle?.nodeId === nodeId && + $connection.endHandle?.type === type && + $connection.endHandle?.handleId === handleId; + $: valid = connectingTo && $connection.status === 'valid'; + // @todo implement connectablestart, connectableend @@ -141,6 +152,11 @@ The Handle component is the part of a node that can be used to connect nodes. 'nodrag', 'nopan', position, + { + valid, + connectingto: connectingTo, + connectingfrom: connectingFrom + }, className ])} class:source={!isTarget} diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index ae894066..50dda4e7 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -1,7 +1,13 @@