Compare commits

...

17 Commits

Author SHA1 Message Date
c6b585403e init commit
Some checks failed
Playwright Tests / Playwright Tests (push) Has been cancelled
2026-03-17 16:30:30 +03:30
Moritz Klack
6ec8214cf3 Merge pull request #5691 from xyflow/changeset-release/main
Release packages
2026-02-19 12:52:21 +01:00
github-actions[bot]
81a284a27c chore(packages): bump 2026-02-18 20:15:08 +00:00
Moritz Klack
ac89f9fa63 Merge pull request #5684 from ysds/fix/nodes-selection-drag-after-programmatic-selection
fix: ensure drag handlers update when nodes selection changes programmatically
2026-02-18 21:13:47 +01:00
Moritz Klack
d66689691d Merge pull request #5704 from xyflow/fix/old-callback-reference
Fix old callback references passed to XYHandle
2026-02-18 21:04:47 +01:00
Moritz Klack
e3c1f42e9a Update connection status during ongoing connection 2026-02-18 11:15:02 +01:00
peterkogo
c91d3d022f chore(changeset) 2026-02-18 10:49:43 +01:00
Moritz Klack
48eed6f105 Merge pull request #5703 from xyflow/fix/use-nodes-data-type
Improve return type of useNodesData
2026-02-16 20:57:22 +01:00
peterkogo
45a4a977c5 Always use latest version of onConnectEnd & isValidConnection in Handle/ReconnectAnchors 2026-02-10 16:02:43 +01:00
peterkogo
ce6c869df4 Improve return type of useNodesdata 2026-02-10 10:58:51 +01:00
Moritz Klack
7813d07345 Merge pull request #5698 from xyflow/fix/svelte-subflow-minimap
Fix/svelte subflow minimap
2026-02-09 22:39:02 +01:00
peterkogo
cb41f444eb simplify code & useEffect instead of uselayoutEffect 2026-02-09 07:57:41 +01:00
peterkogo
ee53f8baf7 add reproduction of the bug 2026-02-06 19:41:24 +01:00
peterkogo
7eeebc9c04 chore(changeset) 2026-02-04 14:18:34 +01:00
peterkogo
5166e1df83 fix child nodes not updating in Minimap 2026-02-04 14:17:31 +01:00
ysds
382c654c31 chore(changeset): add changeset for drag handler fix 2026-01-24 03:01:29 +09:00
ysds
03b8e1ef19 fix: ensure drag handlers update when nodes selection changes programmatically
Fixes #4841

When nodes were programmatically selected (e.g., adding a new node with `selected: true`),
the nodes selection rectangle would appear but dragging it would not work.

Changes:
- Consolidated duplicate effect hooks in useDrag into a single useIsomorphicLayoutEffect
- Added `disabled` parameter to useDrag to allow external control
- NodesSelection now passes `disabled: !shouldRender` to useDrag
- Improved condition clarity with `shouldRender` variable

This ensures drag handlers are properly set up when nodeRef.current becomes available,
even when the selection state changes after the initial render.
2026-01-24 02:29:02 +09:00
28 changed files with 17286 additions and 5604 deletions

View File

@@ -1,5 +0,0 @@
---
'@xyflow/react': patch
---
Optimize zooming performance when panOnScroll mode is enabled

View File

@@ -1,5 +0,0 @@
---
'@xyflow/react': patch
---
Prevent unnecessary updates when selectNodesOnDrag = false

View File

@@ -1,5 +0,0 @@
---
'@xyflow/react': patch
---
Handle undefined node in mini map

14166
_pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,7 @@ import MovingHandles from '../examples/MovingHandles';
import DetachedHandle from '../examples/DetachedHandle';
import ZIndexMode from '../examples/ZIndexMode';
import Middlewares from '../examples/Middlewares';
import NodeSelectionBug from '../examples/NodeSelectionBug';
export interface IRoute {
name: string;
@@ -390,6 +391,11 @@ const routes: IRoute[] = [
path: 'z-index-mode',
component: ZIndexMode,
},
{
name: 'Node Selection Bug',
path: 'node-selection-bug',
component: NodeSelectionBug,
},
];
export default routes;

View File

