From d59707f4f208d2ea18f3a383b1f254fccb418143 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 13 Nov 2025 10:28:56 +0100 Subject: [PATCH] feat(nodes-edges): add zIndexMode --- .../components/ReactFlowProvider/index.tsx | 5 +- .../src/components/StoreUpdater/index.tsx | 1 + .../react/src/container/ReactFlow/Wrapper.tsx | 5 +- .../react/src/container/ReactFlow/index.tsx | 3 ++ packages/react/src/store/index.ts | 28 ++++++++--- packages/react/src/store/initialState.ts | 4 ++ packages/react/src/types/component-props.ts | 2 + packages/react/src/types/store.ts | 3 ++ packages/system/src/types/general.ts | 2 + packages/system/src/utils/store.ts | 50 +++++++++++++------ 10 files changed, 80 insertions(+), 23 deletions(-) diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 11a9c6b8..51d344f0 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -4,7 +4,7 @@ import { Provider } from '../../contexts/StoreContext'; import { createStore } from '../../store'; import { BatchProvider } from '../BatchProvider'; import type { Node, Edge, FitViewOptions } from '../../types'; -import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; +import { CoordinateExtent, NodeOrigin, ZIndexMode } from '@xyflow/system'; export type ReactFlowProviderProps = { /** These nodes are used to initialize the flow. They are not dynamic. */ @@ -48,6 +48,7 @@ export type ReactFlowProviderProps = { */ nodeExtent?: CoordinateExtent; children: ReactNode; + zIndexMode?: ZIndexMode; }; /** @@ -97,6 +98,7 @@ export function ReactFlowProvider({ fitView, nodeOrigin, nodeExtent, + zIndexMode, children, }: ReactFlowProviderProps) { const [store] = useState(() => @@ -113,6 +115,7 @@ export function ReactFlowProvider({ fitViewOptions, nodeOrigin, nodeExtent, + zIndexMode, }) ); diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 7f375d0a..223e62bc 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -70,6 +70,7 @@ const reactFlowFieldsToTrack = [ 'debug', 'autoPanSpeed', 'ariaLabelConfig', + 'zIndexMode', ] as const; type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number]; diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index 9ae0aa62..0591aba3 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -3,7 +3,7 @@ import { useContext, type ReactNode } from 'react'; import StoreContext from '../../contexts/StoreContext'; import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import type { Node, Edge, FitViewOptions } from '../../types'; -import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; +import { CoordinateExtent, NodeOrigin, ZIndexMode } from '@xyflow/system'; export function Wrapper({ children, @@ -19,6 +19,7 @@ export function Wrapper({ maxZoom, nodeOrigin, nodeExtent, + zIndexMode, }: { children: ReactNode; nodes?: Node[]; @@ -33,6 +34,7 @@ export function Wrapper({ maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; + zIndexMode?: ZIndexMode; }) { const isWrapped = useContext(StoreContext); @@ -58,6 +60,7 @@ export function Wrapper({ initialMaxZoom={maxZoom} nodeOrigin={nodeOrigin} nodeExtent={nodeExtent} + zIndexMode={zIndexMode} > {children} diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index bd20242f..aeebb2c4 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -148,6 +148,7 @@ function ReactFlow( debug, onScroll, ariaLabelConfig, + zIndexMode, ...rest }: ReactFlowProps, ref: ForwardedRef @@ -186,6 +187,7 @@ function ReactFlow( maxZoom={maxZoom} nodeOrigin={nodeOrigin} nodeExtent={nodeExtent} + zIndexMode={zIndexMode} > onInit={onInit} @@ -311,6 +313,7 @@ function ReactFlow( onBeforeDelete={onBeforeDelete} debug={debug} ariaLabelConfig={ariaLabelConfig} + zIndexMode={zIndexMode} /> onSelectionChange={onSelectionChange} /> {children} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 3254bdfa..23ce6913 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -14,6 +14,7 @@ import { NodeOrigin, CoordinateExtent, fitViewport, + ZIndexMode, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -33,6 +34,7 @@ const createStore = ({ maxZoom, nodeOrigin, nodeExtent, + zIndexMode, }: { nodes?: Node[]; edges?: Edge[]; @@ -46,6 +48,7 @@ const createStore = ({ maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; + zIndexMode?: ZIndexMode; }) => createWithEqualityFn((set, get) => { async function resolveFitView() { @@ -89,9 +92,10 @@ const createStore = ({ nodeExtent, defaultNodes, defaultEdges, + zIndexMode, }), setNodes: (nodes: Node[]) => { - const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get(); + const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued, zIndexMode } = get(); /* * setNodes() is called exclusively in response to user actions: * - either when the `` prop is updated in the controlled ReactFlow setup, @@ -106,6 +110,7 @@ const createStore = ({ nodeExtent, elevateNodesOnSelect, checkEquality: true, + zIndexMode, }); if (fitViewQueued && nodesInitialized) { @@ -140,8 +145,17 @@ const createStore = ({ * new dimensions and update the nodes. */ updateNodeInternals: (updates) => { - const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued } = - get(); + const { + triggerNodeChanges, + nodeLookup, + parentLookup, + domNode, + nodeOrigin, + nodeExtent, + debug, + fitViewQueued, + zIndexMode, + } = get(); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -149,14 +163,15 @@ const createStore = ({ parentLookup, domNode, nodeOrigin, - nodeExtent + nodeExtent, + zIndexMode ); if (!updatedInternals) { return; } - updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); + updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent, zIndexMode }); if (fitViewQueued) { resolveFitView(); @@ -332,7 +347,7 @@ const createStore = ({ triggerEdgeChanges(edgeChanges); }, setNodeExtent: (nextNodeExtent) => { - const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent } = get(); + const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent, zIndexMode } = get(); if ( nextNodeExtent[0][0] === nodeExtent[0][0] && @@ -348,6 +363,7 @@ const createStore = ({ nodeExtent: nextNodeExtent, elevateNodesOnSelect, checkEquality: false, + zIndexMode, }); set({ nodeExtent: nextNodeExtent }); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 3e561ce9..da1234c0 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -11,6 +11,7 @@ import { initialConnection, CoordinateExtent, defaultAriaLabelConfig, + ZIndexMode, } from '@xyflow/system'; import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types'; @@ -28,6 +29,7 @@ const getInitialState = ({ maxZoom = 2, nodeOrigin, nodeExtent, + zIndexMode = 'auto', }: { nodes?: Node[]; edges?: Edge[]; @@ -41,6 +43,7 @@ const getInitialState = ({ maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; + zIndexMode?: ZIndexMode; } = {}): ReactFlowStore => { const nodeLookup = new Map(); const parentLookup = new Map(); @@ -146,6 +149,7 @@ const getInitialState = ({ lib: 'react', debug: false, ariaLabelConfig: defaultAriaLabelConfig, + zIndexMode, }; }; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 3e534aaa..25c6750a 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -23,6 +23,7 @@ import type { OnReconnect, AriaLabelConfig, FinalConnectionState, + ZIndexMode, } from '@xyflow/system'; import type { @@ -691,4 +692,5 @@ export interface ReactFlowProps; + zIndexMode?: ZIndexMode; } diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 59a05127..1a723405 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -30,6 +30,7 @@ import { type ParentLookup, type AriaLabelConfig, SetCenter, + ZIndexMode, } from '@xyflow/system'; import type { @@ -152,6 +153,8 @@ export type ReactFlowStore = { diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index d43c50ee..a7b0ae49 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -367,3 +367,5 @@ export type OnBeforeDeleteBase Promise; + +export type ZIndexMode = 'auto' | 'manual'; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 88f4bb79..c8207b91 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,4 +1,4 @@ -import { Handle, HandleConnection, infiniteExtent, NodeHandle, NodeHandleBounds } from '..'; +import { Handle, HandleConnection, infiniteExtent, NodeHandleBounds, ZIndexMode } from '..'; import { NodeBase, CoordinateExtent, @@ -37,6 +37,7 @@ const defaultOptions = { nodeOrigin: [0, 0] as NodeOrigin, nodeExtent: infiniteExtent, elevateNodesOnSelect: true, + zIndexMode: 'auto' as ZIndexMode, defaults: {}, }; @@ -112,21 +113,27 @@ type UpdateNodesOptions = { nodeExtent?: CoordinateExtent; elevateNodesOnSelect?: boolean; defaults?: Partial; + zIndexMode?: ZIndexMode; checkEquality?: boolean; }; +function isManualZIndexMode(zIndexMode?: ZIndexMode): boolean { + return zIndexMode === 'manual'; +} + export function adoptUserNodes( nodes: NodeType[], nodeLookup: NodeLookup>, parentLookup: ParentLookup>, - options?: UpdateNodesOptions + options: UpdateNodesOptions = {} ): boolean { const _options = mergeObjects(adoptUserNodesDefaultOptions, options); let rootParentIndex = { i: -1 }; let nodesInitialized = nodes.length > 0; const tmpLookup = new Map(nodeLookup); - const selectedNodeZ: number = _options?.elevateNodesOnSelect ? SELECTED_NODE_Z : 0; + const selectedNodeZ: number = + _options?.elevateNodesOnSelect && !isManualZIndexMode(_options.zIndexMode) ? SELECTED_NODE_Z : 0; nodeLookup.clear(); parentLookup.clear(); @@ -152,7 +159,7 @@ export function adoptUserNodes( positionAbsolute: clampedPosition, // if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured handleBounds: parseHandles(userNode, internalNode), - z: calculateZ(userNode, selectedNodeZ), + z: calculateZ(userNode, selectedNodeZ, _options.zIndexMode), userNode, }, }; @@ -201,10 +208,10 @@ function updateChildNode( node: InternalNodeBase, nodeLookup: NodeLookup>, parentLookup: ParentLookup>, - options?: UpdateNodesOptions, + options: UpdateNodesOptions, rootParentIndex?: { i: number } ) { - const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options); + const { elevateNodesOnSelect, nodeOrigin, nodeExtent, zIndexMode } = mergeObjects(defaultOptions, options); const parentId = node.parentId!; const parentNode = nodeLookup.get(parentId); @@ -218,7 +225,12 @@ function updateChildNode( updateParentLookup(node, parentLookup); // We just want to set the rootParentIndex for the first child - if (rootParentIndex && !parentNode.parentId && parentNode.internals.rootParentIndex === undefined) { + if ( + rootParentIndex && + !parentNode.parentId && + parentNode.internals.rootParentIndex === undefined && + !isManualZIndexMode(zIndexMode) + ) { parentNode.internals.rootParentIndex = ++rootParentIndex.i; parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT; } @@ -228,8 +240,8 @@ function updateChildNode( rootParentIndex.i = parentNode.internals.rootParentIndex; } - const selectedNodeZ = elevateNodesOnSelect ? SELECTED_NODE_Z : 0; - const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ); + const selectedNodeZ = elevateNodesOnSelect && !isManualZIndexMode(zIndexMode) ? SELECTED_NODE_Z : 0; + const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ, zIndexMode); const { positionAbsolute } = node.internals; const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y; @@ -246,8 +258,14 @@ function updateChildNode( } } -function calculateZ(node: NodeBase, selectedNodeZ: number) { - return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0); +function calculateZ(node: NodeBase, selectedNodeZ: number, zIndexMode: ZIndexMode): number { + const zIndex = isNumeric(node.zIndex) ? node.zIndex : 0; + + if (isManualZIndexMode(zIndexMode)) { + return zIndex; + } + + return zIndex + (node.selected ? selectedNodeZ : 0); } function calculateChildXYZ( @@ -255,7 +273,8 @@ function calculateChildXYZ( parentNode: InternalNodeBase, nodeOrigin: NodeOrigin, nodeExtent: CoordinateExtent, - selectedNodeZ: number + selectedNodeZ: number, + zIndexMode: ZIndexMode ) { const { x: parentX, y: parentY } = parentNode.internals.positionAbsolute; const childDimensions = getNodeDimensions(childNode); @@ -274,7 +293,7 @@ function calculateChildXYZ( absolutePosition = clampPositionToParent(absolutePosition, childDimensions, parentNode); } - const childZ = calculateZ(childNode, selectedNodeZ); + const childZ = calculateZ(childNode, selectedNodeZ, zIndexMode); const parentZ = parentNode.internals.z ?? 0; return { @@ -378,7 +397,8 @@ export function updateNodeInternals( parentLookup: ParentLookup, domNode: HTMLElement | null, nodeOrigin?: NodeOrigin, - nodeExtent?: CoordinateExtent + nodeExtent?: CoordinateExtent, + zIndexMode?: ZIndexMode ): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } { const viewportNode = domNode?.querySelector('.xyflow__viewport'); let updatedInternals = false; @@ -446,7 +466,7 @@ export function updateNodeInternals( nodeLookup.set(node.id, newNode); if (node.parentId) { - updateChildNode(newNode, nodeLookup, parentLookup, { nodeOrigin }); + updateChildNode(newNode, nodeLookup, parentLookup, { nodeOrigin, zIndexMode }); } updatedInternals = true;