diff --git a/.changeset/breezy-gorillas-accept.md b/.changeset/breezy-gorillas-accept.md new file mode 100644 index 00000000..920cfac7 --- /dev/null +++ b/.changeset/breezy-gorillas-accept.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': minor +'@xyflow/system': patch +--- + +Add `zIndexMode` to control how z-index is calculated for nodes and edges diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 4cf4b57b..055dc9e1 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -60,6 +60,7 @@ import DevTools from '../examples/DevTools'; import Redux from '../examples/Redux'; import MovingHandles from '../examples/MovingHandles'; import DetachedHandle from '../examples/DetachedHandle'; +import ZIndexMode from '../examples/ZIndexMode'; import Middlewares from '../examples/Middlewares'; export interface IRoute { @@ -384,6 +385,11 @@ const routes: IRoute[] = [ path: 'use-key-press', component: UseKeyPress, }, + { + name: 'zIndexMode', + path: 'z-index-mode', + component: ZIndexMode, + }, ]; export default routes; diff --git a/examples/react/src/examples/ColorMode/index.tsx b/examples/react/src/examples/ColorMode/index.tsx index eae8ad86..f867c650 100644 --- a/examples/react/src/examples/ColorMode/index.tsx +++ b/examples/react/src/examples/ColorMode/index.tsx @@ -15,8 +15,6 @@ import { Position, } from '@xyflow/react'; -import './style.css'; - const nodeDefaults = { sourcePosition: Position.Right, targetPosition: Position.Left, diff --git a/examples/react/src/examples/ColorMode/style.css b/examples/react/src/examples/ColorMode/style.css deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/react/src/examples/ZIndexMode/index.tsx b/examples/react/src/examples/ZIndexMode/index.tsx new file mode 100644 index 00000000..5b5477d9 --- /dev/null +++ b/examples/react/src/examples/ZIndexMode/index.tsx @@ -0,0 +1,88 @@ +import { ChangeEventHandler, useCallback, useState } from 'react'; +import { + ReactFlow, + addEdge, + Node, + useNodesState, + useEdgesState, + OnConnect, + Edge, + MiniMap, + Background, + Controls, + Position, + ZIndexMode, + Panel, +} from '@xyflow/react'; + +const nodeDefaults = { + sourcePosition: Position.Right, + targetPosition: Position.Left, +}; + +const initialNodes: Node[] = [ + { id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults }, + { id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults }, + { id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults }, + + // group 1 + { id: 'D', position: { x: 0, y: 300 }, width: 200, height: 200, data: { label: 'D' }, ...nodeDefaults }, + { id: 'E', parentId: 'D', position: { x: 10, y: 10 }, data: { label: 'E' }, ...nodeDefaults }, + + // group 2 + { id: 'F', position: { x: 250, y: 300 }, width: 200, height: 200, data: { label: 'F' }, ...nodeDefaults }, + { id: 'G', parentId: 'F', position: { x: 10, y: 10 }, data: { label: 'G' }, ...nodeDefaults }, + + // group 3 + { id: 'H', position: { x: 500, y: 300 }, width: 200, height: 200, data: { label: 'H' }, ...nodeDefaults }, + { id: 'I', parentId: 'H', position: { x: 10, y: 10 }, data: { label: 'I' }, ...nodeDefaults }, +]; + +const initialEdges: Edge[] = [ + { + id: 'A-B', + source: 'A', + target: 'B', + }, + { + id: 'A-C', + source: 'A', + target: 'C', + }, +]; + +function ZIndexModeFlow() { + const [zIndexMode, setZIndexMode] = useState('auto'); + const [nodes, , onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onConnect: OnConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + + const onChange: ChangeEventHandler = (evt) => setZIndexMode(evt.target.value as ZIndexMode); + + return ( + + + + + + + + + + ); +} + +export default ZIndexModeFlow; diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 7b76cdfa..700ef11a 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -89,6 +89,7 @@ function EdgeWrapper({ sourceNode, targetNode, elevateOnSelect: store.elevateEdgesOnSelect, + zIndexMode: store.zIndexMode, }); return { 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 f01af831..b04f2955 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -127,8 +127,8 @@ function ReactFlow( attributionPosition, proOptions, defaultEdgeOptions, - elevateNodesOnSelect, - elevateEdgesOnSelect, + elevateNodesOnSelect = true, + elevateEdgesOnSelect = false, disableKeyboardA11y = false, autoPanOnConnect, autoPanOnNodeDrag, @@ -148,6 +148,7 @@ function ReactFlow( debug, onScroll, ariaLabelConfig, + zIndexMode = 'basic', ...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/index.ts b/packages/react/src/index.ts index f251ba5d..79a50440 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -116,6 +116,7 @@ export { type SetViewport, type FitBounds, type HandleConnection, + type ZIndexMode, } from '@xyflow/system'; // we need this workaround to prevent a duplicate identifier error diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index fa1b5e3d..1c5bdd3d 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -16,6 +16,7 @@ import { fitViewport, getHandlePosition, Position, + ZIndexMode } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -35,6 +36,7 @@ const createStore = ({ maxZoom, nodeOrigin, nodeExtent, + zIndexMode, }: { nodes?: Node[]; edges?: Edge[]; @@ -48,6 +50,7 @@ const createStore = ({ maxZoom?: number; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; + zIndexMode?: ZIndexMode; }) => createWithEqualityFn((set, get) => { async function resolveFitView() { @@ -91,9 +94,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, @@ -108,6 +112,7 @@ const createStore = ({ nodeExtent, elevateNodesOnSelect, checkEquality: true, + zIndexMode, }); if (fitViewQueued && nodesInitialized) { @@ -142,8 +147,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, @@ -151,14 +165,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(); @@ -343,7 +358,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] && @@ -359,6 +374,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 c7ddda17..26a5c045 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 = 'basic', }: { 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(); @@ -56,7 +59,7 @@ const getInitialState = ({ const nodesInitialized = adoptUserNodes(storeNodes, nodeLookup, parentLookup, { nodeOrigin: storeNodeOrigin, nodeExtent: storeNodeExtent, - elevateNodesOnSelect: false, + zIndexMode, }); let transform: Transform = [0, 0, 1]; @@ -119,7 +122,7 @@ const getInitialState = ({ edgesReconnectable: true, elementsSelectable: true, elevateNodesOnSelect: true, - elevateEdgesOnSelect: false, + elevateEdgesOnSelect: true, selectNodesOnDrag: true, multiSelectionActive: false, @@ -146,6 +149,7 @@ const getInitialState = ({ lib: 'react', debug: false, ariaLabelConfig: defaultAriaLabelConfig, + zIndexMode, onNodesChangeMiddlewareMap: new Map(), onEdgesChangeMiddlewareMap: new Map(), diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 3e534aaa..effc7ca6 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,11 @@ export interface ReactFlowProps; + /** + * Used to define how z-indexing is calculated for nodes and edges. + * 'auto' is for selections and sub flows, 'basic' for selections only, and 'manual' for no auto z-indexing. + * + * @default 'basic' + */ + zIndexMode?: ZIndexMode; } diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 717961ec..db881fc2 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -29,7 +29,8 @@ import { type EdgeChange, type ParentLookup, type AriaLabelConfig, - SetCenter, + type SetCenter, + type ZIndexMode, } from '@xyflow/system'; import type { @@ -153,6 +154,7 @@ export type ReactFlowStore[]) => NodeChange[]>; onEdgesChangeMiddlewareMap: Map[]) => EdgeChange[]>; }; diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index ab9378b8..a2283be7 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -75,7 +75,7 @@ }: SvelteFlowProps & Omit, 'onselectionchange'> = $props(); - // svelte-ignore non_reactive_update + // svelte-ignore non_reactive_update, state_referenced_locally let store = createStore({ props, width, diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index 88ba9f1a..b9314774 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -96,6 +96,7 @@ ariaLabelConfig, autoPanSpeed, panOnScrollSpeed, + zIndexMode, ...divAttributes } = $derived(rest); /* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 0fbd4b18..8e5c6c8c 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -21,7 +21,8 @@ import type { OnReconnect, OnReconnectStart, OnReconnectEnd, - AriaLabelConfig + AriaLabelConfig, + ZIndexMode } from '@xyflow/system'; import type { @@ -500,4 +501,11 @@ export type SvelteFlowProps< * Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings. */ ariaLabelConfig?: Partial; + /** + * Used to define how z-indexing is calculated for nodes and edges. + * 'auto' is for selections and sub flows, 'basic' for selections only, and 'manual' for no auto z-indexing. + * + * @default 'basic' + */ + zIndexMode?: ZIndexMode; }; diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 815803f7..9665f211 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -122,7 +122,8 @@ export { type SetCenter, type SetViewport, type FitBounds, - type HandleConnection + type HandleConnection, + type ZIndexMode } from '@xyflow/system'; // system utils diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 30bcafc1..3a7b1123 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -73,7 +73,9 @@ export function createStore(signals.width ?? 0); height = $state.raw(signals.height ?? 0); + zIndexMode = $state.raw(signals.props.zIndexMode ?? 'basic'); nodesInitialized: boolean = $derived.by(() => { const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, { nodeExtent: this.nodeExtent, nodeOrigin: this.nodeOrigin, elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true, - checkEquality: true + checkEquality: true, + zIndexMode: this.zIndexMode }); if (this.fitViewQueued && nodesInitialized) { @@ -226,7 +229,8 @@ export function getInitialStore>; @@ -239,6 +243,7 @@ export function getInitialStore( @@ -42,6 +43,7 @@ export interface EdgeLayoutAllOptions = @@ -70,7 +73,8 @@ export function getLayoutedEdges>(); for (const edge of edges) { @@ -134,7 +138,8 @@ export function getLayoutedEdges Promise; + +/** + * The `ZIndexMode` type is used to define how z-indexing is calculated for nodes and edges. + * `auto` mode will automatically manage z-indexing for selections and sub flows. + * `basic` mode will only manage z-indexing for selections. + * `manual` mode does not apply any automatic z-indexing. + * + * @public + */ +export type ZIndexMode = 'auto' | 'basic' | 'manual'; diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index 515a285a..25719c3d 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -1,4 +1,13 @@ -import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..'; +import { + Connection, + InternalNodeBase, + Transform, + errorMessages, + isEdgeBase, + EdgeBase, + ZIndexMode, + isManualZIndexMode, +} from '../..'; import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general'; // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB) @@ -28,6 +37,7 @@ export type GetEdgeZIndexParams = { selected?: boolean; zIndex?: number; elevateOnSelect?: boolean; + zIndexMode?: ZIndexMode; }; /** @@ -41,12 +51,19 @@ export function getElevatedEdgeZIndex({ selected = false, zIndex, elevateOnSelect = false, + zIndexMode = 'basic', }: GetEdgeZIndexParams): number { + const manualZIndexMode = isManualZIndexMode(zIndexMode); + if (zIndex !== undefined) { return zIndex; } - const edgeZ = elevateOnSelect && selected ? 1000 : 0; + if (manualZIndexMode) { + return 0; + } + + const edgeZ = elevateOnSelect && selected ? 1000 : 0; const nodeZ = Math.max( sourceNode.parentId || (elevateOnSelect && sourceNode.selected) ? sourceNode.internals.z : 0, targetNode.parentId || (elevateOnSelect && targetNode.selected) ? targetNode.internals.z : 0 diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 88f4bb79..b499dfee 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: 'basic' as ZIndexMode, defaults: {}, }; @@ -112,21 +113,26 @@ type UpdateNodesOptions = { nodeExtent?: CoordinateExtent; elevateNodesOnSelect?: boolean; defaults?: Partial; + zIndexMode?: ZIndexMode; checkEquality?: boolean; }; +export 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 rootParentIndex = { i: 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; + let nodesInitialized = nodes.length > 0; nodeLookup.clear(); parentLookup.clear(); @@ -152,7 +158,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 +207,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 +224,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 && + zIndexMode === 'auto' + ) { parentNode.internals.rootParentIndex = ++rootParentIndex.i; parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT; } @@ -228,8 +239,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 +257,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 +272,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 +292,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 +396,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 +465,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;