diff --git a/examples/vite-app/src/examples/Subflow/index.tsx b/examples/vite-app/src/examples/Subflow/index.tsx
index 55aea018..db021f52 100644
--- a/examples/vite-app/src/examples/Subflow/index.tsx
+++ b/examples/vite-app/src/examples/Subflow/index.tsx
@@ -11,7 +11,7 @@ import ReactFlow, {
Controls,
MiniMap,
Background,
- NodeOrigin,
+ Panel,
} from 'reactflow';
import DebugNode from './DebugNode';
@@ -211,7 +211,7 @@ const Subflow = () => {
-
+
@@ -225,7 +225,7 @@ const Subflow = () => {
toggleChildNodes
-
+
);
};
diff --git a/packages/core/src/components/Nodes/wrapNode.tsx b/packages/core/src/components/Nodes/wrapNode.tsx
index 70b36ddb..f56f117b 100644
--- a/packages/core/src/components/Nodes/wrapNode.tsx
+++ b/packages/core/src/components/Nodes/wrapNode.tsx
@@ -88,8 +88,6 @@ export default (NodeComponent: ComponentType) => {
return;
}
- const { snapGrid, snapToGrid } = store.getState();
-
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
if (unselect) {
@@ -112,15 +110,10 @@ export default (NodeComponent: ComponentType) => {
.toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`,
});
- // by default a node moves 5px on each key press, or 20px if shift is pressed
- // if snap grid is enabled, we use that for the velocity.
- const xVelo = snapToGrid ? snapGrid[0] : 5;
- const yVelo = snapToGrid ? snapGrid[1] : 5;
- const factor = event.shiftKey ? 4 : 1;
-
updatePositions({
- x: arrowKeyDiffs[event.key].x * xVelo * factor,
- y: arrowKeyDiffs[event.key].y * yVelo * factor,
+ x: arrowKeyDiffs[event.key].x,
+ y: arrowKeyDiffs[event.key].y,
+ isShiftPressed: event.shiftKey,
});
}
};
diff --git a/packages/core/src/components/NodesSelection/index.tsx b/packages/core/src/components/NodesSelection/index.tsx
index cff39b5d..745a0550 100644
--- a/packages/core/src/components/NodesSelection/index.tsx
+++ b/packages/core/src/components/NodesSelection/index.tsx
@@ -41,7 +41,9 @@ function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboar
useEffect(() => {
if (!disableKeyboardA11y) {
- nodeRef.current?.focus();
+ nodeRef.current?.focus({
+ preventScroll: true,
+ });
}
}, [disableKeyboardA11y]);
@@ -65,7 +67,11 @@ function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboar
const onKeyDown = (event: KeyboardEvent) => {
if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
- updatePositions(arrowKeyDiffs[event.key]);
+ updatePositions({
+ x: arrowKeyDiffs[event.key].x,
+ y: arrowKeyDiffs[event.key].y,
+ isShiftPressed: event.shiftKey,
+ });
}
};
diff --git a/packages/core/src/components/UserSelection/SelectionBox.tsx b/packages/core/src/components/UserSelection/SelectionBox.tsx
deleted file mode 100644
index bfd63ea1..00000000
--- a/packages/core/src/components/UserSelection/SelectionBox.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import shallow from 'zustand/shallow';
-import { useStore } from '../../hooks/useStore';
-import { ReactFlowState } from '../../types';
-
-const selector = (s: ReactFlowState) => ({
- userSelectionActive: s.userSelectionActive,
- userSelectionRect: s.userSelectionRect,
-});
-
-function SelectionBox() {
- const { userSelectionActive, userSelectionRect } = useStore(selector, shallow);
- const showSelectionBox = userSelectionActive && userSelectionRect;
-
- if (!showSelectionBox) {
- return null;
- }
-
- return (
-
- );
-}
-
-export default SelectionBox;
diff --git a/packages/core/src/components/UserSelection/index.tsx b/packages/core/src/components/UserSelection/index.tsx
index 26ba621c..34d16a36 100644
--- a/packages/core/src/components/UserSelection/index.tsx
+++ b/packages/core/src/components/UserSelection/index.tsx
@@ -1,245 +1,31 @@
-/**
- * The user selection rectangle gets displayed when a user drags the mouse while pressing shift
- */
-
-import { memo, useRef, MouseEvent as ReactMouseEvent } from 'react';
import shallow from 'zustand/shallow';
-import cc from 'classcat';
-import SelectionBox from './SelectionBox';
-import { containerStyle } from '../../styles';
-import { useStore, useStoreApi } from '../../hooks/useStore';
-import { getSelectionChanges } from '../../utils/changes';
-import { getConnectedEdges, getNodesInside } from '../../utils/graph';
-import { ReactFlowProps, SelectionMode } from '../../types';
-import type { XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types';
-
-type UserSelectionProps = {
- isSelecting: boolean;
- children: React.ReactNode;
-} & Partial<
- Pick<
- ReactFlowProps,
- | 'selectionMode'
- | 'panOnDrag'
- | 'onSelectionStart'
- | 'onSelectionEnd'
- | 'onPaneClick'
- | 'onPaneContextMenu'
- | 'onPaneScroll'
- | 'onPaneMouseEnter'
- | 'onPaneMouseMove'
- | 'onPaneMouseLeave'
- >
->;
-
-function getMousePosition(event: ReactMouseEvent, containerBounds: DOMRect): XYPosition {
- return {
- x: event.clientX - containerBounds.left,
- y: event.clientY - containerBounds.top,
- };
-}
-
-const wrapHandler = (
- handler: React.MouseEventHandler | undefined,
- containerRef: React.MutableRefObject
-): React.MouseEventHandler => {
- return (event: ReactMouseEvent) => {
- if (event.target !== containerRef.current) {
- return;
- }
- handler?.(event);
- };
-};
+import { useStore } from '../../hooks/useStore';
+import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
- elementsSelectable: s.elementsSelectable,
- paneDragging: s.paneDragging,
+ userSelectionRect: s.userSelectionRect,
});
-const UserSelection = memo(
- ({
- isSelecting,
- selectionMode = SelectionMode.Full,
- panOnDrag,
- onSelectionStart,
- onSelectionEnd,
- onPaneClick,
- onPaneContextMenu,
- onPaneScroll,
- onPaneMouseEnter,
- onPaneMouseMove,
- onPaneMouseLeave,
- children,
- }: UserSelectionProps) => {
- const container = useRef(null);
- const store = useStoreApi();
- const prevSelectedNodesCount = useRef(0);
- const prevSelectedEdgesCount = useRef(0);
- const containerBounds = useRef();
- const { userSelectionActive, elementsSelectable, paneDragging } = useStore(selector, shallow);
+function UserSelection() {
+ const { userSelectionActive, userSelectionRect } = useStore(selector, shallow);
+ const showSelectionBox = userSelectionActive && userSelectionRect;
- const resetUserSelection = () => {
- store.setState({ userSelectionActive: false, userSelectionRect: null });
-
- prevSelectedNodesCount.current = 0;
- prevSelectedEdgesCount.current = 0;
- };
-
- const onClick = (event: ReactMouseEvent) => {
- onPaneClick?.(event);
- store.getState().resetSelectedElements();
- store.setState({ nodesSelectionActive: false });
- };
-
- const onContextMenu = (event: ReactMouseEvent) => {
- if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
- event.preventDefault();
- return;
- }
-
- onPaneContextMenu?.(event);
- };
-
- const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
-
- const onMouseDown = (event: ReactMouseEvent): void => {
- const { resetSelectedElements, domNode } = store.getState();
- containerBounds.current = domNode?.getBoundingClientRect();
-
- if (
- !elementsSelectable ||
- !isSelecting ||
- event.button !== 0 ||
- event.target !== container.current ||
- !containerBounds.current
- ) {
- return;
- }
-
- const { x, y } = getMousePosition(event, containerBounds.current);
-
- resetSelectedElements();
-
- store.setState({
- userSelectionRect: {
- width: 0,
- height: 0,
- startX: x,
- startY: y,
- x,
- y,
- },
- });
-
- onSelectionStart?.(event);
- };
-
- const onMouseMove = (event: ReactMouseEvent): void => {
- const { userSelectionRect, nodeInternals, edges, transform, onNodesChange, onEdgesChange, nodeOrigin, getNodes } =
- store.getState();
- if (!isSelecting || !containerBounds.current || !userSelectionRect) {
- return;
- }
-
- store.setState({ userSelectionActive: true, nodesSelectionActive: false });
-
- const mousePos = getMousePosition(event, containerBounds.current);
- const startX = userSelectionRect.startX ?? 0;
- const startY = userSelectionRect.startY ?? 0;
-
- const nextUserSelectRect = {
- ...userSelectionRect,
- x: mousePos.x < startX ? mousePos.x : startX,
- y: mousePos.y < startY ? mousePos.y : startY,
- width: Math.abs(mousePos.x - startX),
- height: Math.abs(mousePos.y - startY),
- };
-
- const nodes = getNodes();
- const selectedNodes = getNodesInside(
- nodeInternals,
- nextUserSelectRect,
- transform,
- selectionMode === SelectionMode.Partial,
- true,
- nodeOrigin
- );
- const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
- const selectedNodeIds = selectedNodes.map((n) => n.id);
-
- if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
- prevSelectedNodesCount.current = selectedNodeIds.length;
- const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
- if (changes.length) {
- onNodesChange?.(changes);
- }
- }
-
- if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
- prevSelectedEdgesCount.current = selectedEdgeIds.length;
- const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
- if (changes.length) {
- onEdgesChange?.(changes);
- }
- }
-
- store.setState({
- userSelectionRect: nextUserSelectRect,
- });
- };
-
- const onMouseUp = (event: ReactMouseEvent) => {
- const { userSelectionRect } = store.getState();
- // We only want to trigger click functions when in selection mode if
- // the user did not move the mouse.
- if (!userSelectionActive && userSelectionRect && event.target === container.current) {
- onClick?.(event);
- }
-
- store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
-
- resetUserSelection();
- onSelectionEnd?.(event);
- };
-
- const onMouseLeave = (event: ReactMouseEvent) => {
- if (userSelectionActive) {
- store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
- onSelectionEnd?.(event);
- }
-
- resetUserSelection();
- };
-
- const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
-
- return (
-
- {children}
-
-
- );
+ if (!showSelectionBox) {
+ return null;
}
-);
-UserSelection.displayName = 'UserSelection';
+ return (
+
+ );
+}
export default UserSelection;
diff --git a/packages/core/src/container/FlowRenderer/index.tsx b/packages/core/src/container/FlowRenderer/index.tsx
index 37bd44d9..a8e9dc69 100644
--- a/packages/core/src/container/FlowRenderer/index.tsx
+++ b/packages/core/src/container/FlowRenderer/index.tsx
@@ -6,7 +6,7 @@ import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useKeyPress from '../../hooks/useKeyPress';
import { GraphViewProps } from '../GraphView';
import ZoomPane from '../ZoomPane';
-import UserSelection from '../../components/UserSelection';
+import Pane from '../Pane';
import NodesSelection from '../../components/NodesSelection';
import type { ReactFlowState } from '../../types';
@@ -100,7 +100,7 @@ const FlowRenderer = ({
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
-
)}
-
+
);
};
diff --git a/packages/core/src/container/Pane/index.tsx b/packages/core/src/container/Pane/index.tsx
new file mode 100644
index 00000000..755e0c04
--- /dev/null
+++ b/packages/core/src/container/Pane/index.tsx
@@ -0,0 +1,241 @@
+/**
+ * The user selection rectangle gets displayed when a user drags the mouse while pressing shift
+ */
+
+import { memo, useRef, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
+import shallow from 'zustand/shallow';
+import cc from 'classcat';
+
+import UserSelection from '../../components/UserSelection';
+import { containerStyle } from '../../styles';
+import { useStore, useStoreApi } from '../../hooks/useStore';
+import { getSelectionChanges } from '../../utils/changes';
+import { getConnectedEdges, getNodesInside } from '../../utils/graph';
+import { SelectionMode } from '../../types';
+import type { ReactFlowProps, XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types';
+
+type PaneProps = {
+ isSelecting: boolean;
+ children: ReactNode;
+} & Partial<
+ Pick<
+ ReactFlowProps,
+ | 'selectionMode'
+ | 'panOnDrag'
+ | 'onSelectionStart'
+ | 'onSelectionEnd'
+ | 'onPaneClick'
+ | 'onPaneContextMenu'
+ | 'onPaneScroll'
+ | 'onPaneMouseEnter'
+ | 'onPaneMouseMove'
+ | 'onPaneMouseLeave'
+ >
+>;
+
+function getMousePosition(event: ReactMouseEvent, containerBounds: DOMRect): XYPosition {
+ return {
+ x: event.clientX - containerBounds.left,
+ y: event.clientY - containerBounds.top,
+ };
+}
+
+const wrapHandler = (
+ handler: React.MouseEventHandler | undefined,
+ containerRef: React.MutableRefObject
+): React.MouseEventHandler => {
+ return (event: ReactMouseEvent) => {
+ if (event.target !== containerRef.current) {
+ return;
+ }
+ handler?.(event);
+ };
+};
+
+const selector = (s: ReactFlowState) => ({
+ userSelectionActive: s.userSelectionActive,
+ elementsSelectable: s.elementsSelectable,
+ dragging: s.paneDragging,
+});
+
+const Pane = memo(
+ ({
+ isSelecting,
+ selectionMode = SelectionMode.Full,
+ panOnDrag,
+ onSelectionStart,
+ onSelectionEnd,
+ onPaneClick,
+ onPaneContextMenu,
+ onPaneScroll,
+ onPaneMouseEnter,
+ onPaneMouseMove,
+ onPaneMouseLeave,
+ children,
+ }: PaneProps) => {
+ const container = useRef(null);
+ const store = useStoreApi();
+ const prevSelectedNodesCount = useRef(0);
+ const prevSelectedEdgesCount = useRef(0);
+ const containerBounds = useRef();
+ const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow);
+
+ const resetUserSelection = () => {
+ store.setState({ userSelectionActive: false, userSelectionRect: null });
+
+ prevSelectedNodesCount.current = 0;
+ prevSelectedEdgesCount.current = 0;
+ };
+
+ const onClick = (event: ReactMouseEvent) => {
+ onPaneClick?.(event);
+ store.getState().resetSelectedElements();
+ store.setState({ nodesSelectionActive: false });
+ };
+
+ const onContextMenu = (event: ReactMouseEvent) => {
+ if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
+ event.preventDefault();
+ return;
+ }
+
+ onPaneContextMenu?.(event);
+ };
+
+ const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
+
+ const onMouseDown = (event: ReactMouseEvent): void => {
+ const { resetSelectedElements, domNode } = store.getState();
+ containerBounds.current = domNode?.getBoundingClientRect();
+
+ if (
+ !elementsSelectable ||
+ !isSelecting ||
+ event.button !== 0 ||
+ event.target !== container.current ||
+ !containerBounds.current
+ ) {
+ return;
+ }
+
+ const { x, y } = getMousePosition(event, containerBounds.current);
+
+ resetSelectedElements();
+
+ store.setState({
+ userSelectionRect: {
+ width: 0,
+ height: 0,
+ startX: x,
+ startY: y,
+ x,
+ y,
+ },
+ });
+
+ onSelectionStart?.(event);
+ };
+
+ const onMouseMove = (event: ReactMouseEvent): void => {
+ const { userSelectionRect, nodeInternals, edges, transform, onNodesChange, onEdgesChange, nodeOrigin, getNodes } =
+ store.getState();
+ if (!isSelecting || !containerBounds.current || !userSelectionRect) {
+ return;
+ }
+
+ store.setState({ userSelectionActive: true, nodesSelectionActive: false });
+
+ const mousePos = getMousePosition(event, containerBounds.current);
+ const startX = userSelectionRect.startX ?? 0;
+ const startY = userSelectionRect.startY ?? 0;
+
+ const nextUserSelectRect = {
+ ...userSelectionRect,
+ x: mousePos.x < startX ? mousePos.x : startX,
+ y: mousePos.y < startY ? mousePos.y : startY,
+ width: Math.abs(mousePos.x - startX),
+ height: Math.abs(mousePos.y - startY),
+ };
+
+ const nodes = getNodes();
+ const selectedNodes = getNodesInside(
+ nodeInternals,
+ nextUserSelectRect,
+ transform,
+ selectionMode === SelectionMode.Partial,
+ true,
+ nodeOrigin
+ );
+ const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
+ const selectedNodeIds = selectedNodes.map((n) => n.id);
+
+ if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
+ prevSelectedNodesCount.current = selectedNodeIds.length;
+ const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
+ if (changes.length) {
+ onNodesChange?.(changes);
+ }
+ }
+
+ if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
+ prevSelectedEdgesCount.current = selectedEdgeIds.length;
+ const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
+ if (changes.length) {
+ onEdgesChange?.(changes);
+ }
+ }
+
+ store.setState({
+ userSelectionRect: nextUserSelectRect,
+ });
+ };
+
+ const onMouseUp = (event: ReactMouseEvent) => {
+ const { userSelectionRect } = store.getState();
+ // We only want to trigger click functions when in selection mode if
+ // the user did not move the mouse.
+ if (!userSelectionActive && userSelectionRect && event.target === container.current) {
+ onClick?.(event);
+ }
+
+ store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
+
+ resetUserSelection();
+ onSelectionEnd?.(event);
+ };
+
+ const onMouseLeave = (event: ReactMouseEvent) => {
+ if (userSelectionActive) {
+ store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
+ onSelectionEnd?.(event);
+ }
+
+ resetUserSelection();
+ };
+
+ const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
+
+ return (
+
+ {children}
+
+
+ );
+ }
+);
+
+Pane.displayName = 'Pane';
+
+export default Pane;
diff --git a/packages/core/src/hooks/useUpdateNodePositions.ts b/packages/core/src/hooks/useUpdateNodePositions.ts
index 56e55925..b20ea4d1 100644
--- a/packages/core/src/hooks/useUpdateNodePositions.ts
+++ b/packages/core/src/hooks/useUpdateNodePositions.ts
@@ -2,18 +2,25 @@ import { useCallback } from 'react';
import { useStoreApi } from '../hooks/useStore';
import { calcNextPosition } from './useDrag/utils';
-import type { XYPosition } from '../types';
function useUpdateNodePositions() {
const store = useStoreApi();
- const updatePositions = useCallback((positionDiff: XYPosition) => {
+ const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => {
const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid } = store.getState();
const selectedNodes = getNodes().filter((n) => n.selected);
+ // by default a node moves 5px on each key press, or 20px if shift is pressed
+ // if snap grid is enabled, we use that for the velocity.
+ const xVelo = snapToGrid ? snapGrid[0] : 5;
+ const yVelo = snapToGrid ? snapGrid[1] : 5;
+ const factor = params.isShiftPressed ? 4 : 1;
+
+ const positionDiffX = params.x * xVelo * factor;
+ const positionDiffY = params.y * yVelo * factor;
const nodeUpdates = selectedNodes.map((n) => {
if (n.positionAbsolute) {
- const nextPosition = { x: n.positionAbsolute.x + positionDiff.x, y: n.positionAbsolute.y + positionDiff.y };
+ const nextPosition = { x: n.positionAbsolute.x + positionDiffX, y: n.positionAbsolute.y + positionDiffY };
if (snapToGrid) {
nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]);
@@ -29,7 +36,7 @@ function useUpdateNodePositions() {
return n;
});
- updateNodePositions(nodeUpdates, true, true);
+ updateNodePositions(nodeUpdates, true, false);
}, []);
return updatePositions;