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
@@ -90,4 +90,57 @@ function BackgroundComponent({
BackgroundComponent.displayName = 'Background';
/**
* The `<Background />` component makes it convenient to render different types of backgrounds common in node-based UIs. It comes with three variants: lines, dots and cross.
*
* @example
*
* A simple example of how to use the Background component.
*
* ```tsx
* import { useState } from 'react';
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background color="#ccc" variant={BackgroundVariant.Dots} />
* </ReactFlow>
* );
* }
* ```
*
* @example
*
* In this example you can see how to combine multiple backgrounds
*
* ```tsx
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
* import '@xyflow/react/dist/style.css';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background
* id="1"
* gap={10}
* color="#f1f1f1"
* variant={BackgroundVariant.Lines}
* />
* <Background
* id="2"
* gap={100}
* color="#ccc"
* variant={BackgroundVariant.Lines}
* />
* </ReactFlow>
* );
* }
* ```
*
* @remarks
*
* When combining multiple <Background /> components its important to give each of them a unique id prop!
*
*/
export const Background = memo(BackgroundComponent);
@@ -1,34 +1,60 @@
import { CSSProperties } from 'react';
/**
* The three variants are exported as an enum for convenience. You can either import
* the enum and use it like `BackgroundVariant.Lines` or you can use the raw string
* value directly.
* @public
*/
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots',
Cross = 'cross',
}
/**
* @expand
*/
export type BackgroundProps = {
/** When multiple backgrounds are present on the page, each one should have a unique id. */
id?: string;
/** Color of the pattern */
/** Color of the pattern. */
color?: string;
/** Color of the background */
/** Color of the background. */
bgColor?: string;
/** Class applied to the container */
/** Class applied to the container. */
className?: string;
/** Class applied to the pattern */
/** Class applied to the pattern. */
patternClassName?: string;
/** Gap between repetitions of the pattern */
/**
* The gap between patterns. Passing in a tuple allows you to control the x and y gap
* independently.
* @default 20
*/
gap?: number | [number, number];
/** Size of a single pattern element */
/**
* The radius of each dot or the size of each rectangle if `BackgroundVariant.Dots` or
* `BackgroundVariant.Cross` is used. This defaults to 1 or 6 respectively, or ignored if
* `BackgroundVariant.Lines` is used.
*/
size?: number;
/** Offset of the pattern */
/**
* Offset of the pattern.
* @default 0
*/
offset?: number | [number, number];
/** Line width of the Line pattern */
/**
* The stroke thickness used when drawing the pattern.
* @default 1
*/
lineWidth?: number;
/** Variant of the pattern
/**
* Variant of the pattern.
* @default BackgroundVariant.Dots
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
* 'lines', 'dots', 'cross'
*/
variant?: BackgroundVariant;
/** Style applied to the container */
/** Style applied to the container. */
style?: CSSProperties;
};
@@ -2,6 +2,29 @@ import cc from 'classcat';
import type { ControlButtonProps } from './types';
/**
* You can add buttons to the control panel by using the `<ControlButton />` component
* and pass it as a child to the [`<Controls />`](/api-reference/components/controls) component.
*
* @public
* @example
*```jsx
*import { MagicWand } from '@radix-ui/react-icons'
*import { ReactFlow, Controls, ControlButton } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls>
* <ControlButton onClick={() => alert('Something magical just happened. ✨')}>
* <MagicWand />
* </ControlButton>
* </Controls>
* </ReactFlow>
* )
*}
*```
*/
export function ControlButton({ children, className, ...rest }: ControlButtonProps) {
return (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>
@@ -125,4 +125,25 @@ function ControlsComponent({
ControlsComponent.displayName = 'Controls';
/**
* The `<Controls />` component renders a small panel that contains convenient
* buttons to zoom in, zoom out, fit the view, and lock the viewport.
*
* @public
* @example
*```tsx
*import { ReactFlow, Controls } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls />
* </ReactFlow>
* )
*}
*```
*
* @remarks To extend or customise the controls, you can use the [`<ControlButton />`](/api-reference/components/control-button) component
*
*/
export const Controls = memo(ControlsComponent);
@@ -3,24 +3,46 @@ import type { PanelPosition } from '@xyflow/system';
import type { FitViewOptions } from '../../types';
/**
* @expand
*/
export type ControlProps = {
/** Show button for zoom in/out */
/**
* Whether or not to show the zoom in and zoom out buttons. These buttons will adjust the viewport
* zoom by a fixed amount each press.
* @default true
*/
showZoom?: boolean;
/** Show button for fit view */
/**
* Whether or not to show the fit view button. By default, this button will adjust the viewport so
* that all nodes are visible at once.
* @default true
*/
showFitView?: boolean;
/** Show button for toggling interactivity */
/**
* Show button for toggling interactivity
* @default true
*/
showInteractive?: boolean;
/** Options being used when fit view button is clicked */
/**
* Customise the options for the fit view button. These are the same options you would pass to the
* fitView function.
*/
fitViewOptions?: FitViewOptions;
/** Callback when zoom in button is clicked */
/** Called in addition the default zoom behavior when the zoom in button is clicked. */
onZoomIn?: () => void;
/** Callback when zoom out button is clicked */
/** Called in addition the default zoom behavior when the zoom out button is clicked. */
onZoomOut?: () => void;
/** Callback when fit view button is clicked */
/**
* Called when the fit view button is clicked. When this is not provided, the viewport will be
* adjusted so that all nodes are visible.
*/
onFitView?: () => void;
/** Callback when interactivity is toggled */
/** Called when the interactive (lock) button is clicked. */
onInteractiveChange?: (interactiveStatus: boolean) => void;
/** Position of the controls on the pane
/**
* Position of the controls on the pane
* @default PanelPosition.BottomLeft
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/
@@ -28,10 +50,19 @@ export type ControlProps = {
children?: ReactNode;
/** Style applied to container */
style?: React.CSSProperties;
/** ClassName applied to container */
/** Class name applied to container */
className?: string;
/**
* @default 'React Flow controls'
*/
'aria-label'?: string;
/**
* @default 'vertical'
*/
orientation?: 'horizontal' | 'vertical';
};
/**
* @expand
*/
export type ControlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
@@ -15,6 +15,8 @@ import type { MiniMapProps } from './types';
const defaultWidth = 200;
const defaultHeight = 150;
const filterHidden = (node: Node) => !node.hidden;
const selector = (s: ReactFlowState) => {
const viewBB: Rect = {
x: -s.transform[0] / s.transform[2],
@@ -25,7 +27,10 @@ const selector = (s: ReactFlowState) => {
return {
viewBB,
boundingRect: s.nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup), viewBB) : viewBB,
boundingRect:
s.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { filter: filterHidden }), viewBB)
: viewBB,
rfId: s.rfId,
panZoom: s.panZoom,
translateExtent: s.translateExtent,
@@ -44,8 +49,10 @@ function MiniMapComponent<NodeType extends Node = Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent,
bgColor,
maskColor,
@@ -118,7 +125,7 @@ function MiniMapComponent<NodeType extends Node = Node>({
const onSvgNodeClick = onNodeClick
? useCallback((event: MouseEvent, nodeId: string) => {
const node = store.getState().nodeLookup.get(nodeId)!;
const node: NodeType = store.getState().nodeLookup.get(nodeId)!.internals.userNode;
onNodeClick(event, node);
}, [])
: undefined;
@@ -136,7 +143,7 @@ function MiniMapComponent<NodeType extends Node = Node>({
typeof maskStrokeWidth === 'number' ? maskStrokeWidth * viewScale : undefined,
'--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
'--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined,
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'number' ? nodeStrokeWidth : undefined,
} as CSSProperties
}
className={cc(['react-flow__minimap', className])}
@@ -176,4 +183,24 @@ function MiniMapComponent<NodeType extends Node = Node>({
MiniMapComponent.displayName = 'MiniMap';
/**
* The `<MiniMap />` component can be used to render an overview of your flow. It
* renders each node as an SVG element and visualizes where the current viewport is
* in relation to the rest of the flow.
*
* @public
* @example
*
* ```jsx
*import { ReactFlow, MiniMap } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]]} edges={[...]]}>
* <MiniMap nodeStrokeWidth={3} />
* </ReactFlow>
* );
*}
*```
*/
export const MiniMap = memo(MiniMapComponent) as typeof MiniMapComponent;
@@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore';
import { MiniMapNode } from './MiniMapNode';
import type { ReactFlowState, Node, InternalNode } from '../../types';
import type { ReactFlowState, Node } from '../../types';
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
declare const window: any;
@@ -21,8 +21,10 @@ function MiniMapNodes<NodeType extends Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent: NodeComponent = MiniMapNode,
onClick,
}: MiniMapNodesProps<NodeType>) {
@@ -36,11 +38,13 @@ function MiniMapNodes<NodeType extends Node>({
return (
<>
{nodeIds.map((nodeId) => (
// The split of responsibilities between MiniMapNodes and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
/*
* The split of responsibilities between MiniMapNodes and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
* For more details, see a similar commit in `NodeRenderer/index.tsx`.
*/
<NodeComponentWrapper<NodeType>
key={nodeId}
id={nodeId}
@@ -80,8 +84,9 @@ function NodeComponentWrapperInner<NodeType extends Node>({
shapeRendering: string;
}) {
const { node, x, y, width, height } = useStore((s) => {
const node = s.nodeLookup.get(id) as InternalNode<NodeType>;
const { x, y } = node.internals.positionAbsolute;
const { internals } = s.nodeLookup.get(id)!;
const node = internals.userNode as NodeType;
const { x, y } = internals.positionAbsolute;
const { width, height } = getNodeDimensions(node);
return {
@@ -6,47 +6,97 @@ import type { Node } from '../../types';
export type GetMiniMapNodeAttribute<NodeType extends Node = Node> = (node: NodeType) => string;
/**
* @expand
*/
export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & {
/** Color of nodes on minimap */
/**
* Color of nodes on minimap.
* @default "#e2e2e2"
*/
nodeColor?: string | GetMiniMapNodeAttribute<NodeType>;
/** Stroke color of nodes on minimap */
/**
* Stroke color of nodes on minimap.
* @default "transparent"
*/
nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeType>;
/** ClassName applied to nodes on minimap */
/**
* Class name applied to nodes on minimap.
* @default ""
*/
nodeClassName?: string | GetMiniMapNodeAttribute<NodeType>;
/** Border radius of nodes on minimap */
/**
* Border radius of nodes on minimap.
* @default 5
*/
nodeBorderRadius?: number;
/** Stroke width of nodes on minimap */
/**
* Stroke width of nodes on minimap.
* @default 2
*/
nodeStrokeWidth?: number;
/** Component used to render nodes on minimap */
/**
* A custom component to render the nodes in the minimap. This component must render an SVG
* element!
*/
nodeComponent?: ComponentType<MiniMapNodeProps>;
/** Background color of minimap */
/** Background color of minimap. */
bgColor?: string;
/** Color of mask representing viewport */
/**
* The color of the mask that covers the portion of the minimap not currently visible in the
* viewport.
* @default "rgba(240, 240, 240, 0.6)"
*/
maskColor?: string;
/** Stroke color of mask representing viewport */
/**
* Stroke color of mask representing viewport.
* @default transparent
*/
maskStrokeColor?: string;
/** Stroke width of mask representing viewport */
/**
* Stroke width of mask representing viewport.
* @default 1
*/
maskStrokeWidth?: number;
/** Position of minimap on pane
/**
* Position of minimap on pane.
* @default PanelPosition.BottomRight
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/
position?: PanelPosition;
/** Callback caled when minimap is clicked*/
/** Callback called when minimap is clicked. */
onClick?: (event: MouseEvent, position: XYPosition) => void;
/** Callback called when node on minimap is clicked */
/** Callback called when node on minimap is clicked. */
onNodeClick?: (event: MouseEvent, node: NodeType) => void;
/** If true, viewport is pannable via mini map component */
/**
* Determines whether you can pan the viewport by dragging inside the minimap.
* @default false
*/
pannable?: boolean;
/** If true, viewport is zoomable via mini map component */
/**
* Determines whether you can zoom the viewport by scrolling inside the minimap.
* @default false
*/
zoomable?: boolean;
/** The aria-label attribute */
/**
* There is no text inside the minimap for a screen reader to use as an accessible name, so it's
* important we provide one to make the minimap accessible. The default is sufficient, but you may
* want to replace it with something more relevant to your app or product.
* @default "React Flow mini map"
*/
ariaLabel?: string | null;
/** Invert direction when panning the minimap viewport */
/** Invert direction when panning the minimap viewport. */
inversePan?: boolean;
/** Step size for zooming in/out on minimap */
/**
* Step size for zooming in/out on minimap.
* @default 10
*/
zoomStep?: number;
/** Offset the viewport on the minmap, acts like a padding */
/**
* Offset the viewport on the minimap, acts like a padding.
* @default 5
*/
offsetScale?: number;
};
@@ -57,6 +107,12 @@ export type MiniMapNodes<NodeType extends Node = Node> = Pick<
onClick?: (event: MouseEvent, nodeId: string) => void;
};
/**
* The props that are passed to the MiniMapNode component
*
* @public
* @expand
*/
export type MiniMapNodeProps = {
id: string;
x: number;
@@ -74,8 +74,8 @@ function ResizeControl({
if (node && node.expandParent && node.parentId) {
const origin = node.origin ?? nodeOrigin;
const width = change.width ?? node.measured.width!;
const height = change.height ?? node.measured.height!;
const width = change.width ?? node.measured.width ?? 0;
const height = change.height ?? node.measured.height ?? 0;
const child: ParentExpandChild = {
id: node.id,
@@ -99,8 +99,10 @@ function ResizeControl({
const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges);
// when the parent was expanded by the child node, its position will be clamped at
// 0,0 when node origin is 0,0 and to width, height if it's 1,1
/*
* when the parent was expanded by the child node, its position will be clamped at
* 0,0 when node origin is 0,0 and to width, height if it's 1,1
*/
nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
}
@@ -140,11 +142,15 @@ function ResizeControl({
triggerNodeChanges(changes);
},
onEnd: () => {
onEnd: ({ width, height }) => {
const dimensionChange: NodeDimensionChange = {
id: id,
type: 'dimensions',
resizing: false,
dimensions: {
width,
height,
},
};
store.getState().triggerNodeChanges([dimensionChange]);
},
@@ -201,4 +207,9 @@ export function ResizeControlLine(props: ResizeControlLineProps) {
return <ResizeControl {...props} variant={ResizeControlVariant.Line} />;
}
/**
* To create your own resizing UI, you can use the `NodeResizeControl` component where you can pass children (such as icons).
* @public
*
*/
export const NodeResizeControl = memo(ResizeControl);
@@ -3,6 +3,30 @@ import { ResizeControlVariant, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSI
import { NodeResizeControl } from './NodeResizeControl';
import type { NodeResizerProps } from './types';
/**
* The `<NodeResizer />` component can be used to add a resize functionality to your
* nodes. It renders draggable controls around the node to resize in all directions.
* @public
*
* @example
*```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeResizer } from '@xyflow/react';
*
*function ResizableNode({ data }) {
* return (
* <>
* <NodeResizer minWidth={100} minHeight={30} />
* <Handle type="target" position={Position.Left} />
* <div style={{ padding: 10 }}>{data.label}</div>
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(ResizableNode);
*```
*/
export function NodeResizer({
nodeId,
isVisible = true,
@@ -9,43 +9,68 @@ import type {
OnResizeEnd,
} from '@xyflow/system';
/**
* @expand
*/
export type NodeResizerProps = {
/** Id of the node it is resizing
/**
* Id of the node it is resizing.
* @remarks optional if used inside custom node
*/
nodeId?: string;
/** Color of the resize handle */
/** Color of the resize handle. */
color?: string;
/** ClassName applied to handle */
/** Class name applied to handle. */
handleClassName?: string;
/** Style applied to handle */
/** Style applied to handle. */
handleStyle?: CSSProperties;
/** ClassName applied to line */
/** Class name applied to line. */
lineClassName?: string;
/** Style applied to line */
/** Style applied to line. */
lineStyle?: CSSProperties;
/** Are the controls visible */
/**
* Are the controls visible.
* @default true
*/
isVisible?: boolean;
/** Minimum width of node */
/**
* Minimum width of node.
* @default 10
*/
minWidth?: number;
/** Minimum height of node */
/**
* Minimum height of node.
* @default 10
*/
minHeight?: number;
/** Maximum width of node */
/**
* Maximum width of node.
* @default Number.MAX_VALUE
*/
maxWidth?: number;
/** Maximum height of node */
/**
* Maximum height of node.
* @default Number.MAX_VALUE
*/
maxHeight?: number;
/** Keep aspect ratio when resizing */
/**
* Keep aspect ratio when resizing.
* @default false
*/
keepAspectRatio?: boolean;
/** Callback to determine if node should resize */
/** Callback to determine if node should resize. */
shouldResize?: ShouldResize;
/** Callback called when resizing starts */
/** Callback called when resizing starts. */
onResizeStart?: OnResizeStart;
/** Callback called when resizing */
/** Callback called when resizing. */
onResize?: OnResize;
/** Callback called when resizing ends */
/** Callback called when resizing ends. */
onResizeEnd?: OnResizeEnd;
};
/**
* @expand
*/
export type ResizeControlProps = Pick<
NodeResizerProps,
| 'nodeId'
@@ -60,12 +85,15 @@ export type ResizeControlProps = Pick<
| 'onResize'
| 'onResizeEnd'
> & {
/** Position of the control
/**
* Position of the control.
* @example ControlPosition.TopLeft, ControlPosition.TopRight,
* ControlPosition.BottomLeft, ControlPosition.BottomRight
*/
position?: ControlPosition;
/** Variant of the control
/**
* Variant of the control.
* @default "handle"
* @example ResizeControlVariant.Handle, ResizeControlVariant.Line
*/
variant?: ResizeControlVariant;
@@ -74,6 +102,9 @@ export type ResizeControlProps = Pick<
children?: ReactNode;
};
/**
* @expand
*/
export type ResizeControlLineProps = ResizeControlProps & {
position?: ControlLinePosition;
};
@@ -38,6 +38,41 @@ const storeSelector = (state: ReactFlowState) => ({
selectedNodesCount: state.nodes.filter((node) => node.selected).length,
});
/**
* This component can render a toolbar or tooltip to one side of a custom node. This
* toolbar doesn't scale with the viewport so that the content is always visible.
*
* @public
* @example
* ```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeToolbar } from '@xyflow/react';
*
*function CustomNode({ data }) {
* return (
* <>
* <NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition}>
* <button>delete</button>
* <button>copy</button>
* <button>expand</button>
* </NodeToolbar>
*
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(CustomNode);
*```
* @remarks By default, the toolbar is only visible when a node is selected. If multiple
* nodes are selected it will not be visible to prevent overlapping toolbars or
* clutter. You can override this behavior by setting the `isVisible` prop to `true`.
*/
export function NodeToolbar({
nodeId,
children,
@@ -74,7 +109,7 @@ export function NodeToolbar({
const isActive =
typeof isVisible === 'boolean'
? isVisible
: nodes.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1;
: nodes.size === 1 && nodes.values().next().value?.selected && selectedNodesCount === 1;
if (!isActive || !nodes.size) {
return null;
@@ -1,19 +1,31 @@
import type { HTMLAttributes } from 'react';
import type { Position, Align } from '@xyflow/system';
/**
* @expand
*/
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
/** Id of the node, or array of ids the toolbar should be displayed at */
/**
* By passing in an array of node id's you can render a single tooltip for a group or collection
* of nodes.
*/
nodeId?: string | string[];
/** If true, node toolbar is visible even if node is not selected */
/** If `true`, node toolbar is visible even if node is not selected. */
isVisible?: boolean;
/** Position of the toolbar relative to the node
* @example Position.TopLeft, Position.TopRight,
* Position.BottomLeft, Position.BottomRight
/**
* Position of the toolbar relative to the node.
* @default Position.Top
* @example Position.TopLeft, Position.TopRight, Position.BottomLeft, Position.BottomRight
*/
position?: Position;
/** Offset the toolbar from the node */
/**
* The space between the node and the toolbar, measured in pixels.
* @default 10
*/
offset?: number;
/** Align the toolbar relative to the node
/**
* Align the toolbar relative to the node.
* @default "center"
* @example Align.Start, Align.Center, Align.End
*/
align?: Align;