@@ -0,0 +1,41 @@
import { Node, ReactFlow, useNodesState } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { useRef } from 'react';
export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([
{
id: '0',
position: { x: 0, y: 0 },
data: { label: 'Rectangle Select Me First' },
},
]);
const id = useRef(0);
return (
<>
<button
onClick={() =>
setNodes((nodes) => [
...nodes.map((node) => ({ ...node, selected: false })),
{
id: (++id.current).toString(),
position: { x: -100, y: 100 * Math.floor((id.current + 1) / 2) },
data: { label: `Button Node ${id.current}` },
selected: true,
},
{
id: (++id.current).toString(),
position: { x: 100, y: (100 * id.current) / 2 },
data: { label: `Button Node ${id.current}` },
selected: true,
},
])
}
>
Click me to add nodes that are already selected.
</button>
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} fitView></ReactFlow>
</>
);
}

View File

@@ -1,7 +1,8 @@
import { memo, useState } from 'react';
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
import type { TextNode } from '.';
function TextNode({ id, data }: NodeProps) {
function TextNode({ id, data }: NodeProps<TextNode>) {
const { updateNodeData } = useReactFlow();
const [text, setText] = useState(data.text);
const updateText = (text: string) => {

View File

@@ -1,18 +1,18 @@
import { memo, useEffect } from 'react';
import { Position, NodeProps, useReactFlow, Handle, useNodeConnections, useNodesData } from '@xyflow/react';
import { isTextNode, type TextNode, type MyNode } from '.';
import { isTextNode, type TextNode, type MyNode, type UppercaseNode } from '.';
function UppercaseNode({ id }: NodeProps) {
function UppercaseNode({ id }: NodeProps<UppercaseNode>) {
const { updateNodeData } = useReactFlow();
const connections = useNodeConnections({
handleType: 'target',
});
const nodesData = useNodesData<MyNode>(connections[0]?.source);
const textNode = isTextNode(nodesData) ? nodesData : null;
useEffect(() => {
updateNodeData(id, { text: textNode?.data.text.toUpperCase() });
}, [textNode]);
const text = nodesData?.type === 'text' ? nodesData?.data.text.toUpperCase() : undefined;
updateNodeData(id, { text });
}, [nodesData]);
return (
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>

View File

@@ -8,22 +8,21 @@
type Node,
type NodeProps
} from '@xyflow/svelte';
import { isTextNode, type MyNode } from './+page.svelte';
import { type MyNode } from './+page.svelte';
let { id }: NodeProps<Node<{ text: string }>> = $props();
const { updateNodeData } = useSvelteFlow();
const connections = useNodeConnections({
id: id,
handleType: 'target'
});
let nodeData = $derived(useNodesData<MyNode>(connections.current[0]?.source));
let textNodeData = $derived(isTextNode(nodeData.current) ? nodeData.current.data.text : null);
let nodesData = $derived(useNodesData<MyNode>(connections.current[0]?.source));
$effect.pre(() => {
const input = textNodeData?.toUpperCase() ?? '';
updateNodeData(id, { text: input });
const text =
nodesData.current?.type === 'text' ? nodesData.current?.data.text.toUpperCase() : undefined;
updateNodeData(id, { text });
});
</script>

View File

@@ -1,5 +1,24 @@
# @xyflow/react
## 12.10.1
### Patch Changes
- [#5704](https://github.com/xyflow/xyflow/pull/5704) [`c91d3d022`](https://github.com/xyflow/xyflow/commit/c91d3d022f4517f4403a898cd02ee891b7e1f2d2) Thanks [@peterkogo](https://github.com/peterkogo)! - Keep `onConnectEnd` and `isValidConnection` up to date in an ongoing connection
- [#5687](https://github.com/xyflow/xyflow/pull/5687) [`2624479ad`](https://github.com/xyflow/xyflow/commit/2624479ad3d0b06fcb690242b2372ff2a7e16f54) Thanks [@vkrol](https://github.com/vkrol)! - Optimize zooming performance when panOnScroll mode is enabled
- [#5682](https://github.com/xyflow/xyflow/pull/5682) [`7b6e46ce1`](https://github.com/xyflow/xyflow/commit/7b6e46ce17f49e759f614a8f933f7dc729635b48) Thanks [@artemtam](https://github.com/artemtam)! - Prevent unnecessary updates when selectNodesOnDrag = false
- [#5703](https://github.com/xyflow/xyflow/pull/5703) [`ce6c869df`](https://github.com/xyflow/xyflow/commit/ce6c869df40b2b013484808c742ca508da4a591f) Thanks [@peterkogo](https://github.com/peterkogo)! - Improve return type of useNodesData. Now you can narrow down the data type by checking the node type.
- [#5692](https://github.com/xyflow/xyflow/pull/5692) [`49646858f`](https://github.com/xyflow/xyflow/commit/49646858f951455921aedbb83725b4225dbdef9d) Thanks [@moklick](https://github.com/moklick)! - Handle undefined node in mini map
- [#5684](https://github.com/xyflow/xyflow/pull/5684) [`382c654c3`](https://github.com/xyflow/xyflow/commit/382c654c315accca2005e39d477eed6649f12e40) Thanks [@ysds](https://github.com/ysds)! - Consolidate drag handler effects in useDrag to fix programmatic selection issues
- Updated dependencies [[`ce6c869df`](https://github.com/xyflow/xyflow/commit/ce6c869df40b2b013484808c742ca508da4a591f)]:
- @xyflow/system@0.0.75
## 12.10.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
"version": "12.10.0",
"version": "12.10.1",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",

View File

@@ -53,12 +53,10 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
const {
autoPanOnConnect,
domNode,
isValidConnection,
connectionMode,
connectionRadius,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
nodeLookup,
rfId: flowId,
@@ -93,10 +91,10 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
flowId,
cancelConnection,
panBy,
isValidConnection,
isValidConnection: (...args) => store.getState().isValidConnection?.(...args) ?? true,
onConnect: onConnectEdge,
onConnectStart: _onConnectStart,
onConnectEnd,
onConnectEnd: (...args) => store.getState().onConnectEnd?.(...args),
onReconnectEnd: _onReconnectEnd,
updateConnection,
getTransform: () => store.getState().transform,

View File

@@ -142,10 +142,10 @@ function HandleComponent(
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
onConnectEnd: (...args) => store.getState().onConnectEnd?.(...args),
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection,
isValidConnection: isValidConnection || ((...args) => store.getState().isValidConnection?.(...args) ?? true),
getTransform: () => store.getState().transform,
getFromHandle: () => store.getState().connection.fromHandle,
autoPanSpeed: currentStore.autoPanSpeed,

View File

@@ -51,11 +51,14 @@ export function NodesSelection<NodeType extends Node>({
}
}, [disableKeyboardA11y]);
const shouldRender = !userSelectionActive && width !== null && height !== null;
useDrag({
nodeRef,
disabled: !shouldRender,
});
if (userSelectionActive || !width || !height) {
if (!shouldRender) {
return null;
}

View File

@@ -52,10 +52,11 @@ export function useDrag({
}, []);
useEffect(() => {
if (disabled) {
xyDrag.current?.destroy();
} else if (nodeRef.current) {
xyDrag.current?.update({
if (disabled || !nodeRef.current || !xyDrag.current) {
return;
}
xyDrag.current.update({
noDragClassName,
handleSelector,
domNode: nodeRef.current,
@@ -63,11 +64,11 @@ export function useDrag({
nodeId,
nodeClickDistance,
});
return () => {
xyDrag.current?.destroy();
};
}
}, [noDragClassName, handleSelector, disabled, isSelectable, nodeRef, nodeId]);
}, [noDragClassName, handleSelector, disabled, isSelectable, nodeRef, nodeId, nodeClickDistance]);
return dragging;
}

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { shallowNodeData } from '@xyflow/system';
import { DistributivePick, shallowNodeData } from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import type { Node } from '../types';
@@ -25,11 +25,11 @@ import type { Node } from '../types';
export function useNodesData<NodeType extends Node = Node>(
/** The id of the node to get the data from. */
nodeId: string
): Pick<NodeType, 'id' | 'type' | 'data'> | null;
): DistributivePick<NodeType, 'id' | 'type' | 'data'> | null;
export function useNodesData<NodeType extends Node = Node>(
/** The ids of the nodes to get the data from. */
nodeIds: string[]
): Pick<NodeType, 'id' | 'type' | 'data'>[];
): DistributivePick<NodeType, 'id' | 'type' | 'data'>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any {
const nodesData = useStore(

View File

@@ -1,5 +1,18 @@
# @xyflow/svelte
## 1.5.1
### Patch Changes
- [#5704](https://github.com/xyflow/xyflow/pull/5704) [`c91d3d022`](https://github.com/xyflow/xyflow/commit/c91d3d022f4517f4403a898cd02ee891b7e1f2d2) Thanks [@peterkogo](https://github.com/peterkogo)! - Keep `onConnectEnd` and `isValidConnection` up to date in an ongoing connection
- [#5703](https://github.com/xyflow/xyflow/pull/5703) [`ce6c869df`](https://github.com/xyflow/xyflow/commit/ce6c869df40b2b013484808c742ca508da4a591f) Thanks [@peterkogo](https://github.com/peterkogo)! - Improve return type of useNodesData. Now you can narrow down the data type by checking the node type.
- [#5698](https://github.com/xyflow/xyflow/pull/5698) [`7eeebc9c0`](https://github.com/xyflow/xyflow/commit/7eeebc9c04ce88f821363e3ea83e4dae36e04c45) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix child nodes not updating on the Minimap when parent is dragged
- Updated dependencies [[`ce6c869df`](https://github.com/xyflow/xyflow/commit/ce6c869df40b2b013484808c742ca508da4a591f)]:
- @xyflow/system@0.0.75
## 1.5.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/svelte",
"version": "1.5.0",
"version": "1.5.1",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [
"svelte",

View File

@@ -79,9 +79,9 @@
flowId,
cancelConnection,
panBy,
isValidConnection,
isValidConnection: (...args) => store.isValidConnection?.(...args) ?? true,
onConnectStart: _onConnectStart,
onConnectEnd: onconnectend,
onConnectEnd: (...args) => store.onconnectend?.(...args),
onConnect: (connection) => {
const reconnectedEdge = { ...edge, ...connection };
const newEdge = onbeforereconnect

View File

@@ -125,21 +125,14 @@
autoPanOnConnect: store.autoPanOnConnect,
autoPanSpeed: store.autoPanSpeed,
flowId: store.flowId,
isValidConnection: isValidConnection ?? store.isValidConnection,
isValidConnection:
isValidConnection || ((...args) => store.isValidConnection?.(...args) ?? true),
updateConnection: store.updateConnection,
cancelConnection: store.cancelConnection,
panBy: store.panBy,
onConnect: onConnectExtended,
onConnectStart: (event, startParams) => {
store.onconnectstart?.(event, {
nodeId: startParams.nodeId,
handleId: startParams.handleId,
handleType: startParams.handleType
});
},
onConnectEnd: (event, connectionState) => {
store.onconnectend?.(event, connectionState);
},
onConnectStart: store.onconnectstart,
onConnectEnd: (...args) => store.onconnectend?.(...args),
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle,
dragThreshold: store.connectionDragThreshold,

View File

@@ -1,4 +1,4 @@
import { shallowNodeData } from '@xyflow/system';
import { shallowNodeData, type DistributivePick } from '@xyflow/system';
import type { Node } from '$lib/types';
import { useStore } from '$lib/store';
@@ -12,10 +12,10 @@ import { useStore } from '$lib/store';
*/
export function useNodesData<NodeType extends Node = Node>(
nodeId: string
): { current: Pick<NodeType, 'id' | 'data' | 'type'> | null };
): { current: DistributivePick<NodeType, 'id' | 'data' | 'type'> | null };
export function useNodesData<NodeType extends Node = Node>(
nodeIds: string[]
): { current: Pick<NodeType, 'id' | 'data' | 'type'>[] };
): { current: DistributivePick<NodeType, 'id' | 'data' | 'type'>[] };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any {
const { nodes, nodeLookup } = $derived(useStore());

View File

@@ -6,12 +6,7 @@
</script>
<script lang="ts">
import {
getBoundsOfRects,
getInternalNodesBounds,
getNodeDimensions,
nodeHasDimensions
} from '@xyflow/system';
import { getBoundsOfRects, getInternalNodesBounds, nodeHasDimensions } from '@xyflow/system';
import { useStore } from '$lib/store';
import { Panel } from '$lib/container/Panel';
@@ -45,9 +40,6 @@
let store = $derived(useStore());
let ariaLabelConfig = $derived(store.ariaLabelConfig);
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassFunc = getAttrFunction(nodeClass);
const shapeRendering =
// @ts-expect-error - TS doesn't know about chrome
typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
@@ -119,20 +111,16 @@
{#each store.nodes as userNode (userNode.id)}
{@const node = store.nodeLookup.get(userNode.id)}
{#if node && nodeHasDimensions(node) && !node.hidden}
{@const nodeDimesions = getNodeDimensions(node)}
<MinimapNode
id={node.id}
x={node.internals.positionAbsolute.x}
y={node.internals.positionAbsolute.y}
{...nodeDimesions}
selected={node.selected}
{nodeComponent}
color={nodeColorFunc?.(node)}
color={nodeColor === undefined ? undefined : getAttrFunction(nodeColor)(userNode)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeColor={getAttrFunction(nodeStrokeColor)(userNode)}
strokeWidth={nodeStrokeWidth}
{shapeRendering}
class={nodeClassFunc(node)}
class={getAttrFunction(nodeClass)(userNode)}
/>
{/if}
{/each}

View File

@@ -1,14 +1,15 @@
<script lang="ts">
import type { ClassValue } from 'svelte/elements';
import type { Component } from 'svelte';
import type { MiniMapNodeProps } from './types';
import { useInternalNode } from '$lib/hooks/useInternalNode.svelte';
import { getNodeDimensions } from '@xyflow/system';
let {
id,
x,
y,
width,
height,
x: xProp,
y: yProp,
width: widthProp,
height: heightProp,
borderRadius = 5,
color,
shapeRendering,
@@ -17,21 +18,26 @@
selected,
class: className,
nodeComponent
}: {
id: string;
x: number;
y: number;
width: number;
height: number;
borderRadius?: number;
color?: string;
shapeRendering: string;
strokeColor?: string;
strokeWidth?: number;
selected?: boolean;
class?: ClassValue;
}: MiniMapNodeProps & {
nodeComponent?: Component<MiniMapNodeProps>;
} = $props();
let internalNode = $derived(useInternalNode(id));
let { width, height, x, y } = $derived.by(() => {
if (!internalNode.current) {
return { width: 0, height: 0, x: 0, y: 0 };
}
const { width, height } = getNodeDimensions(internalNode.current);
return {
width: widthProp ?? width,
height: heightProp ?? height,
x: xProp ?? internalNode.current.internals.positionAbsolute.x,
y: yProp ?? internalNode.current.internals.positionAbsolute.y
};
});
</script>
{#if nodeComponent}

View File

@@ -12,10 +12,10 @@ export type GetMiniMapNodeAttribute = (node: Node) => string;
*/
export type MiniMapNodeProps = {
id: string;
x: number;
y: number;
width: number;
height: number;
x?: number;
y?: number;
width?: number;
height?: number;
borderRadius?: number;
class?: ClassValue;
color?: string;

View File

@@ -1,5 +1,11 @@
# @xyflow/system
## 0.0.75
### Patch Changes
- [#5703](https://github.com/xyflow/xyflow/pull/5703) [`ce6c869df`](https://github.com/xyflow/xyflow/commit/ce6c869df40b2b013484808c742ca508da4a591f) Thanks [@peterkogo](https://github.com/peterkogo)! - Improve return type of useNodesData. Now you can narrow down the data type by checking the node type.
## 0.0.74
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.74",
"version": "0.0.75",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",

View File

@@ -56,3 +56,10 @@ export type Transform = [number, number, number];
* to represent an unbounded extent.
*/
export type CoordinateExtent = [[number, number], [number, number]];
/**
* Using Pick with a union type (e.g. `NodeType`) will merge every property type along all union members.
* See https://github.com/microsoft/TypeScript/issues/28339#issuecomment-463577347
* Note: Currently you are able to Pick properties that are not in the type without error.
*/
export type DistributivePick<T, K extends string> = T extends unknown ? { [P in Extract<keyof T, K>]: T[P] } : never;

8440
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff