Merge pull request #5637 from xyflow/feat/zindexmode

feat(nodes-edges): add zIndexMode
This commit is contained in:
Moritz Klack
2025-12-03 14:31:23 +01:00
committed by GitHub
25 changed files with 256 additions and 46 deletions

View File

@@ -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

View File

@@ -60,6 +60,7 @@ import DevTools from '../examples/DevTools';
import Redux from '../examples/Redux'; import Redux from '../examples/Redux';
import MovingHandles from '../examples/MovingHandles'; import MovingHandles from '../examples/MovingHandles';
import DetachedHandle from '../examples/DetachedHandle'; import DetachedHandle from '../examples/DetachedHandle';
import ZIndexMode from '../examples/ZIndexMode';
import Middlewares from '../examples/Middlewares'; import Middlewares from '../examples/Middlewares';
export interface IRoute { export interface IRoute {
@@ -384,6 +385,11 @@ const routes: IRoute[] = [
path: 'use-key-press', path: 'use-key-press',
component: UseKeyPress, component: UseKeyPress,
}, },
{
name: 'zIndexMode',
path: 'z-index-mode',
component: ZIndexMode,
},
]; ];
export default routes; export default routes;

View File

@@ -15,8 +15,6 @@ import {
Position, Position,
} from '@xyflow/react'; } from '@xyflow/react';
import './style.css';
const nodeDefaults = { const nodeDefaults = {
sourcePosition: Position.Right, sourcePosition: Position.Right,
targetPosition: Position.Left, targetPosition: Position.Left,

View File

@@ -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<ZIndexMode>('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<HTMLSelectElement> = (evt) => setZIndexMode(evt.target.value as ZIndexMode);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
zIndexMode={zIndexMode}
fitView
>
<MiniMap />
<Background />
<Controls />
<Panel position="top-right">
<select value={zIndexMode} onChange={onChange} data-testid="zindexmode-select">
<option value="manual">manual</option>
<option value="basic">basic</option>
<option value="auto">auto</option>
</select>
</Panel>
</ReactFlow>
);
}
export default ZIndexModeFlow;

View File

@@ -89,6 +89,7 @@ function EdgeWrapper<EdgeType extends Edge = Edge>({
sourceNode, sourceNode,
targetNode, targetNode,
elevateOnSelect: store.elevateEdgesOnSelect, elevateOnSelect: store.elevateEdgesOnSelect,
zIndexMode: store.zIndexMode,
}); });
return { return {

View File

@@ -4,7 +4,7 @@ import { Provider } from '../../contexts/StoreContext';
import { createStore } from '../../store'; import { createStore } from '../../store';
import { BatchProvider } from '../BatchProvider'; import { BatchProvider } from '../BatchProvider';
import type { Node, Edge, FitViewOptions } from '../../types'; import type { Node, Edge, FitViewOptions } from '../../types';
import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; import { CoordinateExtent, NodeOrigin, ZIndexMode } from '@xyflow/system';
export type ReactFlowProviderProps = { export type ReactFlowProviderProps = {
/** These nodes are used to initialize the flow. They are not dynamic. */ /** These nodes are used to initialize the flow. They are not dynamic. */
@@ -48,6 +48,7 @@ export type ReactFlowProviderProps = {
*/ */
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
children: ReactNode; children: ReactNode;
zIndexMode?: ZIndexMode;
}; };
/** /**
@@ -97,6 +98,7 @@ export function ReactFlowProvider({
fitView, fitView,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
zIndexMode,
children, children,
}: ReactFlowProviderProps) { }: ReactFlowProviderProps) {
const [store] = useState(() => const [store] = useState(() =>
@@ -113,6 +115,7 @@ export function ReactFlowProvider({
fitViewOptions, fitViewOptions,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
zIndexMode,
}) })
); );

View File

@@ -70,6 +70,7 @@ const reactFlowFieldsToTrack = [
'debug', 'debug',
'autoPanSpeed', 'autoPanSpeed',
'ariaLabelConfig', 'ariaLabelConfig',
'zIndexMode',
] as const; ] as const;
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number]; type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];

View File

@@ -3,7 +3,7 @@ import { useContext, type ReactNode } from 'react';
import StoreContext from '../../contexts/StoreContext'; import StoreContext from '../../contexts/StoreContext';
import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import { ReactFlowProvider } from '../../components/ReactFlowProvider';
import type { Node, Edge, FitViewOptions } from '../../types'; import type { Node, Edge, FitViewOptions } from '../../types';
import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; import { CoordinateExtent, NodeOrigin, ZIndexMode } from '@xyflow/system';
export function Wrapper({ export function Wrapper({
children, children,
@@ -19,6 +19,7 @@ export function Wrapper({
maxZoom, maxZoom,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
zIndexMode,
}: { }: {
children: ReactNode; children: ReactNode;
nodes?: Node[]; nodes?: Node[];
@@ -33,6 +34,7 @@ export function Wrapper({
maxZoom?: number; maxZoom?: number;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
zIndexMode?: ZIndexMode;
}) { }) {
const isWrapped = useContext(StoreContext); const isWrapped = useContext(StoreContext);
@@ -58,6 +60,7 @@ export function Wrapper({
initialMaxZoom={maxZoom} initialMaxZoom={maxZoom}
nodeOrigin={nodeOrigin} nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
zIndexMode={zIndexMode}
> >
{children} {children}
</ReactFlowProvider> </ReactFlowProvider>

View File

@@ -127,8 +127,8 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
attributionPosition, attributionPosition,
proOptions, proOptions,
defaultEdgeOptions, defaultEdgeOptions,
elevateNodesOnSelect, elevateNodesOnSelect = true,
elevateEdgesOnSelect, elevateEdgesOnSelect = false,
disableKeyboardA11y = false, disableKeyboardA11y = false,
autoPanOnConnect, autoPanOnConnect,
autoPanOnNodeDrag, autoPanOnNodeDrag,
@@ -148,6 +148,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
debug, debug,
onScroll, onScroll,
ariaLabelConfig, ariaLabelConfig,
zIndexMode = 'basic',
...rest ...rest
}: ReactFlowProps<NodeType, EdgeType>, }: ReactFlowProps<NodeType, EdgeType>,
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>
@@ -186,6 +187,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
maxZoom={maxZoom} maxZoom={maxZoom}
nodeOrigin={nodeOrigin} nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
zIndexMode={zIndexMode}
> >
<GraphView<NodeType, EdgeType> <GraphView<NodeType, EdgeType>
onInit={onInit} onInit={onInit}
@@ -311,6 +313,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onBeforeDelete={onBeforeDelete} onBeforeDelete={onBeforeDelete}
debug={debug} debug={debug}
ariaLabelConfig={ariaLabelConfig} ariaLabelConfig={ariaLabelConfig}
zIndexMode={zIndexMode}
/> />
<SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} /> <SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} />
{children} {children}

View File

@@ -116,6 +116,7 @@ export {
type SetViewport, type SetViewport,
type FitBounds, type FitBounds,
type HandleConnection, type HandleConnection,
type ZIndexMode,
} from '@xyflow/system'; } from '@xyflow/system';
// we need this workaround to prevent a duplicate identifier error // we need this workaround to prevent a duplicate identifier error

View File

@@ -16,6 +16,7 @@ import {
fitViewport, fitViewport,
getHandlePosition, getHandlePosition,
Position, Position,
ZIndexMode
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -35,6 +36,7 @@ const createStore = ({
maxZoom, maxZoom,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
zIndexMode,
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
@@ -48,6 +50,7 @@ const createStore = ({
maxZoom?: number; maxZoom?: number;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
zIndexMode?: ZIndexMode;
}) => }) =>
createWithEqualityFn<ReactFlowState>((set, get) => { createWithEqualityFn<ReactFlowState>((set, get) => {
async function resolveFitView() { async function resolveFitView() {
@@ -91,9 +94,10 @@ const createStore = ({
nodeExtent, nodeExtent,
defaultNodes, defaultNodes,
defaultEdges, defaultEdges,
zIndexMode,
}), }),
setNodes: (nodes: Node[]) => { 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: * setNodes() is called exclusively in response to user actions:
* - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup, * - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
@@ -108,6 +112,7 @@ const createStore = ({
nodeExtent, nodeExtent,
elevateNodesOnSelect, elevateNodesOnSelect,
checkEquality: true, checkEquality: true,
zIndexMode,
}); });
if (fitViewQueued && nodesInitialized) { if (fitViewQueued && nodesInitialized) {
@@ -142,8 +147,17 @@ const createStore = ({
* new dimensions and update the nodes. * new dimensions and update the nodes.
*/ */
updateNodeInternals: (updates) => { updateNodeInternals: (updates) => {
const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued } = const {
get(); triggerNodeChanges,
nodeLookup,
parentLookup,
domNode,
nodeOrigin,
nodeExtent,
debug,
fitViewQueued,
zIndexMode,
} = get();
const { changes, updatedInternals } = updateNodeInternalsSystem( const { changes, updatedInternals } = updateNodeInternalsSystem(
updates, updates,
@@ -151,14 +165,15 @@ const createStore = ({
parentLookup, parentLookup,
domNode, domNode,
nodeOrigin, nodeOrigin,
nodeExtent nodeExtent,
zIndexMode
); );
if (!updatedInternals) { if (!updatedInternals) {
return; return;
} }
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent, zIndexMode });
if (fitViewQueued) { if (fitViewQueued) {
resolveFitView(); resolveFitView();
@@ -343,7 +358,7 @@ const createStore = ({
triggerEdgeChanges(edgeChanges); triggerEdgeChanges(edgeChanges);
}, },
setNodeExtent: (nextNodeExtent) => { setNodeExtent: (nextNodeExtent) => {
const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent } = get(); const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent, zIndexMode } = get();
if ( if (
nextNodeExtent[0][0] === nodeExtent[0][0] && nextNodeExtent[0][0] === nodeExtent[0][0] &&
@@ -359,6 +374,7 @@ const createStore = ({
nodeExtent: nextNodeExtent, nodeExtent: nextNodeExtent,
elevateNodesOnSelect, elevateNodesOnSelect,
checkEquality: false, checkEquality: false,
zIndexMode,
}); });
set({ nodeExtent: nextNodeExtent }); set({ nodeExtent: nextNodeExtent });

View File

@@ -11,6 +11,7 @@ import {
initialConnection, initialConnection,
CoordinateExtent, CoordinateExtent,
defaultAriaLabelConfig, defaultAriaLabelConfig,
ZIndexMode,
} from '@xyflow/system'; } from '@xyflow/system';
import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types'; import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types';
@@ -28,6 +29,7 @@ const getInitialState = ({
maxZoom = 2, maxZoom = 2,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
zIndexMode = 'basic',
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
@@ -41,6 +43,7 @@ const getInitialState = ({
maxZoom?: number; maxZoom?: number;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
zIndexMode?: ZIndexMode;
} = {}): ReactFlowStore => { } = {}): ReactFlowStore => {
const nodeLookup = new Map<string, InternalNode>(); const nodeLookup = new Map<string, InternalNode>();
const parentLookup = new Map(); const parentLookup = new Map();
@@ -56,7 +59,7 @@ const getInitialState = ({
const nodesInitialized = adoptUserNodes(storeNodes, nodeLookup, parentLookup, { const nodesInitialized = adoptUserNodes(storeNodes, nodeLookup, parentLookup, {
nodeOrigin: storeNodeOrigin, nodeOrigin: storeNodeOrigin,
nodeExtent: storeNodeExtent, nodeExtent: storeNodeExtent,
elevateNodesOnSelect: false, zIndexMode,
}); });
let transform: Transform = [0, 0, 1]; let transform: Transform = [0, 0, 1];
@@ -119,7 +122,7 @@ const getInitialState = ({
edgesReconnectable: true, edgesReconnectable: true,
elementsSelectable: true, elementsSelectable: true,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
elevateEdgesOnSelect: false, elevateEdgesOnSelect: true,
selectNodesOnDrag: true, selectNodesOnDrag: true,
multiSelectionActive: false, multiSelectionActive: false,
@@ -146,6 +149,7 @@ const getInitialState = ({
lib: 'react', lib: 'react',
debug: false, debug: false,
ariaLabelConfig: defaultAriaLabelConfig, ariaLabelConfig: defaultAriaLabelConfig,
zIndexMode,
onNodesChangeMiddlewareMap: new Map(), onNodesChangeMiddlewareMap: new Map(),
onEdgesChangeMiddlewareMap: new Map(), onEdgesChangeMiddlewareMap: new Map(),

View File

@@ -23,6 +23,7 @@ import type {
OnReconnect, OnReconnect,
AriaLabelConfig, AriaLabelConfig,
FinalConnectionState, FinalConnectionState,
ZIndexMode,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -691,4 +692,11 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings. * Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings.
*/ */
ariaLabelConfig?: Partial<AriaLabelConfig>; ariaLabelConfig?: Partial<AriaLabelConfig>;
/**
* 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;
} }

View File

@@ -29,7 +29,8 @@ import {
type EdgeChange, type EdgeChange,
type ParentLookup, type ParentLookup,
type AriaLabelConfig, type AriaLabelConfig,
SetCenter, type SetCenter,
type ZIndexMode,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -153,6 +154,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
debug: boolean; debug: boolean;
ariaLabelConfig: AriaLabelConfig; ariaLabelConfig: AriaLabelConfig;
zIndexMode: ZIndexMode;
onNodesChangeMiddlewareMap: Map<symbol, (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]>; onNodesChangeMiddlewareMap: Map<symbol, (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]>;
onEdgesChangeMiddlewareMap: Map<symbol, (changes: EdgeChange<EdgeType>[]) => EdgeChange<EdgeType>[]>; onEdgesChangeMiddlewareMap: Map<symbol, (changes: EdgeChange<EdgeType>[]) => EdgeChange<EdgeType>[]>;
}; };

View File

@@ -75,7 +75,7 @@
}: SvelteFlowProps<NodeType, EdgeType> & }: SvelteFlowProps<NodeType, EdgeType> &
Omit<HTMLAttributes<HTMLDivElement>, 'onselectionchange'> = $props(); Omit<HTMLAttributes<HTMLDivElement>, 'onselectionchange'> = $props();
// svelte-ignore non_reactive_update // svelte-ignore non_reactive_update, state_referenced_locally
let store = createStore<NodeType, EdgeType>({ let store = createStore<NodeType, EdgeType>({
props, props,
width, width,

View File

@@ -96,6 +96,7 @@
ariaLabelConfig, ariaLabelConfig,
autoPanSpeed, autoPanSpeed,
panOnScrollSpeed, panOnScrollSpeed,
zIndexMode,
...divAttributes ...divAttributes
} = $derived(rest); } = $derived(rest);
/* eslint-enable @typescript-eslint/no-unused-vars */ /* eslint-enable @typescript-eslint/no-unused-vars */

View File

@@ -21,7 +21,8 @@ import type {
OnReconnect, OnReconnect,
OnReconnectStart, OnReconnectStart,
OnReconnectEnd, OnReconnectEnd,
AriaLabelConfig AriaLabelConfig,
ZIndexMode
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -500,4 +501,11 @@ export type SvelteFlowProps<
* Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings. * Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings.
*/ */
ariaLabelConfig?: Partial<AriaLabelConfig>; ariaLabelConfig?: Partial<AriaLabelConfig>;
/**
* 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;
}; };

View File

@@ -122,7 +122,8 @@ export {
type SetCenter, type SetCenter,
type SetViewport, type SetViewport,
type FitBounds, type FitBounds,
type HandleConnection type HandleConnection,
type ZIndexMode
} from '@xyflow/system'; } from '@xyflow/system';
// system utils // system utils

View File

@@ -73,7 +73,9 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
store.nodeLookup, store.nodeLookup,
store.parentLookup, store.parentLookup,
store.domNode, store.domNode,
store.nodeOrigin store.nodeOrigin,
store.nodeExtent,
store.zIndexMode
); );
if (!updatedInternals) { if (!updatedInternals) {
@@ -82,7 +84,8 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
updateAbsolutePositions(store.nodeLookup, store.parentLookup, { updateAbsolutePositions(store.nodeLookup, store.parentLookup, {
nodeOrigin: store.nodeOrigin, nodeOrigin: store.nodeOrigin,
nodeExtent: store.nodeExtent nodeExtent: store.nodeExtent,
zIndexMode: store.zIndexMode
}); });
if (store.fitViewQueued) { if (store.fitViewQueued) {

View File

@@ -36,7 +36,8 @@ import {
type OnReconnect, type OnReconnect,
type OnReconnectStart, type OnReconnectStart,
type OnReconnectEnd, type OnReconnectEnd,
type AriaLabelConfig type AriaLabelConfig,
type ZIndexMode
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -116,13 +117,15 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
panZoom: PanZoomInstance | null = $state.raw(null); panZoom: PanZoomInstance | null = $state.raw(null);
width = $state.raw<number>(signals.width ?? 0); width = $state.raw<number>(signals.width ?? 0);
height = $state.raw<number>(signals.height ?? 0); height = $state.raw<number>(signals.height ?? 0);
zIndexMode = $state.raw<ZIndexMode>(signals.props.zIndexMode ?? 'basic');
nodesInitialized: boolean = $derived.by(() => { nodesInitialized: boolean = $derived.by(() => {
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, { const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
nodeExtent: this.nodeExtent, nodeExtent: this.nodeExtent,
nodeOrigin: this.nodeOrigin, nodeOrigin: this.nodeOrigin,
elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true, elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true,
checkEquality: true checkEquality: true,
zIndexMode: this.zIndexMode
}); });
if (this.fitViewQueued && nodesInitialized) { if (this.fitViewQueued && nodesInitialized) {
@@ -226,7 +229,8 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
connectionMode, connectionMode,
onerror, onerror,
onlyRenderVisibleElements, onlyRenderVisibleElements,
defaultEdgeOptions defaultEdgeOptions,
zIndexMode
} = this; } = this;
let visibleNodes: Map<string, InternalNode<NodeType>>; let visibleNodes: Map<string, InternalNode<NodeType>>;
@@ -239,6 +243,7 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
nodeLookup, nodeLookup,
connectionMode, connectionMode,
elevateEdgesOnSelect: signals.props.elevateEdgesOnSelect ?? true, elevateEdgesOnSelect: signals.props.elevateEdgesOnSelect ?? true,
zIndexMode,
onerror onerror
}; };

View File

@@ -7,7 +7,8 @@ import {
isEdgeVisible, isEdgeVisible,
type NodeLookup, type NodeLookup,
type OnError, type OnError,
type Transform type Transform,
type ZIndexMode
} from '@xyflow/system'; } from '@xyflow/system';
export function getVisibleNodes<NodeType extends Node = Node>( export function getVisibleNodes<NodeType extends Node = Node>(
@@ -42,6 +43,7 @@ export interface EdgeLayoutAllOptions<NodeType extends Node = Node, EdgeType ext
transform: never; transform: never;
width: never; width: never;
height: never; height: never;
zIndexMode: never;
} }
export interface EdgeLayoutOnlyVisibleOptions< export interface EdgeLayoutOnlyVisibleOptions<
@@ -53,6 +55,7 @@ export interface EdgeLayoutOnlyVisibleOptions<
width: number; width: number;
height: number; height: number;
onlyRenderVisible: true; onlyRenderVisible: true;
zIndexMode: ZIndexMode;
} }
export type EdgeLayoutOptions<NodeType extends Node = Node, EdgeType extends Edge = Edge> = export type EdgeLayoutOptions<NodeType extends Node = Node, EdgeType extends Edge = Edge> =
@@ -70,7 +73,8 @@ export function getLayoutedEdges<NodeType extends Node = Node, EdgeType extends
connectionMode, connectionMode,
onerror, onerror,
onlyRenderVisible, onlyRenderVisible,
elevateEdgesOnSelect elevateEdgesOnSelect,
zIndexMode
} = options; } = options;
const layoutedEdges = new Map<string, EdgeLayouted<EdgeType>>(); const layoutedEdges = new Map<string, EdgeLayouted<EdgeType>>();
for (const edge of edges) { for (const edge of edges) {
@@ -134,7 +138,8 @@ export function getLayoutedEdges<NodeType extends Node = Node, EdgeType extends
zIndex: edge.zIndex ?? defaultEdgeOptions.zIndex, zIndex: edge.zIndex ?? defaultEdgeOptions.zIndex,
sourceNode, sourceNode,
targetNode, targetNode,
elevateOnSelect: elevateEdgesOnSelect elevateOnSelect: elevateEdgesOnSelect,
zIndexMode
}), }),
sourceNode, sourceNode,
targetNode, targetNode,

View File

@@ -367,3 +367,13 @@ export type OnBeforeDeleteBase<NodeType extends NodeBase = NodeBase, EdgeType ex
nodes: NodeType[]; nodes: NodeType[];
edges: EdgeType[]; edges: EdgeType[];
}) => Promise<boolean | { nodes: NodeType[]; edges: EdgeType[] }>; }) => Promise<boolean | { nodes: NodeType[]; edges: EdgeType[] }>;
/**
* 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';

View File

@@ -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'; import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB) // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
@@ -28,6 +37,7 @@ export type GetEdgeZIndexParams = {
selected?: boolean; selected?: boolean;
zIndex?: number; zIndex?: number;
elevateOnSelect?: boolean; elevateOnSelect?: boolean;
zIndexMode?: ZIndexMode;
}; };
/** /**
@@ -41,12 +51,19 @@ export function getElevatedEdgeZIndex({
selected = false, selected = false,
zIndex, zIndex,
elevateOnSelect = false, elevateOnSelect = false,
zIndexMode = 'basic',
}: GetEdgeZIndexParams): number { }: GetEdgeZIndexParams): number {
const manualZIndexMode = isManualZIndexMode(zIndexMode);
if (zIndex !== undefined) { if (zIndex !== undefined) {
return zIndex; return zIndex;
} }
const edgeZ = elevateOnSelect && selected ? 1000 : 0;
if (manualZIndexMode) {
return 0;
}
const edgeZ = elevateOnSelect && selected ? 1000 : 0;
const nodeZ = Math.max( const nodeZ = Math.max(
sourceNode.parentId || (elevateOnSelect && sourceNode.selected) ? sourceNode.internals.z : 0, sourceNode.parentId || (elevateOnSelect && sourceNode.selected) ? sourceNode.internals.z : 0,
targetNode.parentId || (elevateOnSelect && targetNode.selected) ? targetNode.internals.z : 0 targetNode.parentId || (elevateOnSelect && targetNode.selected) ? targetNode.internals.z : 0

View File

@@ -1,4 +1,4 @@
import { Handle, HandleConnection, infiniteExtent, NodeHandle, NodeHandleBounds } from '..'; import { Handle, HandleConnection, infiniteExtent, NodeHandleBounds, ZIndexMode } from '..';
import { import {
NodeBase, NodeBase,
CoordinateExtent, CoordinateExtent,
@@ -37,6 +37,7 @@ const defaultOptions = {
nodeOrigin: [0, 0] as NodeOrigin, nodeOrigin: [0, 0] as NodeOrigin,
nodeExtent: infiniteExtent, nodeExtent: infiniteExtent,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
zIndexMode: 'basic' as ZIndexMode,
defaults: {}, defaults: {},
}; };
@@ -112,21 +113,26 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
elevateNodesOnSelect?: boolean; elevateNodesOnSelect?: boolean;
defaults?: Partial<NodeType>; defaults?: Partial<NodeType>;
zIndexMode?: ZIndexMode;
checkEquality?: boolean; checkEquality?: boolean;
}; };
export function isManualZIndexMode(zIndexMode?: ZIndexMode): boolean {
return zIndexMode === 'manual';
}
export function adoptUserNodes<NodeType extends NodeBase>( export function adoptUserNodes<NodeType extends NodeBase>(
nodes: NodeType[], nodes: NodeType[],
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
parentLookup: ParentLookup<InternalNodeBase<NodeType>>, parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
options?: UpdateNodesOptions<NodeType> options: UpdateNodesOptions<NodeType> = {}
): boolean { ): boolean {
const _options = mergeObjects(adoptUserNodesDefaultOptions, options); const _options = mergeObjects(adoptUserNodesDefaultOptions, options);
const rootParentIndex = { i: 0 };
let rootParentIndex = { i: -1 };
let nodesInitialized = nodes.length > 0;
const tmpLookup = new Map(nodeLookup); 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(); nodeLookup.clear();
parentLookup.clear(); parentLookup.clear();
@@ -152,7 +158,7 @@ export function adoptUserNodes<NodeType extends NodeBase>(
positionAbsolute: clampedPosition, 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 // 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), handleBounds: parseHandles(userNode, internalNode),
z: calculateZ(userNode, selectedNodeZ), z: calculateZ(userNode, selectedNodeZ, _options.zIndexMode),
userNode, userNode,
}, },
}; };
@@ -201,10 +207,10 @@ function updateChildNode<NodeType extends NodeBase>(
node: InternalNodeBase<NodeType>, node: InternalNodeBase<NodeType>,
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
parentLookup: ParentLookup<InternalNodeBase<NodeType>>, parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
options?: UpdateNodesOptions<NodeType>, options: UpdateNodesOptions<NodeType>,
rootParentIndex?: { i: number } rootParentIndex?: { i: number }
) { ) {
const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options); const { elevateNodesOnSelect, nodeOrigin, nodeExtent, zIndexMode } = mergeObjects(defaultOptions, options);
const parentId = node.parentId!; const parentId = node.parentId!;
const parentNode = nodeLookup.get(parentId); const parentNode = nodeLookup.get(parentId);
@@ -218,7 +224,12 @@ function updateChildNode<NodeType extends NodeBase>(
updateParentLookup(node, parentLookup); updateParentLookup(node, parentLookup);
// We just want to set the rootParentIndex for the first child // 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.rootParentIndex = ++rootParentIndex.i;
parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT; parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT;
} }
@@ -228,8 +239,8 @@ function updateChildNode<NodeType extends NodeBase>(
rootParentIndex.i = parentNode.internals.rootParentIndex; rootParentIndex.i = parentNode.internals.rootParentIndex;
} }
const selectedNodeZ = elevateNodesOnSelect ? SELECTED_NODE_Z : 0; const selectedNodeZ = elevateNodesOnSelect && !isManualZIndexMode(zIndexMode) ? SELECTED_NODE_Z : 0;
const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ); const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ, zIndexMode);
const { positionAbsolute } = node.internals; const { positionAbsolute } = node.internals;
const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y; const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y;
@@ -246,8 +257,14 @@ function updateChildNode<NodeType extends NodeBase>(
} }
} }
function calculateZ(node: NodeBase, selectedNodeZ: number) { function calculateZ(node: NodeBase, selectedNodeZ: number, zIndexMode: ZIndexMode): number {
return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0); const zIndex = isNumeric(node.zIndex) ? node.zIndex : 0;
if (isManualZIndexMode(zIndexMode)) {
return zIndex;
}
return zIndex + (node.selected ? selectedNodeZ : 0);
} }
function calculateChildXYZ<NodeType extends NodeBase>( function calculateChildXYZ<NodeType extends NodeBase>(
@@ -255,7 +272,8 @@ function calculateChildXYZ<NodeType extends NodeBase>(
parentNode: InternalNodeBase<NodeType>, parentNode: InternalNodeBase<NodeType>,
nodeOrigin: NodeOrigin, nodeOrigin: NodeOrigin,
nodeExtent: CoordinateExtent, nodeExtent: CoordinateExtent,
selectedNodeZ: number selectedNodeZ: number,
zIndexMode: ZIndexMode
) { ) {
const { x: parentX, y: parentY } = parentNode.internals.positionAbsolute; const { x: parentX, y: parentY } = parentNode.internals.positionAbsolute;
const childDimensions = getNodeDimensions(childNode); const childDimensions = getNodeDimensions(childNode);
@@ -274,7 +292,7 @@ function calculateChildXYZ<NodeType extends NodeBase>(
absolutePosition = clampPositionToParent(absolutePosition, childDimensions, parentNode); absolutePosition = clampPositionToParent(absolutePosition, childDimensions, parentNode);
} }
const childZ = calculateZ(childNode, selectedNodeZ); const childZ = calculateZ(childNode, selectedNodeZ, zIndexMode);
const parentZ = parentNode.internals.z ?? 0; const parentZ = parentNode.internals.z ?? 0;
return { return {
@@ -378,7 +396,8 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
parentLookup: ParentLookup<NodeType>, parentLookup: ParentLookup<NodeType>,
domNode: HTMLElement | null, domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin, nodeOrigin?: NodeOrigin,
nodeExtent?: CoordinateExtent nodeExtent?: CoordinateExtent,
zIndexMode?: ZIndexMode
): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } { ): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } {
const viewportNode = domNode?.querySelector('.xyflow__viewport'); const viewportNode = domNode?.querySelector('.xyflow__viewport');
let updatedInternals = false; let updatedInternals = false;
@@ -446,7 +465,7 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
nodeLookup.set(node.id, newNode); nodeLookup.set(node.id, newNode);
if (node.parentId) { if (node.parentId) {
updateChildNode(newNode, nodeLookup, parentLookup, { nodeOrigin }); updateChildNode(newNode, nodeLookup, parentLookup, { nodeOrigin, zIndexMode });
} }
updatedInternals = true; updatedInternals = true;