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

View File

@@ -0,0 +1,5 @@
---
"@xyflow/react": patch
---
fix: improve TSDoc comments for `BackgroundProps`

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)

View File

@@ -0,0 +1,5 @@
---
"@xyflow/react": patch
---
fix: improve TSDoc comments for `EdgeLabelOptions` and `BaseEdgeProps`

View File

@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Add TSDoc annotations for exported edges

View File

@@ -0,0 +1,6 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
---
Hidden nodes are not displayed in the mini map anymore

View File

@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Release key even when an inout field is focused

View File

@@ -0,0 +1,5 @@
---
'@xyflow/system': patch
---
Add center-left and center-right as a panel position

View File

@@ -16,6 +16,7 @@ export enum BackgroundVariant {
* @expand
*/
export type BackgroundProps = {
/** When multiple backgrounds are present on the page, each one should have a unique id. */
id?: string;
/** Color of the pattern */
color?: string;
@@ -25,16 +26,31 @@ export type BackgroundProps = {
className?: string;
/** Class applied to the pattern */
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];
/** 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;
/** Offset of the pattern */
/**
* Offset of the pattern
* @default 2
*/
offset?: number | [number, number];
/** Line width of the Line pattern */
/**
* The stroke thickness used when drawing the pattern.
* @default 1
*/
lineWidth?: number;
/**
* Variant of the pattern
* @default BackgroundVariant.Dots
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
* 'lines', 'dots', 'cross'
*/

View File

@@ -15,6 +15,8 @@ import type { MiniMapProps } from './types';
const defaultWidth = 200;
const defaultHeight = 150;
const filterHidden = (node: Node) => !node.hidden;
const selector = (s: ReactFlowState) => {
const viewBB: Rect = {
x: -s.transform[0] / s.transform[2],
@@ -25,7 +27,10 @@ const selector = (s: ReactFlowState) => {
return {
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,
panZoom: s.panZoom,
translateExtent: s.translateExtent,
@@ -113,16 +118,16 @@ function MiniMapComponent<NodeType extends Node = Node>({
const onSvgClick = onClick
? (event: MouseEvent) => {
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y });
}
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y });
}
: undefined;
const onSvgNodeClick = onNodeClick
? useCallback((event: MouseEvent, nodeId: string) => {
const node = store.getState().nodeLookup.get(nodeId)!;
onNodeClick(event, node);
}, [])
const node = store.getState().nodeLookup.get(nodeId)!;
onNodeClick(event, node);
}, [])
: undefined;
return (

View File

@@ -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
* `<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
* 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
*
* @example
*```jsx
*import React from 'react';
*import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react';
* ```jsx
* import React from 'react';
* import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react';
*
*export function CustomEdge({ id, data, ...props }) {
* const [edgePath, labelX, labelY] = getBezierPath(props);
* export function CustomEdge({ id, data, ...props }) {
* const [edgePath, labelX, labelY] = getBezierPath(props);
*
* return (
* <>
* <BaseEdge id={id} path={edgePath} />
* <EdgeLabelRenderer>
* <div
* style={{
* position: 'absolute',
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
* background: '#ffcc00',
* padding: 10,
* }}
* className="nodrag nopan"
* >
* {data.label}
* </div>
* </EdgeLabelRenderer>
* </>
* );
*};
*```
* return (
* <>
* <BaseEdge id={id} path={edgePath} />
* <EdgeLabelRenderer>
* <div
* style={{
* position: 'absolute',
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
* background: '#ffcc00',
* padding: 10,
* }}
* className="nodrag nopan"
* >
* {data.label}
* </div>
* </EdgeLabelRenderer>
* </>
* );
* };
* ```
*
* @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

View File

@@ -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 });
/**
* @internal
*/
const BezierEdgeInternal = createBezierEdge({ isInternal: true });
BezierEdge.displayName = 'BezierEdge';

View File

@@ -27,6 +27,9 @@ export interface EdgeAnchorProps extends SVGAttributes<SVGGElement> {
const EdgeUpdaterClassName = 'react-flow__edgeupdater';
/**
* @internal
*/
export function EdgeAnchor({
position,
centerX,

View File

@@ -77,26 +77,26 @@ EdgeTextComponent.displayName = 'EdgeText';
* You can use the `<EdgeText />` component as a helper component to display text
* within your custom edges.
*
*@public
* @public
*
*@example
*```jsx
*import { EdgeText } from '@xyflow/react';
* @example
* ```jsx
* import { EdgeText } from '@xyflow/react';
*
*export function CustomEdgeLabel({ label }) {
* return (
* <EdgeText
* x={100}
* y={100}
* label={label}
* labelStyle={{ fill: 'white' }}
* labelShowBg
* labelBgStyle={{ fill: 'red' }}
* labelBgPadding={[2, 4]}
* labelBgBorderRadius={2}
* />
* );
*}
* export function CustomEdgeLabel({ label }) {
* return (
* <EdgeText
* x={100}
* y={100}
* label={label}
* labelStyle={{ fill: 'white' }}
* labelShowBg
* labelBgStyle={{ fill: 'red' }}
* labelBgPadding={[2, 4]}
* labelBgBorderRadius={2}
* />
* );
* }
*```
*/
export const EdgeText = memo(EdgeTextComponent);

View File

@@ -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 });
/**
* @internal
*/
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
SmoothStepEdge.displayName = 'SmoothStepEdge';

