Merge branch 'main' into bug-fix-node-positions

This commit is contained in:
Dylan Middendorf
2025-07-09 14:37:26 -04:00
103 changed files with 4114 additions and 3146 deletions
+4
View File
@@ -265,6 +265,10 @@ svg.xy-flow__connectionline {
pointer-events: all;
}
.xy-flow__pane.selection .xy-flow__panel {
pointer-events: none;
}
.xy-flow__panel {
position: absolute;
z-index: 5;
+3 -3
View File
@@ -28,12 +28,12 @@
/* handle styles */
.xy-flow__resize-control.handle {
width: 4px;
height: 4px;
width: 5px;
height: 5px;
border: 1px solid #fff;
border-radius: 1px;
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
transform: translate(-50%, -50%);
translate: -50% -50%;
}
.xy-flow__resize-control.handle.left {
-5
View File
@@ -40,11 +40,6 @@ export type EdgeBase<
* This property sets the width of that invisible path.
*/
interactionWidth?: number;
/**
* A description of the edge's, used for accessibility.
* @default "edge"
*/
ariaRoleDescription?: string;
};
export type SmoothStepPathOptions = {
+46
View File
@@ -13,16 +13,56 @@ import { EdgeBase } from '..';
export type Project = (position: XYPosition) => XYPosition;
/**
* This type is used to define the `onMove` handler.
*/
export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;
export type OnMoveStart = OnMove;
export type OnMoveEnd = OnMove;
/**
* @inline
*/
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/**
* @inline
*/
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/**
* @inline
*/
export type GetZoom = () => number;
/**
* @inline
*/
export type GetViewport = () => Viewport;
/**
* The `SetViewport` function is used to set the viewport of the flow.
*
* @inline
* @param viewport - The viewport to set.
* @param options - Optional parameters to control the animation and easing of the viewport change.
*/
export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/**
* The `SetCenter` function is used to set the center of the flow viewport to a specific position
*
* @inline
* @param x - x coordinate
* @param y - y coordinate
* @param options - Optional parameters to control the animation and easing of the viewport change.
*/
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
/**
* The `FitBounds` function is used to fit the flow viewport to the bounds of the nodes.
*
* @inline
* @param bounds - The bounds to fit the viewport to.
* @param options - Optional parameters to control the animation and easing of the viewport change.
*/
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
/**
@@ -179,10 +219,16 @@ export type ViewportHelperFunctionOptions = {
interpolate?: 'smooth' | 'linear';
};
/**
* @inline
*/
export type SetCenterOptions = ViewportHelperFunctionOptions & {
zoom?: number;
};
/**
* @inline
*/
export type FitBoundsOptions = ViewportHelperFunctionOptions & {
padding?: number;
};
-5
View File
@@ -73,11 +73,6 @@ export type NodeBase<
*/
origin?: NodeOrigin;
handles?: NodeHandle[];
/**
* A description of the node's role, used for accessibility.
* @default "node"
*/
ariaRoleDescription?: string;
measured?: {
width?: number;
height?: number;
+14 -5
View File
@@ -30,21 +30,30 @@ export type GetEdgeZIndexParams = {
elevateOnSelect?: boolean;
};
/**
* Returns the z-index for an edge based on the node it connects and whether it is selected.
* By default, edges are rendered below nodes. This behaviour is different for edges that are
* connected to nodes with a parent, as they are rendered above the parent node.
*/
export function getElevatedEdgeZIndex({
sourceNode,
targetNode,
selected = false,
zIndex = 0,
zIndex,
elevateOnSelect = false,
}: GetEdgeZIndexParams): number {
if (!elevateOnSelect) {
if (zIndex !== undefined) {
return zIndex;
}
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
const edgeZ = elevateOnSelect && selected ? 1000 : 0;
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
const nodeZ = Math.max(
sourceNode.parentId ? sourceNode.internals.z : 0,
targetNode.parentId ? targetNode.internals.z : 0
);
return edgeZ + nodeZ;
}
type IsEdgeVisibleParams = {
+1 -1
View File
@@ -232,7 +232,7 @@ function calculateChildXYZ<NodeType extends NodeBase>(
return {
x: absolutePosition.x,
y: absolutePosition.y,
z: parentZ > childZ ? parentZ : childZ,
z: parentZ >= childZ ? parentZ + 1 : childZ,
};
}
+8 -1
View File
@@ -104,6 +104,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let dragStarted = false;
let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
let abortDrag = false; // prevents unintentional dragging on multitouch
let nodePositionsChanged = false;
// public functions
function update({
@@ -191,6 +192,8 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
dragItem.internals.positionAbsolute = positionAbsolute;
}
nodePositionsChanged = nodePositionsChanged || hasChange;
if (!hasChange) {
return;
}
@@ -294,6 +297,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
containerBounds = domNode?.getBoundingClientRect() || null;
abortDrag = false;
nodePositionsChanged = false;
if (nodeDragThreshold === 0) {
startDrag(event);
@@ -354,7 +358,10 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
if (dragItems.size > 0) {
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
updateNodePositions(dragItems, false);
if (nodePositionsChanged) {
updateNodePositions(dragItems, false);
nodePositionsChanged = false;
}
if (onDragStop || onNodeDragStop || (!nodeId && onSelectionDragStop)) {
const [currentNode, currentNodes] = getEventHandlerParams({
+43 -21
View File
@@ -45,6 +45,7 @@ function onPointerDown(
getTransform,
getFromHandle,
autoPanSpeed,
dragThreshold = 1,
}: OnPointerDownParams
) {
// when xyflow is used inside a shadow root we can't use document
@@ -56,6 +57,7 @@ function onPointerDown(
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
let connectionStarted = false;
if (!containerBounds || !handleType) {
return;
@@ -92,10 +94,9 @@ function onPointerDown(
};
const fromNodeInternal = nodeLookup.get(nodeId)!;
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
const newConnection: ConnectionInProgress = {
let previousConnection: ConnectionInProgress = {
inProgress: true,
isValid: null,
@@ -110,12 +111,30 @@ function onPointerDown(
toNode: null,
};
updateConnection(newConnection);
let previousConnection: ConnectionInProgress = newConnection;
function startConnection() {
connectionStarted = true;
updateConnection(previousConnection);
onConnectStart?.(event, { nodeId, handleId, handleType });
}
onConnectStart?.(event, { nodeId, handleId, handleType });
if (dragThreshold === 0) {
startConnection();
}
function onPointerMove(event: MouseEvent | TouchEvent) {
if (!connectionStarted) {
const { x: evtX, y: evtY } = getEventPosition(event);
const dx = evtX - x;
const dy = evtY - y;
const nextConnectionStarted = dx * dx + dy * dy > dragThreshold * dragThreshold;
if (!nextConnectionStarted) {
return;
}
startConnection();
}
if (!getFromHandle() || !fromHandle) {
onPointerUp(event);
return;
@@ -188,24 +207,27 @@ function onPointerDown(
}
function onPointerUp(event: MouseEvent | TouchEvent) {
if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
if (connectionStarted) {
if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
/*
* it's important to get a fresh reference from the store here
* in order to get the latest state of onConnectEnd
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inProgress, ...connectionState } = previousConnection;
const finalConnectionState = {
...connectionState,
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
};
onConnectEnd?.(event, finalConnectionState);
/*
* it's important to get a fresh reference from the store here
* in order to get the latest state of onConnectEnd
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inProgress, ...connectionState } = previousConnection;
const finalConnectionState = {
...connectionState,
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
};
if (edgeUpdaterType) {
onReconnectEnd?.(event, finalConnectionState);
onConnectEnd?.(event, finalConnectionState);
if (edgeUpdaterType) {
onReconnectEnd?.(event, finalConnectionState);
}
}
cancelConnection();
+1
View File
@@ -37,6 +37,7 @@ export type OnPointerDownParams = {
getTransform: () => Transform;
getFromHandle: () => Handle | null;
autoPanSpeed?: number;
dragThreshold?: number;
};
export type IsValidParams = {
@@ -20,6 +20,7 @@ import {
createZoomOnScrollHandler,
} from './eventhandler';
import { createFilter } from './filter';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { transition } from 'd3-transition';
export type ZoomPanValues = {
+1
View File
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { transition } from 'd3-transition';
import { type D3SelectionInstance, type Viewport } from '../types';