diff --git a/.changeset/strange-cows-explode.md b/.changeset/strange-cows-explode.md new file mode 100644 index 00000000..9efb9fdf --- /dev/null +++ b/.changeset/strange-cows-explode.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +fix: fitView for subflows, context manu on right mouse pan diff --git a/examples/vite-app/package.json b/examples/vite-app/package.json index 7e88c874..fe42723e 100644 --- a/examples/vite-app/package.json +++ b/examples/vite-app/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite --port 3000 --open", "serve": "vite serve --port 3000", + "build": "vite build", "test:dev": "cypress open", "test": "pnpm test-component && pnpm test-e2e", "test-component": "cypress run --component", diff --git a/examples/vite-app/src/examples/Subflow/index.tsx b/examples/vite-app/src/examples/Subflow/index.tsx index db021f52..75bd3629 100644 --- a/examples/vite-app/src/examples/Subflow/index.tsx +++ b/examples/vite-app/src/examples/Subflow/index.tsx @@ -22,7 +22,6 @@ const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const defaultViewport = { x: 0, y: 0, zoom: 1.5 }; - const initialNodes: Node[] = [ { id: '1', diff --git a/packages/core/src/container/ZoomPane/index.tsx b/packages/core/src/container/ZoomPane/index.tsx index 2099d9a1..ef5408e4 100644 --- a/packages/core/src/container/ZoomPane/index.tsx +++ b/packages/core/src/container/ZoomPane/index.tsx @@ -76,6 +76,7 @@ const ZoomPane = ({ const prevTransform = useRef({ x: 0, y: 0, zoom: 0 }); const { d3Zoom, d3Selection, d3ZoomHandler, userSelectionActive } = useStore(selector, shallow); const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode); + const mouseButton = useRef(0); useResizeHandler(zoomPane); @@ -164,30 +165,6 @@ const ZoomPane = ({ noWheelClassName, ]); - useEffect(() => { - if (d3Zoom) { - if (userSelectionActive && !isZoomingOrPanning.current) { - d3Zoom.on('zoom', null); - } else if (!userSelectionActive) { - d3Zoom.on('zoom', (event: D3ZoomEvent) => { - const { onViewportChange } = store.getState(); - store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] }); - - zoomedWithRightMouseButton.current = !!( - onPaneContextMenu && isRightClickPan(panOnDrag, event.sourceEvent?.button) - ); - - if (onMove || onViewportChange) { - const flowTransform = eventToFlowTransform(event.transform); - - onViewportChange?.(flowTransform); - onMove?.(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); - } - }); - } - } - }, [userSelectionActive, d3Zoom, onMove, panOnDrag, onPaneContextMenu]); - useEffect(() => { if (d3Zoom) { d3Zoom.on('start', (event: D3ZoomEvent) => { @@ -195,6 +172,9 @@ const ZoomPane = ({ return null; } + // we need to remember it here, because it's always 0 in the "zoom" event + mouseButton.current = event.sourceEvent.button; + const { onViewportChangeStart } = store.getState(); isZoomingOrPanning.current = true; @@ -213,6 +193,30 @@ const ZoomPane = ({ } }, [d3Zoom, onMoveStart]); + useEffect(() => { + if (d3Zoom) { + if (userSelectionActive && !isZoomingOrPanning.current) { + d3Zoom.on('zoom', null); + } else if (!userSelectionActive) { + d3Zoom.on('zoom', (event: D3ZoomEvent) => { + const { onViewportChange } = store.getState(); + store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] }); + + zoomedWithRightMouseButton.current = !!( + onPaneContextMenu && isRightClickPan(panOnDrag, mouseButton.current ?? 0) + ); + + if (onMove || onViewportChange) { + const flowTransform = eventToFlowTransform(event.transform); + + onViewportChange?.(flowTransform); + onMove?.(event.sourceEvent as MouseEvent | TouchEvent, flowTransform); + } + }); + } + } + }, [userSelectionActive, d3Zoom, onMove, panOnDrag, onPaneContextMenu]); + useEffect(() => { if (d3Zoom) { d3Zoom.on('end', (event: D3ZoomEvent) => { @@ -226,7 +230,7 @@ const ZoomPane = ({ if ( onPaneContextMenu && - isRightClickPan(panOnDrag, event.sourceEvent?.button) && + isRightClickPan(panOnDrag, mouseButton.current ?? 0) && !zoomedWithRightMouseButton.current ) { onPaneContextMenu(event.sourceEvent); diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 5ff9bf05..5c4d65a6 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -3,7 +3,7 @@ import { createStore } from 'zustand'; import { clampPosition, getDimensions, internalsSymbol } from '../utils'; import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { getHandleBounds } from '../components/Nodes/utils'; -import { createNodeInternals, fitView, updateNodesAndEdgesSelections } from './utils'; +import { createNodeInternals, fitView, updateAbsoluteNodePositions, updateNodesAndEdgesSelections } from './utils'; import initialState from './initialState'; import type { ReactFlowState, @@ -99,6 +99,8 @@ const createRFStore = () => return res; }, []); + updateAbsoluteNodePositions(nodeInternals, nodeOrigin); + const nextFitViewOnInitDone = fitViewOnInitDone || (fitViewOnInit && !fitViewOnInitDone && fitView(get, { initial: true, ...fitViewOnInitOptions })); diff --git a/packages/core/src/store/utils.ts b/packages/core/src/store/utils.ts index 0224e656..caf860af 100644 --- a/packages/core/src/store/utils.ts +++ b/packages/core/src/store/utils.ts @@ -20,7 +20,6 @@ type ParentNodes = Record; function calculateXYZPosition( node: Node, nodeInternals: NodeInternals, - parentNodes: ParentNodes, result: XYZPosition, nodeOrigin: NodeOrigin ): XYZPosition { @@ -33,7 +32,6 @@ function calculateXYZPosition( return calculateXYZPosition( parentNode, nodeInternals, - parentNodes, { x: (result.x ?? 0) + parentNodePosition.x, y: (result.y ?? 0) + parentNodePosition.y, @@ -43,6 +41,41 @@ function calculateXYZPosition( ); } +export function updateAbsoluteNodePositions( + nodeInternals: NodeInternals, + nodeOrigin: NodeOrigin, + parentNodes?: ParentNodes +) { + nodeInternals.forEach((node) => { + if (node.parentNode && !nodeInternals.has(node.parentNode)) { + throw new Error(`Parent node ${node.parentNode} not found`); + } + + if (node.parentNode || parentNodes?.[node.id]) { + const { x, y, z } = calculateXYZPosition( + node, + nodeInternals, + { + ...node.position, + z: node[internalsSymbol]?.z ?? 0, + }, + nodeOrigin + ); + + node.positionAbsolute = { + x, + y, + }; + + node[internalsSymbol]!.z = z; + + if (parentNodes?.[node.id]) { + node[internalsSymbol]!.isParent = true; + } + } + }); +} + export function createNodeInternals( nodes: Node[], nodeInternals: NodeInternals, @@ -83,35 +116,7 @@ export function createNodeInternals( nextNodeInternals.set(node.id, internals); }); - nextNodeInternals.forEach((node) => { - if (node.parentNode && !nextNodeInternals.has(node.parentNode)) { - throw new Error(`Parent node ${node.parentNode} not found`); - } - - if (node.parentNode || parentNodes[node.id]) { - const { x, y, z } = calculateXYZPosition( - node, - nextNodeInternals, - parentNodes, - { - ...node.position, - z: node[internalsSymbol]?.z ?? 0, - }, - nodeOrigin - ); - - node.positionAbsolute = { - x, - y, - }; - - node[internalsSymbol]!.z = z; - - if (parentNodes[node.id]) { - node[internalsSymbol]!.isParent = true; - } - } - }); + updateAbsoluteNodePositions(nextNodeInternals, nodeOrigin, parentNodes); return nextNodeInternals; } @@ -142,6 +147,7 @@ export function fitView(get: StoreApi['getState'], options: Inte if (nodes.length > 0 && nodesInitialized) { const bounds = getRectOfNodes(nodes, nodeOrigin); + const [x, y, zoom] = getTransformForBounds( bounds, width,