View File

@@ -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 });
/**
* @internal
*/
const StepEdgeInternal = createStepEdge({ isInternal: true });
StepEdge.displayName = 'StepEdge';

View File

@@ -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 });
/**
* @internal
*/
const StraightEdgeInternal = createStraightEdge({ isInternal: true });
StraightEdge.displayName = 'StraightEdge';

View File

@@ -61,6 +61,7 @@ const wrapHandler = (
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
connectionInProgress: s.connection.inProgress,
dragging: s.paneDragging,
});
@@ -81,7 +82,7 @@ export function Pane({
children,
}: PaneProps) {
const store = useStoreApi();
const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow);
const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
const container = useRef<HTMLDivElement | null>(null);
@@ -96,7 +97,8 @@ export function Pane({
const onClick = (event: ReactMouseEvent) => {
// 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;
return;
}

View File

@@ -106,13 +106,6 @@ export function useKeyPress(
};
const upHandler = (event: KeyboardEvent) => {
const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event);
if (preventAction) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
if (isMatchingKey(keyCodes, pressedKeys.current, true)) {

View File

@@ -23,7 +23,11 @@ import { EdgeTypes, InternalNode, Node } from '.';
* @inline
*/
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;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
@@ -136,13 +140,20 @@ export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
*/
export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd'> &
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;
/** The x position of edge label */
labelX?: number;
/** The y position of edge label */
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;
};

View File

@@ -63,7 +63,8 @@
selectionKeyPressed,
selectionMode,
panActivationKeyPressed,
unselectNodesAndEdges
unselectNodesAndEdges,
connection,
} = useStore();
let container: HTMLDivElement;
@@ -80,7 +81,8 @@
function onClick(event: MouseEvent | TouchEvent) {
// 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;
return;
}

View File

@@ -76,7 +76,12 @@
$: {
boundingRect =
$nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds($nodeLookup), viewBB) : viewBB;
$nodeLookup.size > 0
? getBoundsOfRects(
getInternalNodesBounds($nodeLookup, { filter: (n) => !n.hidden }),
viewBB
)
: viewBB;
$nodes;
}

View File

@@ -278,6 +278,14 @@ svg.xy-flow__connectionline {
bottom: 0;
}
&.top,
&.bottom {
&.center {
left: 50%;
transform: translateX(-50%);
}
}
&.left {
left: 0;
}
@@ -286,9 +294,12 @@ svg.xy-flow__connectionline {
right: 0;
}
&.center {
left: 50%;
transform: translateX(-50%);
&.left,
&.right {
&.center {
top: 50%;
transform: translateY(-50%);
}
}
}

View File

@@ -169,7 +169,15 @@ export type UpdateNodeInternals = (nodeId: string | string[]) => void;
*
* @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 = {
account?: string;