Merge branch 'main' into enhance/fitView

This commit is contained in:
peterkogo
2025-03-25 13:21:11 +01:00
23 changed files with 274 additions and 74 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@xyflow/react": patch
---
fix: improve TSDoc comments for `BackgroundProps`
+6
View File
@@ -0,0 +1,6 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
---
Prevent onPaneClick when connection is in progress. Closes [#5057](https://github.com/xyflow/xyflow/issues/5057)
+5
View File
@@ -0,0 +1,5 @@
---
"@xyflow/react": patch
---
fix: improve TSDoc comments for `EdgeLabelOptions` and `BaseEdgeProps`
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Add TSDoc annotations for exported edges
+6
View File
@@ -0,0 +1,6 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
---
Hidden nodes are not displayed in the mini map anymore
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Release key even when an inout field is focused
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/system': patch
---
Add center-left and center-right as a panel position
@@ -16,6 +16,7 @@ export enum BackgroundVariant {
* @expand * @expand
*/ */
export type BackgroundProps = { export type BackgroundProps = {
/** When multiple backgrounds are present on the page, each one should have a unique id. */
id?: string; id?: string;
/** Color of the pattern */ /** Color of the pattern */
color?: string; color?: string;
@@ -25,16 +26,31 @@ export type BackgroundProps = {
className?: string; className?: string;
/** Class applied to the pattern */ /** Class applied to the pattern */
patternClassName?: string; 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 '28'
*/
gap?: number | [number, number]; 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; size?: number;
/** Offset of the pattern */ /**
* Offset of the pattern
* @default 2
*/
offset?: number | [number, number]; offset?: number | [number, number];
/** Line width of the Line pattern */ /**
* The stroke thickness used when drawing the pattern.
* @default 1
*/
lineWidth?: number; lineWidth?: number;
/** /**
* Variant of the pattern * Variant of the pattern
* @default BackgroundVariant.Dots
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross * @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
* 'lines', 'dots', 'cross' * 'lines', 'dots', 'cross'
*/ */
@@ -15,6 +15,8 @@ import type { MiniMapProps } from './types';
const defaultWidth = 200; const defaultWidth = 200;
const defaultHeight = 150; const defaultHeight = 150;
const filterHidden = (node: Node) => !node.hidden;
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
const viewBB: Rect = { const viewBB: Rect = {
x: -s.transform[0] / s.transform[2], x: -s.transform[0] / s.transform[2],
@@ -25,7 +27,10 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, 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, rfId: s.rfId,
panZoom: s.panZoom, panZoom: s.panZoom,
translateExtent: s.translateExtent, translateExtent: s.translateExtent,
@@ -113,16 +118,16 @@ function MiniMapComponent<NodeType extends Node = Node>({
const onSvgClick = onClick const onSvgClick = onClick
? (event: MouseEvent) => { ? (event: MouseEvent) => {
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0]; const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y }); onClick(event, { x, y });
} }
: undefined; : undefined;
const onSvgNodeClick = onNodeClick const onSvgNodeClick = onNodeClick
? useCallback((event: MouseEvent, nodeId: string) => { ? useCallback((event: MouseEvent, nodeId: string) => {
const node = store.getState().nodeLookup.get(nodeId)!; const node = store.getState().nodeLookup.get(nodeId)!;
onNodeClick(event, node); onNodeClick(event, node);
}, []) }, [])
: undefined; : undefined;
return ( return (
@@ -10,37 +10,38 @@ const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__e
* Edges are SVG-based. If you want to render more complex labels you can use the * Edges are SVG-based. If you want to render more complex labels you can use the
* `<EdgeLabelRenderer />` component to access a div based renderer. This component * `<EdgeLabelRenderer />` component to access a div based renderer. This component
* is a portal that renders the label in a `<div />` that is positioned on top of * is a portal that renders the label in a `<div />` that is positioned on top of
* the edges. You can see an example usage of the component in the [edge label renderer](/examples/edges/edge-label-renderer) example. * the edges. You can see an example usage of the component in the
* [edge label renderer example](/examples/edges/edge-label-renderer).
* @public * @public
* *
* @example * @example
*```jsx * ```jsx
*import React from 'react'; * import React from 'react';
*import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react'; * import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react';
* *
*export function CustomEdge({ id, data, ...props }) { * export function CustomEdge({ id, data, ...props }) {
* const [edgePath, labelX, labelY] = getBezierPath(props); * const [edgePath, labelX, labelY] = getBezierPath(props);
* *
* return ( * return (
* <> * <>
* <BaseEdge id={id} path={edgePath} /> * <BaseEdge id={id} path={edgePath} />
* <EdgeLabelRenderer> * <EdgeLabelRenderer>
* <div * <div
* style={{ * style={{
* position: 'absolute', * position: 'absolute',
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, * transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
* background: '#ffcc00', * background: '#ffcc00',
* padding: 10, * padding: 10,
* }} * }}
* className="nodrag nopan" * className="nodrag nopan"
* > * >
* {data.label} * {data.label}
* </div> * </div>
* </EdgeLabelRenderer> * </EdgeLabelRenderer>
* </> * </>
* ); * );
*}; * };
*``` * ```
* *
* @remarks The `<EdgeLabelRenderer />` has no pointer events by default. If you want to * @remarks The `<EdgeLabelRenderer />` has no pointer events by default. If you want to
* add mouse interactions you need to set the style `pointerEvents: all` and add * add mouse interactions you need to set the style `pointerEvents: all` and add
@@ -61,7 +61,34 @@ function createBezierEdge(params: { isInternal: boolean }) {
); );
} }
/**
* Component that can be used inside a custom edge to render a bezier curve.
*
* @public
* @example
*
* ```tsx
* import { BezierEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <BezierEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const BezierEdge = createBezierEdge({ isInternal: false }); const BezierEdge = createBezierEdge({ isInternal: false });
/**
* @internal
*/
const BezierEdgeInternal = createBezierEdge({ isInternal: true }); const BezierEdgeInternal = createBezierEdge({ isInternal: true });
BezierEdge.displayName = 'BezierEdge'; BezierEdge.displayName = 'BezierEdge';
@@ -27,6 +27,9 @@ export interface EdgeAnchorProps extends SVGAttributes<SVGGElement> {
const EdgeUpdaterClassName = 'react-flow__edgeupdater'; const EdgeUpdaterClassName = 'react-flow__edgeupdater';
/**
* @internal
*/
export function EdgeAnchor({ export function EdgeAnchor({
position, position,
centerX, centerX,
@@ -77,26 +77,26 @@ EdgeTextComponent.displayName = 'EdgeText';
* You can use the `<EdgeText />` component as a helper component to display text * You can use the `<EdgeText />` component as a helper component to display text
* within your custom edges. * within your custom edges.
* *
*@public * @public
* *
*@example * @example
*```jsx * ```jsx
*import { EdgeText } from '@xyflow/react'; * import { EdgeText } from '@xyflow/react';
* *
*export function CustomEdgeLabel({ label }) { * export function CustomEdgeLabel({ label }) {
* return ( * return (
* <EdgeText * <EdgeText
* x={100} * x={100}
* y={100} * y={100}
* label={label} * label={label}
* labelStyle={{ fill: 'white' }} * labelStyle={{ fill: 'white' }}
* labelShowBg * labelShowBg
* labelBgStyle={{ fill: 'red' }} * labelBgStyle={{ fill: 'red' }}
* labelBgPadding={[2, 4]} * labelBgPadding={[2, 4]}
* labelBgBorderRadius={2} * labelBgBorderRadius={2}
* /> * />
* ); * );
*} * }
*``` *```
*/ */
export const EdgeText = memo(EdgeTextComponent); export const EdgeText = memo(EdgeTextComponent);
@@ -62,7 +62,34 @@ function createSmoothStepEdge(params: { isInternal: boolean }) {
); );
} }
/**
* Component that can be used inside a custom edge to render a smooth step edge.
*
* @public
* @example
*
* ```tsx
* import { SmoothStepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <SmoothStepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false }); const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
/**
* @internal
*/
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true }); const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
SmoothStepEdge.displayName = 'SmoothStepEdge'; SmoothStepEdge.displayName = 'SmoothStepEdge';
@@ -21,7 +21,34 @@ function createStepEdge(params: { isInternal: boolean }) {
}); });
} }
/**
* Component that can be used inside a custom edge to render a step edge.
*
* @public
* @example
*
* ```tsx
* import { StepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <StepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const StepEdge = createStepEdge({ isInternal: false }); const StepEdge = createStepEdge({ isInternal: false });
/**
* @internal
*/
const StepEdgeInternal = createStepEdge({ isInternal: true }); const StepEdgeInternal = createStepEdge({ isInternal: true });
StepEdge.displayName = 'StepEdge'; StepEdge.displayName = 'StepEdge';
@@ -50,7 +50,32 @@ function createStraightEdge(params: { isInternal: boolean }) {
); );
} }
/**
* Component that can be used inside a custom edge to render a straight line.
*
* @public
* @example
*
* ```tsx
* import { StraightEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY }) {
* return (
* <StraightEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* />
* );
* }
* ```
*/
const StraightEdge = createStraightEdge({ isInternal: false }); const StraightEdge = createStraightEdge({ isInternal: false });
/**
* @internal
*/
const StraightEdgeInternal = createStraightEdge({ isInternal: true }); const StraightEdgeInternal = createStraightEdge({ isInternal: true });
StraightEdge.displayName = 'StraightEdge'; StraightEdge.displayName = 'StraightEdge';
+4 -2
View File
@@ -61,6 +61,7 @@ const wrapHandler = (
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive, userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
connectionInProgress: s.connection.inProgress,
dragging: s.paneDragging, dragging: s.paneDragging,
}); });
@@ -81,7 +82,7 @@ export function Pane({
children, children,
}: PaneProps) { }: PaneProps) {
const store = useStoreApi(); const store = useStoreApi();
const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow); const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive); const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
const container = useRef<HTMLDivElement | null>(null); const container = useRef<HTMLDivElement | null>(null);
@@ -96,7 +97,8 @@ export function Pane({
const onClick = (event: ReactMouseEvent) => { const onClick = (event: ReactMouseEvent) => {
// We prevent click events when the user let go of the selectionKey during a selection // We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress.current) { // We also prevent click events when a connection is in progress
if (selectionInProgress.current || connectionInProgress) {
selectionInProgress.current = false; selectionInProgress.current = false;
return; return;
} }
-7
View File
@@ -106,13 +106,6 @@ export function useKeyPress(
}; };
const upHandler = (event: KeyboardEvent) => { const upHandler = (event: KeyboardEvent) => {
const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event);
if (preventAction) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch); const keyOrCode = useKeyOrCode(event.code, keysToWatch);
if (isMatchingKey(keyCodes, pressedKeys.current, true)) { if (isMatchingKey(keyCodes, pressedKeys.current, true)) {
+14 -3
View File
@@ -23,7 +23,11 @@ import { EdgeTypes, InternalNode, Node } from '.';
* @inline * @inline
*/ */
export type EdgeLabelOptions = { export type EdgeLabelOptions = {
label?: string | ReactNode; /**
* The label or custom element to render along the edge. This is commonly a text label or some
* custom controls.
*/
label?: ReactNode;
labelStyle?: CSSProperties; labelStyle?: CSSProperties;
labelShowBg?: boolean; labelShowBg?: boolean;
labelBgStyle?: CSSProperties; labelBgStyle?: CSSProperties;
@@ -136,13 +140,20 @@ export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
*/ */
export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd'> & export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd'> &
EdgeLabelOptions & { EdgeLabelOptions & {
/** Additional padding where interacting with an edge is still possible */ /**
* The width of the invisible area around the edge that the user can interact with. This is
* useful for making the edge easier to click or hover over.
*/
interactionWidth?: number; interactionWidth?: number;
/** The x position of edge label */ /** The x position of edge label */
labelX?: number; labelX?: number;
/** The y position of edge label */ /** The y position of edge label */
labelY?: number; labelY?: number;
/** SVG path of the edge */ /**
* The SVG path string that defines the edge. This should look something like
* `'M 0 0 L 100 100'` for a simple line. The utility functions like `getSimpleBezierEdge` can
* be used to generate this string for you.
*/
path: string; path: string;
}; };
@@ -63,7 +63,8 @@
selectionKeyPressed, selectionKeyPressed,
selectionMode, selectionMode,
panActivationKeyPressed, panActivationKeyPressed,
unselectNodesAndEdges unselectNodesAndEdges,
connection,
} = useStore(); } = useStore();
let container: HTMLDivElement; let container: HTMLDivElement;
@@ -80,7 +81,8 @@
function onClick(event: MouseEvent | TouchEvent) { function onClick(event: MouseEvent | TouchEvent) {
// We prevent click events when the user let go of the selectionKey during a selection // We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress) { // We also prevent click events when a connection is in progress
if (selectionInProgress || $connection.inProgress) {
selectionInProgress = false; selectionInProgress = false;
return; return;
} }
@@ -76,7 +76,12 @@
$: { $: {
boundingRect = boundingRect =
$nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds($nodeLookup), viewBB) : viewBB; $nodeLookup.size > 0
? getBoundsOfRects(
getInternalNodesBounds($nodeLookup, { filter: (n) => !n.hidden }),
viewBB
)
: viewBB;
$nodes; $nodes;
} }
+14 -3
View File
@@ -278,6 +278,14 @@ svg.xy-flow__connectionline {
bottom: 0; bottom: 0;
} }
&.top,
&.bottom {
&.center {
left: 50%;
transform: translateX(-50%);
}
}
&.left { &.left {
left: 0; left: 0;
} }
@@ -286,9 +294,12 @@ svg.xy-flow__connectionline {
right: 0; right: 0;
} }
&.center { &.left,
left: 50%; &.right {
transform: translateX(-50%); &.center {
top: 50%;
transform: translateY(-50%);
}
} }
} }
+9 -1
View File
@@ -169,7 +169,15 @@ export type UpdateNodeInternals = (nodeId: string | string[]) => void;
* *
* @public * @public
*/ */
export type PanelPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; export type PanelPosition =
| 'top-left'
| 'top-center'
| 'top-right'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right'
| 'center-left'
| 'center-right';
export type ProOptions = { export type ProOptions = {
account?: string; account?: string;