diff --git a/.changeset/happy-lobsters-speak.md b/.changeset/happy-lobsters-speak.md new file mode 100644 index 00000000..59b81980 --- /dev/null +++ b/.changeset/happy-lobsters-speak.md @@ -0,0 +1,5 @@ +--- +'@xyflow/react': minor +--- + +Add initialMinZoom, initialMaxZoom and initialFitViewOptions to ReactFlowProvider diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 1492ca3f..11a9c6b8 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -3,18 +3,49 @@ import { useState, type ReactNode } from 'react'; import { Provider } from '../../contexts/StoreContext'; import { createStore } from '../../store'; import { BatchProvider } from '../BatchProvider'; -import type { Node, Edge } from '../../types'; +import type { Node, Edge, FitViewOptions } from '../../types'; import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; export type ReactFlowProviderProps = { + /** These nodes are used to initialize the flow. They are not dynamic. */ initialNodes?: Node[]; + /** These edges are used to initialize the flow. They are not dynamic. */ initialEdges?: Edge[]; + /** These nodes are used to initialize the flow. They are not dynamic. */ defaultNodes?: Node[]; + /** These edges are used to initialize the flow. They are not dynamic. */ defaultEdges?: Edge[]; + /** The initial width is necessary to be able to use fitView on the server */ initialWidth?: number; + /** The initial height is necessary to be able to use fitView on the server */ initialHeight?: number; + /** When `true`, the flow will be zoomed and panned to fit all the nodes initially provided. */ fitView?: boolean; + /** + * You can provide an object of options to customize the initial fitView behavior. + */ + initialFitViewOptions?: FitViewOptions; + /** Initial minimum zoom level */ + initialMinZoom?: number; + /** Initial maximum zoom level */ + initialMaxZoom?: number; + /** + * The origin of the node to use when placing it in the flow or looking up its `x` and `y` + * position. An origin of `[0, 0]` means that a node's top left corner will be placed at the `x` + * and `y` position. + * @default [0, 0] + * @example + * [0, 0] // default, top left + * [0.5, 0.5] // center + * [1, 1] // bottom right + */ nodeOrigin?: NodeOrigin; + /** + * By default, nodes can be placed on an infinite flow. You can use this prop to set a boundary. + * + * The first pair of coordinates is the top left boundary and the second pair is the bottom right. + * @example [[-1000, -10000], [1000, 1000]] + */ nodeExtent?: CoordinateExtent; children: ReactNode; }; @@ -60,6 +91,9 @@ export function ReactFlowProvider({ defaultEdges, initialWidth: width, initialHeight: height, + initialMinZoom: minZoom, + initialMaxZoom: maxZoom, + initialFitViewOptions: fitViewOptions, fitView, nodeOrigin, nodeExtent, @@ -74,6 +108,9 @@ export function ReactFlowProvider({ width, height, fitView, + minZoom, + maxZoom, + fitViewOptions, nodeOrigin, nodeExtent, }) diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index 71a84316..9ae0aa62 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -2,7 +2,7 @@ import { useContext, type ReactNode } from 'react'; import StoreContext from '../../contexts/StoreContext'; import { ReactFlowProvider } from '../../components/ReactFlowProvider'; -import type { Node, Edge } from '../../types'; +import type { Node, Edge, FitViewOptions } from '../../types'; import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; export function Wrapper({ @@ -14,6 +14,9 @@ export function Wrapper({ width, height, fitView, + fitViewOptions, + minZoom, + maxZoom, nodeOrigin, nodeExtent, }: { @@ -25,6 +28,9 @@ export function Wrapper({ width?: number; height?: number; fitView?: boolean; + fitViewOptions?: FitViewOptions; + minZoom?: number; + maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; }) { @@ -47,6 +53,9 @@ export function Wrapper({ initialWidth={width} initialHeight={height} fitView={fitView} + initialFitViewOptions={fitViewOptions} + initialMinZoom={minZoom} + initialMaxZoom={maxZoom} nodeOrigin={nodeOrigin} nodeExtent={nodeExtent} > diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index 31389887..2247a004 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -177,6 +177,9 @@ function ReactFlow( width={width} height={height} fitView={fitView} + fitViewOptions={fitViewOptions} + minZoom={minZoom} + maxZoom={maxZoom} nodeOrigin={nodeOrigin} nodeExtent={nodeExtent} > diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 7682dc90..5588718c 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -18,7 +18,7 @@ import { import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import getInitialState from './initialState'; -import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams } from '../types'; +import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions } from '../types'; const createStore = ({ nodes, @@ -28,6 +28,9 @@ const createStore = ({ width, height, fitView, + fitViewOptions, + minZoom, + maxZoom, nodeOrigin, nodeExtent, }: { @@ -38,6 +41,9 @@ const createStore = ({ width?: number; height?: number; fitView?: boolean; + fitViewOptions?: FitViewOptions; + minZoom?: number; + maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; }) => @@ -70,7 +76,20 @@ const createStore = ({ } return { - ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), + ...getInitialState({ + nodes, + edges, + width, + height, + fitView, + fitViewOptions, + minZoom, + maxZoom, + nodeOrigin, + nodeExtent, + defaultNodes, + defaultEdges, + }), setNodes: (nodes: Node[]) => { const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get(); /* diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 996cc148..34507efd 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -12,7 +12,7 @@ import { CoordinateExtent, } from '@xyflow/system'; -import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; +import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types'; const getInitialState = ({ nodes, @@ -22,6 +22,9 @@ const getInitialState = ({ width, height, fitView, + fitViewOptions, + minZoom = 0.5, + maxZoom = 2, nodeOrigin, nodeExtent, }: { @@ -32,6 +35,9 @@ const getInitialState = ({ width?: number; height?: number; fitView?: boolean; + fitViewOptions?: FitViewOptions; + minZoom?: number; + maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; } = {}): ReactFlowStore => { @@ -59,7 +65,14 @@ const getInitialState = ({ filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)), }); - const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); + const { x, y, zoom } = getViewportForBounds( + bounds, + width, + height, + minZoom, + maxZoom, + fitViewOptions?.padding ?? 0.1 + ); transform = [x, y, zoom]; } @@ -80,8 +93,8 @@ const getInitialState = ({ hasDefaultNodes: defaultNodes !== undefined, hasDefaultEdges: defaultEdges !== undefined, panZoom: null, - minZoom: 0.5, - maxZoom: 2, + minZoom, + maxZoom, translateExtent: infiniteExtent, nodeExtent: storeNodeExtent, nodesSelectionActive: false, @@ -110,7 +123,7 @@ const getInitialState = ({ multiSelectionActive: false, fitViewQueued: fitView ?? false, - fitViewOptions: undefined, + fitViewOptions, fitViewResolver: null, connection: { ...initialConnection },