implemented new fitView logic

This commit is contained in:
peterkogo
2025-04-09 10:36:16 +02:00
parent 8b5284697c
commit 9383ba5554
132 changed files with 5506 additions and 2297 deletions
@@ -28,33 +28,49 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
const store = useStoreApi<NodeType, EdgeType>();
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState();
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued } = store.getState();
// This is essentially an `Array.reduce` in imperative clothing. Processing
// this queue is a relatively hot path so we'd like to avoid the overhead of
// array methods where we can.
let next = nodes as NodeType[];
/*
* This is essentially an `Array.reduce` in imperative clothing. Processing
* this queue is a relatively hot path so we'd like to avoid the overhead of
* array methods where we can.
*/
let next = nodes;
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;
}
if (hasDefaultNodes) {
setNodes(next);
} else if (onNodesChange) {
onNodesChange(
getElementsDiffChanges({
items: next,
lookup: nodeLookup,
}) as NodeChange<NodeType>[]
);
} else {
// When a controlled flow is used we need to collect the changes
const changes = getElementsDiffChanges({
items: next,
lookup: nodeLookup,
}) as NodeChange<NodeType>[];
// We only want to fire onNodesChange if there are changes to the nodes
if (changes.length > 0) {
onNodesChange?.(changes);
} else if (fitViewQueued) {
// If there are no changes to the nodes, we still need to call setNodes
// to trigger a re-render and fitView.
window.requestAnimationFrame(() => {
const { fitViewQueued, nodes, setNodes } = store.getState();
if (fitViewQueued) {
setNodes(nodes);
}
});
}
}
}, []);
const nodeQueue = useQueue<NodeType>(nodeQueueHandler);
const edgeQueueHandler = useCallback((queueItems: QueueItem<EdgeType>[]) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState();
let next = edges as EdgeType[];
let next = edges;
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;
}
@@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types';
* @returns a Queue object
*/
export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
// Because we're using a ref above, we need some way to let React know when to
// actually process the queue. We increment this number any time we mutate the
// queue, creating a new state to trigger the layout effect below.
// Using a boolean dirty flag here instead would lead to issues related to
// automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
/*
* Because we're using a ref above, we need some way to let React know when to
* actually process the queue. We increment this number any time we mutate the
* queue, creating a new state to trigger the layout effect below.
* Using a boolean dirty flag here instead would lead to issues related to
* automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
*/
const [serial, setSerial] = useState(BigInt(0));
// A reference of all the batched updates to process before the next render. We
// want a reference here so multiple synchronous calls to `setNodes` etc can be
// batched together.
/*
* A reference of all the batched updates to process before the next render. We
* want a reference here so multiple synchronous calls to `setNodes` etc can be
* batched together.
*/
const [queue] = useState(() => createQueue<T>(() => setSerial(n => n + BigInt(1))));
// Layout effects are guaranteed to run before the next render which means we
// shouldn't run into any issues with stale state or weird issues that come from
// rendering things one frame later than expected (we used to use `setTimeout`).
/*
* Layout effects are guaranteed to run before the next render which means we
* shouldn't run into any issues with stale state or weird issues that come from
* rendering things one frame later than expected (we used to use `setTimeout`).
*/
useIsomorphicLayoutEffect(() => {
const queueItems = queue.get();
@@ -11,12 +11,12 @@ import {
import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState } from '../../types';
import type { ConnectionLineComponent, Node, ReactFlowState } from '../../types';
import { useConnection } from '../../hooks/useConnection';
type ConnectionLineWrapperProps = {
type ConnectionLineWrapperProps<NodeType extends Node = Node> = {
type: ConnectionLineType;
component?: ConnectionLineComponent;
component?: ConnectionLineComponent<NodeType>;
containerStyle?: CSSProperties;
style?: CSSProperties;
};
@@ -29,7 +29,12 @@ const selector = (s: ReactFlowState) => ({
height: s.height,
});
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
export function ConnectionLineWrapper<NodeType extends Node = Node>({
containerStyle,
style,
type,
component,
}: ConnectionLineWrapperProps<NodeType>) {
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const renderConnection = !!(width && nodesConnectable && inProgress);
@@ -45,21 +50,27 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
className="react-flow__connectionline react-flow__container"
>
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
<ConnectionLine<NodeType> style={style} type={type} CustomComponent={component} isValid={isValid} />
</g>
</svg>
);
}
type ConnectionLineProps = {
type ConnectionLineProps<NodeType extends Node = Node> = {
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
CustomComponent?: ConnectionLineComponent<NodeType>;
isValid: boolean | null;
};
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useConnection();
const ConnectionLine = <NodeType extends Node = Node>({
style,
type = ConnectionLineType.Bezier,
CustomComponent,
isValid,
}: ConnectionLineProps<NodeType>) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } =
useConnection<NodeType>();
if (!inProgress) {
return;
@@ -6,7 +6,52 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer');
export function EdgeLabelRenderer({ children }: { children: ReactNode }) {
export type EdgeLabelRendererProps = {
children: ReactNode
}
/**
* Edges are SVG-based. If you want to render more complex labels you can use the
* `<EdgeLabelRenderer />` component to access a div based renderer. This component
* is a portal that renders the label in a `<div />` that is positioned on top of
* the edges. You can see an example usage of the component in the
* [edge label renderer example](/examples/edges/edge-label-renderer).
* @public
*
* @example
* ```jsx
* import React from 'react';
* import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react';
*
* export function CustomEdge({ id, data, ...props }) {
* const [edgePath, labelX, labelY] = getBezierPath(props);
*
* return (
* <>
* <BaseEdge id={id} path={edgePath} />
* <EdgeLabelRenderer>
* <div
* style={{
* position: 'absolute',
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
* background: '#ffcc00',
* padding: 10,
* }}
* className="nodrag nopan"
* >
* {data.label}
* </div>
* </EdgeLabelRenderer>
* </>
* );
* };
* ```
*
* @remarks The `<EdgeLabelRenderer />` has no pointer events by default. If you want to
* add mouse interactions you need to set the style `pointerEvents: all` and add
* the `nopan` class on the label or the element you want to interact with.
*/
export function EdgeLabelRenderer({ children }: EdgeLabelRendererProps) {
const edgeLabelRenderer = useStore(selector);
if (!edgeLabelRenderer) {
@@ -1,4 +1,4 @@
import { useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
const onEdgeDoubleClick = onDoubleClick
? (event: React.MouseEvent) => {
onDoubleClick(event, { ...edge });
}
onDoubleClick(event, { ...edge });
}
: undefined;
const onEdgeContextMenu = onContextMenu
? (event: React.MouseEvent) => {
onContextMenu(event, { ...edge });
}
onContextMenu(event, { ...edge });
}
: undefined;
const onEdgeMouseEnter = onMouseEnter
? (event: React.MouseEvent) => {
onMouseEnter(event, { ...edge });
}
onMouseEnter(event, { ...edge });
}
: undefined;
const onEdgeMouseMove = onMouseMove
? (event: React.MouseEvent) => {
onMouseMove(event, { ...edge });
}
onMouseMove(event, { ...edge });
}
: undefined;
const onEdgeMouseLeave = onMouseLeave
? (event: React.MouseEvent) => {
onMouseLeave(event, { ...edge });
}
onMouseLeave(event, { ...edge });
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {
@@ -4,6 +4,33 @@ import cc from 'classcat';
import { EdgeText } from './EdgeText';
import type { BaseEdgeProps } from '../../types';
/**
* The `<BaseEdge />` component gets used internally for all the edges. It can be
* used inside a custom edge and handles the invisible helper edge and the edge label
* for you.
*
* @public
* @example
* ```jsx
*import { BaseEdge } from '@xyflow/react';
*
*export function CustomEdge({ sourceX, sourceY, targetX, targetY, ...props }) {
* const [edgePath] = getStraightPath({
* sourceX,
* sourceY,
* targetX,
* targetY,
* });
*
* return <BaseEdge path={edgePath} {...props} />;
*}
*```
*
* @remarks If you want to use an edge marker with the [`<BaseEdge />`](/api-reference/components/base-edge) component,
* you can pass the `markerStart` or `markerEnd` props passed to your custom edge
* through to the [`<BaseEdge />`](/api-reference/components/base-edge) component.
* You can see all the props passed to a custom edge by looking at the [`EdgeProps`](/api-reference/types/edge-props) type.
*/
export function BaseEdge({
path,
labelX,
@@ -61,7 +61,34 @@ function createBezierEdge(params: { isInternal: boolean }) {
);
}
/**
* Component that can be used inside a custom edge to render a bezier curve.
*
* @public
* @example
*
* ```tsx
* import { BezierEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <BezierEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const BezierEdge = createBezierEdge({ isInternal: false });
/**
* @internal
*/
const BezierEdgeInternal = createBezierEdge({ isInternal: true });
BezierEdge.displayName = 'BezierEdge';
@@ -27,6 +27,9 @@ export interface EdgeAnchorProps extends SVGAttributes<SVGGElement> {
const EdgeUpdaterClassName = 'react-flow__edgeupdater';
/**
* @internal
*/
export function EdgeAnchor({
position,
centerX,
@@ -8,9 +8,9 @@ function EdgeTextComponent({
x,
y,
label,
labelStyle = {},
labelStyle,
labelShowBg = true,
labelBgStyle = {},
labelBgStyle,
labelBgPadding = [2, 4],
labelBgBorderRadius = 2,
children,
@@ -34,7 +34,7 @@ function EdgeTextComponent({
}
}, [label]);
if (typeof label === 'undefined' || !label) {
if (!label) {
return null;
}
@@ -73,4 +73,30 @@ function EdgeTextComponent({
EdgeTextComponent.displayName = 'EdgeText';
/**
* You can use the `<EdgeText />` component as a helper component to display text
* within your custom edges.
*
* @public
*
* @example
* ```jsx
* import { EdgeText } from '@xyflow/react';
*
* export function CustomEdgeLabel({ label }) {
* return (
* <EdgeText
* x={100}
* y={100}
* label={label}
* labelStyle={{ fill: 'white' }}
* labelShowBg
* labelBgStyle={{ fill: 'red' }}
* labelBgPadding={[2, 4]}
* labelBgBorderRadius={2}
* />
* );
* }
*```
*/
export const EdgeText = memo(EdgeTextComponent);
@@ -7,9 +7,11 @@ import type { SimpleBezierEdgeProps } from '../../types';
export interface GetSimpleBezierPathParams {
sourceX: number;
sourceY: number;
/** @default Position.Bottom */
sourcePosition?: Position;
targetX: number;
targetY: number;
/** @default Position.Top */
targetPosition?: Position;
}
@@ -29,6 +31,19 @@ function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number]
return [x1, 0.5 * (y1 + y2)];
}
/**
* The `getSimpleBezierPath` util returns everything you need to render a simple
* bezier edge between two nodes.
* @public
* @returns
* - `path`: the path to use in an SVG `<path>` element.
* - `labelX`: the `x` position you can use to render a label for this edge.
* - `labelY`: the `y` position you can use to render a label for this edge.
* - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
* middle of this path.
* - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
* middle of this path.
*/
export function getSimpleBezierPath({
sourceX,
sourceY,
@@ -80,8 +95,8 @@ function createSimpleBezierEdge(params: { isInternal: boolean }) {
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
sourcePosition,
targetPosition,
label,
labelStyle,
labelShowBg,
@@ -62,7 +62,34 @@ function createSmoothStepEdge(params: { isInternal: boolean }) {
);
}
/**
* Component that can be used inside a custom edge to render a smooth step edge.
*
* @public
* @example
*
* ```tsx
* import { SmoothStepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <SmoothStepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
/**
* @internal
*/
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
SmoothStepEdge.displayName = 'SmoothStepEdge';
@@ -21,7 +21,34 @@ function createStepEdge(params: { isInternal: boolean }) {
});
}
/**
* Component that can be used inside a custom edge to render a step edge.
*
* @public
* @example
*
* ```tsx
* import { StepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <StepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const StepEdge = createStepEdge({ isInternal: false });
/**
* @internal
*/
const StepEdgeInternal = createStepEdge({ isInternal: true });
StepEdge.displayName = 'StepEdge';
@@ -50,7 +50,32 @@ function createStraightEdge(params: { isInternal: boolean }) {
);
}
/**
* Component that can be used inside a custom edge to render a straight line.
*
* @public
* @example
*
* ```tsx
* import { StraightEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY }) {
* return (
* <StraightEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* />
* );
* }
* ```
*/
const StraightEdge = createStraightEdge({ isInternal: false });
/**
* @internal
*/
const StraightEdgeInternal = createStraightEdge({ isInternal: true });
StraightEdge.displayName = 'StraightEdge';
+5 -3
View File
@@ -1,6 +1,8 @@
// We distinguish between internal and exported edges
// The internal edges are used directly like custom edges and always get an id, source and target props
// If you import an edge from the library, the id is optional and source and target are not used at all
/*
* We distinguish between internal and exported edges
* The internal edges are used directly like custom edges and always get an id, source and target props
* If you import an edge from the library, the id is optional and source and target are not used at all
*/
export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge';
export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge';
+46 -15
View File
@@ -28,10 +28,14 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import { type ReactFlowState } from '../../types';
import { fixedForwardRef } from '../../utils';
export interface HandleProps extends HandlePropsSystem, Omit<HTMLAttributes<HTMLDivElement>, 'id'> {
/** Callback called when connection is made */
onConnect?: OnConnect;
}
/**
* @expand
*/
export type HandleProps = HandlePropsSystem &
Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
/** Callback called when connection is made */
onConnect?: OnConnect;
};
const selector = (s: ReactFlowState) => ({
connectOnClick: s.connectOnClick,
@@ -42,9 +46,7 @@ const selector = (s: ReactFlowState) => ({
const connectingSelector =
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state;
const { fromHandle, toHandle, isValid } = connection;
const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type;
return {
@@ -56,6 +58,7 @@ const connectingSelector =
? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
connectionInProcess: !!fromHandle,
clickConnectionInProcess: !!clickHandle,
valid: connectingTo && isValid,
};
};
@@ -83,11 +86,15 @@ function HandleComponent(
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
connectingSelector(nodeId, handleId, type),
shallow
);
const {
connectingFrom,
connectingTo,
clickConnecting,
isPossibleEndHandle,
connectionInProcess,
clickConnectionInProcess,
valid,
} = useStore(connectingSelector(nodeId, handleId, type), shallow);
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
}
@@ -228,12 +235,14 @@ function HandleComponent(
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
/*
* shows where you can start a connection from
* and where you can end it while connecting
*/
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
(connectionInProcess || clickConnectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
@@ -248,6 +257,28 @@ function HandleComponent(
}
/**
* The Handle component is a UI element that is used to connect nodes.
* The `<Handle />` component is used in your [custom nodes](/learn/customization/custom-nodes)
* to define connection points.
*
*@public
*
*@example
*
*```jsx
*import { Handle, Position } from '@xyflow/react';
*
*export function CustomNode({ data }) {
* return (
* <>
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*```
*/
export const Handle = memo(fixedForwardRef(HandleComponent));
@@ -37,7 +37,6 @@ export function NodeWrapper<NodeType extends Node>({
disableKeyboardA11y,
rfId,
nodeTypes,
nodeExtent,
nodeClickDistance,
onError,
}: NodeWrapperProps<NodeType>) {
@@ -109,8 +108,10 @@ export function NodeWrapper<NodeType extends Node>({
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
/*
* this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
* here we only need to call it when selectNodesOnDrag=false
*/
handleNodeClick({
id,
store,
@@ -49,8 +49,10 @@ export function useNodeObserver({
useEffect(() => {
if (nodeRef.current) {
// when the user programmatically changes the source or handle position, we need to update the internals
// to make sure the edges are updated correctly
/*
* when the user programmatically changes the source or handle position, we need to update the internals
* to make sure the edges are updated correctly
*/
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
@@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = {
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: InternalNode<NodeType>
): {
width: number | string | undefined;
height: number | string | undefined;
} {
width: number | string | undefined;
height: number | string | undefined;
} {
if (node.internals.handleBounds === undefined) {
return {
width: node.width ?? node.initialWidth ?? node.style?.width,
+6 -4
View File
@@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system';
import type { ReactFlowState } from '../../types';
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
/*
* this handler is called by
* 1. the click handler when node is not draggable or selectNodesOnDrag = false
* or
* 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
*/
export function handleNodeClick({
id,
store,
@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {
+34 -4
View File
@@ -1,20 +1,48 @@
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import { HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import type { PanelPosition } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
/**
* @expand
*/
export type PanelProps = HTMLAttributes<HTMLDivElement> & {
/** Set position of the panel
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
/**
* The position of the panel.
* @default "top-left"
*/
position?: PanelPosition;
children: ReactNode;
};
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
/**
* The `<Panel />` component helps you position content above the viewport.
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
* and [`<Controls />`](/api-reference/components/controls) components.
*
* @public
*
* @example
* ```jsx
*import { ReactFlow, Background, Panel } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[]} fitView>
* <Panel position="top-left">top-left</Panel>
* <Panel position="top-center">top-center</Panel>
* <Panel position="top-right">top-right</Panel>
* <Panel position="bottom-left">bottom-left</Panel>
* <Panel position="bottom-center">bottom-center</Panel>
* <Panel position="bottom-right">bottom-right</Panel>
* </ReactFlow>
* );
*}
*```
*/
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
({ position = 'top-left', children, className, style, ...rest }, ref) => {
const pointerEvents = useStore(selector);
@@ -32,3 +60,5 @@ export const Panel = forwardRef<HTMLDivElement, PanelProps>(
);
}
);
Panel.displayName = 'Panel';
@@ -19,6 +19,40 @@ export type ReactFlowProviderProps = {
children: ReactNode;
};
/**
* The `<ReactFlowProvider />` component is a [context provider](https://react.dev/learn/passing-data-deeply-with-context#)
* that makes it possible to access a flow's internal state outside of the
* [`<ReactFlow />`](/api-reference/react-flow) component. Many of the hooks we
* provide rely on this component to work.
* @public
*
* @example
* ```tsx
*import { ReactFlow, ReactFlowProvider, useNodes } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlowProvider>
* <ReactFlow nodes={...} edges={...} />
* <Sidebar />
* </ReactFlowProvider>
* );
*}
*
*function Sidebar() {
* // This hook will only work if the component it's used in is a child of a
* // <ReactFlowProvider />.
* const nodes = useNodes()
*
* return <aside>do something with nodes</aside>;
*}
*```
*
* @remarks If you're using a router and want your flow's state to persist across routes,
* it's vital that you place the `<ReactFlowProvider />` component _outside_ of
* your router. If you have multiple flows on the same page you will need to use a separate
* `<ReactFlowProvider />` for each flow.
*/
export function ReactFlowProvider({
initialNodes: nodes,
initialEdges: edges,
@@ -10,8 +10,8 @@ import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore';
import type { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
type SelectionListenerProps = {
onSelectionChange?: OnSelectionChangeFunc;
type SelectionListenerProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
onSelectionChange?: OnSelectionChangeFunc<NodeType, EdgeType>;
};
const selector = (s: ReactFlowState) => {
@@ -44,12 +44,14 @@ function areEqual(a: SelectorSlice, b: SelectorSlice) {
);
}
function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) {
const store = useStoreApi();
function SelectionListenerInner<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
onSelectionChange,
}: SelectionListenerProps<NodeType, EdgeType>) {
const store = useStoreApi<NodeType, EdgeType>();
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
useEffect(() => {
const params = { nodes: selectedNodes, edges: selectedEdges };
const params = { nodes: selectedNodes as NodeType[], edges: selectedEdges as EdgeType[] };
onSelectionChange?.(params);
store.getState().onSelectionChangeHandlers.forEach((fn) => fn(params));
@@ -60,11 +62,13 @@ function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) {
const changeSelector = (s: ReactFlowState) => !!s.onSelectionChangeHandlers;
export function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
export function SelectionListener<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
onSelectionChange,
}: SelectionListenerProps<NodeType, EdgeType>) {
const storeHasSelectionChangeHandlers = useStore(changeSelector);
if (onSelectionChange || storeHasSelectionChangeHandlers) {
return <SelectionListenerInner onSelectionChange={onSelectionChange} />;
return <SelectionListenerInner<NodeType, EdgeType> onSelectionChange={onSelectionChange} />;
}
return null;
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
import { defaultNodeOrigin } from '../../container/ReactFlow/init-values';
// these fields exist in the global store and we need to keep them up to date
// These fields exist in the global store, and we need to keep them up to date
const reactFlowFieldsToTrack = [
'nodes',
'edges',
@@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({
});
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.
/*
* 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,
@@ -152,8 +154,8 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
// Renamed fields
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
// General case
else store.setState({ [fieldName]: fieldValue });
}
@@ -6,6 +6,31 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal');
/**
* The `<ViewportPortal />` component can be used to add components to the same viewport
* of the flow where nodes and edges are rendered. This is useful when you want to render
* your own components that are adhere to the same coordinate system as the nodes & edges
* and are also affected by zooming and panning
* @public
* @example
*
* ```jsx
*import React from 'react';
*import { ViewportPortal } from '@xyflow/react';
*
*export default function () {
* return (
* <ViewportPortal>
* <div
* style={{ transform: 'translate(100px, 100px)', position: 'absolute' }}
* >
* This div is positioned at [100, 100] on the flow.
* </div>
* </ViewportPortal>
* );
*}
*```
*/
export function ViewportPortal({ children }: { children: ReactNode }) {
const viewPortalDiv = useStore(selector);