node {id}
-
diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx
index d99ddb43..cbeb2478 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 a7eb71c3..ddb6d327 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
- "version": "12.0.0-next.7",
+ "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 cd3f64ae..f841c8b6 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 28323a19..5973751d 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 1e796de1..548a4aa4 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 { 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 d3ed7a06..9a88c80b 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';
@@ -35,7 +35,7 @@ const selector = (s: ReactFlowState) => {
export function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y }: NodesSelectionProps) {
const store = useStoreApi();
const { width, height, transformString, userSelectionActive } = useStore(selector, shallow);
- const updatePositions = useUpdateNodePositions();
+ const moveSelectedNodes = useMoveSelectedNodes();
const nodeRef = useRef(null);
@@ -64,10 +64,9 @@ export function NodesSelection({ onSelectionContextMenu, noPanClassName, disable
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 3a12594c..f1d68e6c 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 = [
@@ -76,48 +76,51 @@ 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 fd7ac277..b95fa610 100644
--- a/packages/react/src/container/GraphView/index.tsx
+++ b/packages/react/src/container/GraphView/index.tsx
@@ -27,7 +27,6 @@ export type GraphViewProps = Omit<
| 'maxZoom'
| 'defaultMarkerColor'
| 'noDragClassName'
- | 'noDragClassName'
| 'noWheelClassName'
| 'noPanClassName'
| 'defaultViewport'
diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx
index 4d3cb7b7..b82ca249 100644
--- a/packages/react/src/container/ReactFlow/Wrapper.tsx
+++ b/packages/react/src/container/ReactFlow/Wrapper.tsx
@@ -8,6 +8,8 @@ export function Wrapper({
children,
nodes,
edges,
+ defaultNodes,
+ defaultEdges,
width,
height,
fitView,
@@ -15,6 +17,8 @@ export function Wrapper({
children: ReactNode;
nodes?: Node[];
edges?: Edge[];
+ defaultNodes?: Node[];
+ defaultEdges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
@@ -31,6 +35,8 @@ export function Wrapper({
(
nodesDraggable,
nodesConnectable,
nodesFocusable,
- nodeOrigin = initNodeOrigin,
+ nodeOrigin = defaultNodeOrigin,
edgesFocusable,
edgesUpdatable,
elementsSelectable = true,
- defaultViewport = initDefaultViewport,
+ defaultViewport = initViewport,
minZoom = 0.5,
maxZoom = 2,
translateExtent = infiniteExtent,
@@ -166,7 +156,15 @@ const ReactFlow = forwardRef(
data-testid="rf__wrapper"
id={id}
>
-
+
| null>(null);
- const currentNodeId = nodeId || _nodeId;
+ const currentNodeId = nodeId ?? _nodeId;
+
+ const prevConnections = useRef