node {id}
-
diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx
index b9bc68f1..41c537ab 100644
--- a/examples/react/src/examples/UseNodesData/index.tsx
+++ b/examples/react/src/examples/UseNodesData/index.tsx
@@ -41,7 +41,6 @@ const initNodes: MyNode[] = [
data: {},
position: { x: 100, y: 0 },
},
-
{
id: '2',
type: 'text',
diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte
index 61257ee1..39778156 100644
--- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte
+++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte
@@ -100,6 +100,29 @@
data: { label: 'horizontal resizer with maxWidth', maxWidth: 300 },
position: { x: 250, y: 400 },
style: nodeStyle
+ },
+ {
+ id: '5',
+ type: 'defaultResizer',
+ data: { label: 'Parent' },
+ position: { x: 700, y: 0 },
+ style: nodeStyle + 'width: 300px; height: 300px'
+ },
+ {
+ id: '5a',
+ type: 'defaultResizer',
+ data: { label: 'Child' },
+ position: { x: 50, y: 50 },
+ parentNode: '5',
+ style: nodeStyle
+ },
+ {
+ id: '5b',
+ type: 'defaultResizer',
+ data: { label: 'Child' },
+ position: { x: 100, y: 100 },
+ parentNode: '5',
+ style: nodeStyle
}
]);
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index 6931ea5b..e2e23ca4 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,14 @@
# @xyflow/react
+## 12.0.0-next.9
+
+### Patch changes
+
+- a better `NodeResizer` that works with subflows. Child nodes do not move when parent node gets resized and parent extent is taken into account
+- refactor `setNodes` batching
+- re-measure nodes when necessary
+- don't trigger drag start / end when node is not draggable
+
## 12.0.0-next.8
### Patch changes
diff --git a/packages/react/package.json b/packages/react/package.json
index 097cba0d..ddb6d327 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
- "version": "12.0.0-next.8",
+ "version": "12.0.0-next.9",
"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 60caca1b..c4bbf13b 100644
--- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx
+++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx
@@ -49,9 +49,10 @@ function MiniMapComponent
({
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
nodeComponent,
+ bgColor,
maskColor,
- maskStrokeColor = 'none',
- maskStrokeWidth = 1,
+ maskStrokeColor,
+ maskStrokeWidth,
position = 'bottom-right',
onClick,
onNodeClick,
@@ -130,7 +131,11 @@ function MiniMapComponent({
style={
{
...style,
- '--xy-minimap-mask-color-props': typeof maskColor === 'string' ? maskColor : undefined,
+ '--xy-minimap-background-color-props': typeof bgColor === 'string' ? bgColor : undefined,
+ '--xy-minimap-mask-background-color-props': typeof maskColor === 'string' ? maskColor : undefined,
+ '--xy-minimap-mask-stroke-color-props': typeof maskStrokeColor === 'string' ? maskStrokeColor : undefined,
+ '--xy-minimap-mask-stroke-width-props':
+ typeof maskStrokeWidth === 'number' ? maskStrokeWidth * viewScale : undefined,
'--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
'--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined,
@@ -143,6 +148,7 @@ function MiniMapComponent({
width={elementWidth}
height={elementHeight}
viewBox={`${x} ${y} ${width} ${height}`}
+ className="react-flow__minimap-svg"
role="img"
aria-labelledby={labelledBy}
ref={svg}
@@ -163,8 +169,6 @@ function MiniMapComponent({
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
fillRule="evenodd"
- stroke={maskStrokeColor}
- strokeWidth={maskStrokeWidth}
pointerEvents="none"
/>
diff --git a/packages/react/src/additional-components/MiniMap/types.ts b/packages/react/src/additional-components/MiniMap/types.ts
index 31e67428..4bfd1a51 100644
--- a/packages/react/src/additional-components/MiniMap/types.ts
+++ b/packages/react/src/additional-components/MiniMap/types.ts
@@ -19,6 +19,8 @@ export type MiniMapProps = Omit;
+ /** Background color of minimap */
+ bgColor?: string;
/** Color of mask representing viewport */
maskColor?: string;
/** Stroke color of mask representing viewport */
diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx
index 2f16f150..c03c023e 100644
--- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx
+++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx
@@ -1,6 +1,12 @@
import { useRef, useEffect, memo } from 'react';
import cc from 'classcat';
-import { XYResizer, ResizeControlVariant, type XYResizerInstance, type XYResizerChange } from '@xyflow/system';
+import {
+ XYResizer,
+ ResizeControlVariant,
+ type XYResizerInstance,
+ type XYResizerChange,
+ XYResizerChildChange,
+} from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
@@ -52,7 +58,7 @@ function ResizeControl({
snapToGrid,
};
},
- onChange: (change: XYResizerChange) => {
+ onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
const { triggerNodeChanges } = store.getState();
const changes: NodeChange[] = [];
@@ -83,6 +89,16 @@ function ResizeControl({
changes.push(dimensionChange);
}
+
+ for (const childChange of childChanges) {
+ const positionChange: NodePositionChange = {
+ ...childChange,
+ type: 'position',
+ };
+
+ changes.push(positionChange);
+ }
+
triggerNodeChanges(changes);
},
onEnd: () => {
diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx
index b1fdb4a8..cbe01b8c 100644
--- a/packages/react/src/components/EdgeWrapper/index.tsx
+++ b/packages/react/src/components/EdgeWrapper/index.tsx
@@ -101,12 +101,12 @@ export function EdgeWrapper({
);
const markerStartUrl = useMemo(
- () => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined),
+ () => (edge.markerStart ? `url('#${getMarkerId(edge.markerStart, rfId)}')` : undefined),
[edge.markerStart, rfId]
);
const markerEndUrl = useMemo(
- () => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined),
+ () => (edge.markerEnd ? `url('#${getMarkerId(edge.markerEnd, rfId)}')` : undefined),
[edge.markerEnd, rfId]
);
diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx
index 017dd0f9..4199d158 100644
--- a/packages/react/src/components/NodeWrapper/index.tsx
+++ b/packages/react/src/components/NodeWrapper/index.tsx
@@ -14,7 +14,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import { Provider } from '../../contexts/NodeIdContext';
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
import { useDrag } from '../../hooks/useDrag';
-import { useUpdateNodePositions } from '../../hooks/useUpdateNodePositions';
+import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { handleNodeClick } from '../Nodes/utils';
import { arrowKeyDiffs, builtinNodeTypes } from './utils';
import type { Node, NodeWrapperProps } from '../../types';
@@ -79,16 +79,33 @@ export function NodeWrapper({
const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType);
- const updatePositions = useUpdateNodePositions();
+ 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 hasHandleBounds = !!node[internalsSymbol]?.handleBounds;
+
+ const moveSelectedNodes = useMoveSelectedNodes();
+
+ useEffect(() => {
+ return () => {
+ if (nodeRef.current) {
+ resizeObserver?.unobserve(nodeRef.current);
+ }
+ };
+ }, []);
useEffect(() => {
if (nodeRef.current && !node.hidden) {
const currNode = nodeRef.current;
- resizeObserver?.observe(currNode);
- return () => resizeObserver?.unobserve(currNode);
+ if (!initialized || !hasHandleBounds) {
+ resizeObserver?.unobserve(currNode);
+ resizeObserver?.observe(currNode);
+ }
}
- }, [node.hidden]);
+ }, [node.hidden, initialized, hasHandleBounds]);
useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
@@ -123,11 +140,6 @@ export function NodeWrapper({
return null;
}
- const width = node.width ?? undefined;
- const height = node.height ?? undefined;
- const computedWidth = node.computed?.width;
- const computedHeight = node.computed?.height;
-
const positionAbsoluteOrigin = getPositionWithOrigin({
x: positionAbsoluteX,
y: positionAbsoluteY,
@@ -135,7 +147,6 @@ export function NodeWrapper({
height: computedHeight ?? height ?? 0,
origin: node.origin || nodeOrigin,
});
- const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
@@ -188,10 +199,9 @@ export function NodeWrapper({
.toLowerCase()}. New position, x: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`,
});
- updatePositions({
- x: arrowKeyDiffs[event.key].x,
- y: arrowKeyDiffs[event.key].y,
- isShiftPressed: event.shiftKey,
+ moveSelectedNodes({
+ direction: arrowKeyDiffs[event.key],
+ factor: event.shiftKey ? 4 : 1,
});
}
};
diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx
index 9ded77b8..c0423827 100644
--- a/packages/react/src/components/NodesSelection/index.tsx
+++ b/packages/react/src/components/NodesSelection/index.tsx
@@ -10,7 +10,7 @@ import { getNodesBounds } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { useDrag } from '../../hooks/useDrag';
-import { useUpdateNodePositions } from '../../hooks/useUpdateNodePositions';
+import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { arrowKeyDiffs } from '../NodeWrapper/utils';
import type { Node, ReactFlowState } from '../../types';
@@ -39,7 +39,7 @@ export function NodesSelection({
}: NodesSelectionProps) {
const store = useStoreApi();
const { width, height, transformString, userSelectionActive } = useStore(selector, shallow);
- const updatePositions = useUpdateNodePositions();
+ const moveSelectedNodes = useMoveSelectedNodes();
const nodeRef = useRef(null);
@@ -68,10 +68,9 @@ export function NodesSelection({
const onKeyDown = (event: KeyboardEvent) => {
if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
- updatePositions({
- x: arrowKeyDiffs[event.key].x,
- y: arrowKeyDiffs[event.key].y,
- isShiftPressed: event.shiftKey,
+ moveSelectedNodes({
+ direction: arrowKeyDiffs[event.key],
+ factor: event.shiftKey ? 4 : 1,
});
}
};
diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx
index 34999c6f..3d2577e7 100644
--- a/packages/react/src/components/ReactFlowProvider/index.tsx
+++ b/packages/react/src/components/ReactFlowProvider/index.tsx
@@ -10,6 +10,8 @@ export function ReactFlowProvider({
children,
initialNodes,
initialEdges,
+ defaultNodes,
+ defaultEdges,
initialWidth,
initialHeight,
fitView,
@@ -17,16 +19,19 @@ export function ReactFlowProvider({
children: ReactNode;
initialNodes?: Node[];
initialEdges?: Edge[];
+ defaultNodes?: Node[];
+ defaultEdges?: Edge[];
initialWidth?: number;
initialHeight?: number;
fitView?: boolean;
}) {
const storeRef = useRef> | null>(null);
-
if (!storeRef.current) {
storeRef.current = createRFStore({
nodes: initialNodes,
edges: initialEdges,
+ defaultNodes,
+ defaultEdges,
width: initialWidth,
height: initialHeight,
fitView,
diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx
index 9c77f5e0..85159721 100644
--- a/packages/react/src/components/StoreUpdater/index.tsx
+++ b/packages/react/src/components/StoreUpdater/index.tsx
@@ -9,7 +9,7 @@ import { infiniteExtent, type CoordinateExtent } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
-import { initNodeOrigin } from '../../container/ReactFlow';
+import { defaultNodeOrigin } from '../../container/ReactFlow/init-values';
// these fields exist in the global store and we need to keep them up to date
const reactFlowFieldsToTrack = [
@@ -81,50 +81,53 @@ const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const;
const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes,
setEdges: s.setEdges,
- setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setMinZoom: s.setMinZoom,
setMaxZoom: s.setMaxZoom,
setTranslateExtent: s.setTranslateExtent,
setNodeExtent: s.setNodeExtent,
reset: s.reset,
+ setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
});
+const initPrevValues = {
+ // these are values that are also passed directly to other components
+ // than the StoreUpdater. We can reduce the number of setStore calls
+ // by setting the same values here as prev fields.
+ translateExtent: infiniteExtent,
+ nodeOrigin: defaultNodeOrigin,
+ minZoom: 0.5,
+ maxZoom: 2,
+ elementsSelectable: true,
+ noPanClassName: 'nopan',
+ rfId: '1',
+};
+
export function StoreUpdater(
props: StoreUpdaterProps
) {
const {
setNodes,
setEdges,
- setDefaultNodesAndEdges,
setMinZoom,
setMaxZoom,
setTranslateExtent,
setNodeExtent,
reset,
+ setDefaultNodesAndEdges,
} = useStore(selector, shallow);
const store = useStoreApi();
useEffect(() => {
- const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions }));
- setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults);
+ setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges);
return () => {
+ // when we reset the store we also need to reset the previous fields
+ previousFields.current = initPrevValues;
reset();
};
}, []);
- const previousFields = useRef>>({
- // these are values that are also passed directly to other components
- // than the StoreUpdater. We can reduce the number of setStore calls
- // by setting the same values here as prev fields.
- translateExtent: infiniteExtent,
- nodeOrigin: initNodeOrigin,
- minZoom: 0.5,
- maxZoom: 2,
- elementsSelectable: true,
- noPanClassName: 'nopan',
- rfId: '1',
- });
+ const previousFields = useRef>>(initPrevValues);
useEffect(
() => {
diff --git a/packages/react/src/container/GraphView/index.tsx b/packages/react/src/container/GraphView/index.tsx
index f1965b04..80c25dea 100644
--- a/packages/react/src/container/GraphView/index.tsx
+++ b/packages/react/src/container/GraphView/index.tsx
@@ -27,7 +27,6 @@ export type GraphViewProps(
nodesDraggable,
nodesConnectable,
nodesFocusable,
- nodeOrigin = initNodeOrigin,
+ nodeOrigin = defaultNodeOrigin,
edgesFocusable,
edgesUpdatable,
elementsSelectable = true,
- defaultViewport = initDefaultViewport,
+ defaultViewport = initViewport,
minZoom = 0.5,
maxZoom = 2,
translateExtent = infiniteExtent,
@@ -230,7 +220,7 @@ function ReactFlow(
viewport={viewport}
onViewportChange={onViewportChange}
/>
-
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
diff --git a/packages/react/src/container/ReactFlow/init-values.ts b/packages/react/src/container/ReactFlow/init-values.ts
new file mode 100644
index 00000000..75e7fcaa
--- /dev/null
+++ b/packages/react/src/container/ReactFlow/init-values.ts
@@ -0,0 +1,4 @@
+import { type NodeOrigin, Viewport } from '@xyflow/system';
+
+export const defaultNodeOrigin: NodeOrigin = [0, 0];
+export const defaultViewport: Viewport = { x: 0, y: 0, zoom: 1 };
diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts
index 4f62615e..20b65a2d 100644
--- a/packages/react/src/hooks/useHandleConnections.ts
+++ b/packages/react/src/hooks/useHandleConnections.ts
@@ -1,5 +1,11 @@
import { useEffect, useMemo, useRef } from 'react';
-import { Connection, HandleType, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system';
+import {
+ Connection,
+ HandleConnection,
+ HandleType,
+ areConnectionMapsEqual,
+ handleConnectionChange,
+} from '@xyflow/system';
import { useStore } from './useStore';
import { useNodeId } from '../contexts/NodeIdContext';
@@ -21,7 +27,7 @@ type useHandleConnectionsParams = {
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
- * @returns an array with connections
+ * @returns an array with handle connections
*/
export function useHandleConnections({
type,
@@ -29,10 +35,11 @@ export function useHandleConnections({
nodeId,
onConnect,
onDisconnect,
-}: useHandleConnectionsParams): Connection[] {
+}: useHandleConnectionsParams): HandleConnection[] {
const _nodeId = useNodeId();
- const prevConnections = useRef