Merge pull request #5219 from xyflow/feat/provider-init-zoom

feat(provider): add minZoom, maxZoom and fitViewOptions
This commit is contained in:
Moritz Klack
2025-04-15 13:23:53 +02:00
committed by GitHub
6 changed files with 95 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': minor
---
Add initialMinZoom, initialMaxZoom and initialFitViewOptions to ReactFlowProvider
@@ -3,18 +3,49 @@ import { useState, type ReactNode } from 'react';
import { Provider } from '../../contexts/StoreContext'; import { Provider } from '../../contexts/StoreContext';
import { createStore } from '../../store'; import { createStore } from '../../store';
import { BatchProvider } from '../BatchProvider'; import { BatchProvider } from '../BatchProvider';
import type { Node, Edge } from '../../types'; import type { Node, Edge, FitViewOptions } from '../../types';
import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; import { CoordinateExtent, NodeOrigin } from '@xyflow/system';
export type ReactFlowProviderProps = { export type ReactFlowProviderProps = {
/** These nodes are used to initialize the flow. They are not dynamic. */
initialNodes?: Node[]; initialNodes?: Node[];
/** These edges are used to initialize the flow. They are not dynamic. */
initialEdges?: Edge[]; initialEdges?: Edge[];
/** These nodes are used to initialize the flow. They are not dynamic. */
defaultNodes?: Node[]; defaultNodes?: Node[];
/** These edges are used to initialize the flow. They are not dynamic. */
defaultEdges?: Edge[]; defaultEdges?: Edge[];
/** The initial width is necessary to be able to use fitView on the server */
initialWidth?: number; initialWidth?: number;
/** The initial height is necessary to be able to use fitView on the server */
initialHeight?: number; initialHeight?: number;
/** When `true`, the flow will be zoomed and panned to fit all the nodes initially provided. */
fitView?: boolean; 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; 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; nodeExtent?: CoordinateExtent;
children: ReactNode; children: ReactNode;
}; };
@@ -60,6 +91,9 @@ export function ReactFlowProvider({
defaultEdges, defaultEdges,
initialWidth: width, initialWidth: width,
initialHeight: height, initialHeight: height,
initialMinZoom: minZoom,
initialMaxZoom: maxZoom,
initialFitViewOptions: fitViewOptions,
fitView, fitView,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
@@ -74,6 +108,9 @@ export function ReactFlowProvider({
width, width,
height, height,
fitView, fitView,
minZoom,
maxZoom,
fitViewOptions,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
}) })
@@ -2,7 +2,7 @@ import { useContext, type ReactNode } from 'react';
import StoreContext from '../../contexts/StoreContext'; import StoreContext from '../../contexts/StoreContext';
import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import { ReactFlowProvider } from '../../components/ReactFlowProvider';
import type { Node, Edge } from '../../types'; import type { Node, Edge, FitViewOptions } from '../../types';
import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; import { CoordinateExtent, NodeOrigin } from '@xyflow/system';
export function Wrapper({ export function Wrapper({
@@ -14,6 +14,9 @@ export function Wrapper({
width, width,
height, height,
fitView, fitView,
fitViewOptions,
minZoom,
maxZoom,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
}: { }: {
@@ -25,6 +28,9 @@ export function Wrapper({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
fitViewOptions?: FitViewOptions;
minZoom?: number;
maxZoom?: number;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
}) { }) {
@@ -47,6 +53,9 @@ export function Wrapper({
initialWidth={width} initialWidth={width}
initialHeight={height} initialHeight={height}
fitView={fitView} fitView={fitView}
initialFitViewOptions={fitViewOptions}
initialMinZoom={minZoom}
initialMaxZoom={maxZoom}
nodeOrigin={nodeOrigin} nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
> >
@@ -177,6 +177,9 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
width={width} width={width}
height={height} height={height}
fitView={fitView} fitView={fitView}
fitViewOptions={fitViewOptions}
minZoom={minZoom}
maxZoom={maxZoom}
nodeOrigin={nodeOrigin} nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
> >
+21 -2
View File
@@ -18,7 +18,7 @@ import {
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState'; import getInitialState from './initialState';
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams } from '../types'; import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions } from '../types';
const createStore = ({ const createStore = ({
nodes, nodes,
@@ -28,6 +28,9 @@ const createStore = ({
width, width,
height, height,
fitView, fitView,
fitViewOptions,
minZoom,
maxZoom,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
}: { }: {
@@ -38,6 +41,9 @@ const createStore = ({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
fitViewOptions?: FitViewOptions;
minZoom?: number;
maxZoom?: number;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
}) => }) =>
@@ -70,7 +76,20 @@ const createStore = ({
} }
return { 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[]) => { setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get(); const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get();
/* /*
+18 -5
View File
@@ -12,7 +12,7 @@ import {
CoordinateExtent, CoordinateExtent,
} from '@xyflow/system'; } from '@xyflow/system';
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types';
const getInitialState = ({ const getInitialState = ({
nodes, nodes,
@@ -22,6 +22,9 @@ const getInitialState = ({
width, width,
height, height,
fitView, fitView,
fitViewOptions,
minZoom = 0.5,
maxZoom = 2,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
}: { }: {
@@ -32,6 +35,9 @@ const getInitialState = ({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
fitViewOptions?: FitViewOptions;
minZoom?: number;
maxZoom?: number;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
} = {}): ReactFlowStore => { } = {}): ReactFlowStore => {
@@ -59,7 +65,14 @@ const getInitialState = ({
filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)), 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]; transform = [x, y, zoom];
} }
@@ -80,8 +93,8 @@ const getInitialState = ({
hasDefaultNodes: defaultNodes !== undefined, hasDefaultNodes: defaultNodes !== undefined,
hasDefaultEdges: defaultEdges !== undefined, hasDefaultEdges: defaultEdges !== undefined,
panZoom: null, panZoom: null,
minZoom: 0.5, minZoom,
maxZoom: 2, maxZoom,
translateExtent: infiniteExtent, translateExtent: infiniteExtent,
nodeExtent: storeNodeExtent, nodeExtent: storeNodeExtent,
nodesSelectionActive: false, nodesSelectionActive: false,
@@ -110,7 +123,7 @@ const getInitialState = ({
multiSelectionActive: false, multiSelectionActive: false,
fitViewQueued: fitView ?? false, fitViewQueued: fitView ?? false,
fitViewOptions: undefined, fitViewOptions,
fitViewResolver: null, fitViewResolver: null,
connection: { ...initialConnection }, connection: { ...initialConnection },