Merge branch 'next' into refactor/edge-rendering

This commit is contained in:
moklick
2023-12-17 11:04:32 +01:00
187 changed files with 5192 additions and 2186 deletions
@@ -1,13 +1,13 @@
import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { errorMessages } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges';
import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView';
import type { EdgeTypesWrapped, ReactFlowState } from '../../types';
import type { ReactFlowState } from '../../types';
import EdgeWrapper from '../../components/EdgeWrapper';
type EdgeRendererProps = Pick<
GraphViewProps,
@@ -27,8 +27,8 @@ type EdgeRendererProps = Pick<
| 'elevateEdgesOnSelect'
| 'rfId'
| 'disableKeyboardA11y'
| 'edgeTypes'
> & {
edgeTypes: EdgeTypesWrapped;
elevateEdgesOnSelect: boolean;
children: ReactNode;
};
@@ -72,14 +72,6 @@ const EdgeRenderer = ({
</svg>
{edges.map((edge) => {
let edgeType = edge.type || 'default';
if (!edgeTypes[edgeType]) {
onError?.('011', errorMessages['error011'](edgeType));
edgeType = 'default';
}
const EdgeComponent = edgeTypes[edgeType];
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable =
typeof onEdgeUpdate !== 'undefined' &&
@@ -87,15 +79,16 @@ const EdgeRenderer = ({
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
return (
<EdgeComponent
<EdgeWrapper
key={edge.id}
id={edge.id}
className={cc([edge.className, noPanClassName])}
type={edgeType}
type={edge.type}
data={edge.data}
selected={!!edge.selected}
animated={!!edge.animated}
hidden={!!edge.hidden}
zIndex={edge.zIndex}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
@@ -103,7 +96,6 @@ const EdgeRenderer = ({
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style}
zIndex={edge.zIndex}
source={edge.source}
target={edge.target}
sourceHandleId={edge.sourceHandle}
@@ -127,10 +119,11 @@ const EdgeRenderer = ({
isUpdatable={isUpdatable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={edge.interactionWidth}
onError={onError}
edgeTypes={edgeTypes}
/>
);
})}
{children}
</div>
);
@@ -1,31 +0,0 @@
import type { ComponentType } from 'react';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import type { EdgeProps, EdgeTypes, EdgeTypesWrapped } from '../../types';
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
const standardTypes: EdgeTypesWrapped = {
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeProps>),
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>),
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>),
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdge) as ComponentType<EdgeProps>),
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) as ComponentType<EdgeProps>),
};
const wrappedTypes = {} as EdgeTypesWrapped;
const specialTypes: EdgeTypesWrapped = Object.keys(edgeTypes)
.filter((k) => !['default', 'bezier'].includes(k))
.reduce((res, key) => {
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
@@ -8,19 +8,15 @@ import useOnInitHandler from '../../hooks/useOnInitHandler';
import useViewportSync from '../../hooks/useViewportSync';
import ConnectionLine from '../../components/ConnectionLine';
import type { ReactFlowProps } from '../../types';
import { createNodeTypes } from '../NodeRenderer/utils';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import { useNodeOrEdgeTypes } from './utils';
import useNodeOrEdgeTypesWarning from './useNodeOrEdgeTypesWarning';
export type GraphViewProps = Omit<
ReactFlowProps,
'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
'onSelectionChange' | 'nodes' | 'edges' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
> &
Required<
Pick<
ReactFlowProps,
| 'nodeTypes'
| 'edgeTypes'
| 'selectionKeyCode'
| 'deleteKeyCode'
| 'multiSelectionKeyCode'
@@ -108,8 +104,8 @@ const GraphView = ({
viewport,
onViewportChange,
}: GraphViewProps) => {
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
useNodeOrEdgeTypesWarning(nodeTypes);
useNodeOrEdgeTypesWarning(edgeTypes);
useOnInitHandler(onInit);
useViewportSync(viewport);
@@ -154,7 +150,7 @@ const GraphView = ({
>
<ViewportWrapper>
<EdgeRenderer
edgeTypes={edgeTypesWrapped}
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdate={onEdgeUpdate}
@@ -182,7 +178,7 @@ const GraphView = ({
<div className="react-flow__edgelabel-renderer" />
<NodeRenderer
nodeTypes={nodeTypesWrapped}
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter}
@@ -0,0 +1,27 @@
import { useEffect, useRef } from 'react';
import { errorMessages } from '@xyflow/system';
import type { EdgeTypes, NodeTypes } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
const emptyTypes = {};
/*
* This hook warns the user if node or edgeTypes change.
*/
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: NodeTypes): void;
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: EdgeTypes): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes: any = emptyTypes): any {
const updateCount = useRef(0);
const store = useStoreApi();
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
if (updateCount.current > 1) {
store.getState().onError?.('002', errorMessages['error002']());
}
updateCount.current += 1;
}
}, [nodeOrEdgeTypes]);
}
@@ -1,31 +0,0 @@
import { useMemo, useRef } from 'react';
import { shallow } from 'zustand/shallow';
import { errorMessages } from '@xyflow/system';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
const typesKeysRef = useRef<string[] | null>(null);
const store = useStoreApi();
const typesParsed = useMemo(() => {
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
store.getState().onError?.('002', errorMessages['error002']());
}
typesKeysRef.current = typeKeys;
}
return createTypes(nodeOrEdgeTypes);
}, [nodeOrEdgeTypes]);
return typesParsed;
}
@@ -1,14 +1,15 @@
import { memo, useMemo, useEffect, useRef, type ComponentType } from 'react';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
import { useStore } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils';
import { GraphViewProps } from '../GraphView';
import type { NodeTypesWrapped, ReactFlowState, WrapNodeProps } from '../../types';
import type { ReactFlowState } from '../../types';
import useResizeObserver from './useResizeObserver';
import NodeWrapper from '../../components/NodeWrapper';
type NodeRendererProps = Pick<
export type NodeRendererProps = Pick<
GraphViewProps,
| 'onNodeClick'
| 'onNodeDoubleClick'
@@ -23,126 +24,71 @@ type NodeRendererProps = Pick<
| 'disableKeyboardA11y'
| 'nodeOrigin'
| 'nodeExtent'
> & {
nodeTypes: NodeTypesWrapped;
};
| 'nodeTypes'
>;
const selector = (s: ReactFlowState) => ({
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
onError: s.onError,
});
const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = new Map();
entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string;
updates.set(id, {
id,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
});
});
updateNodeDimensions(updates);
});
resizeObserverRef.current = observer;
return observer;
}, []);
useEffect(() => {
return () => {
resizeObserverRef?.current?.disconnect();
};
}, []);
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow);
const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
const resizeObserver = useResizeObserver();
return (
<div className="react-flow__nodes" style={containerStyle}>
{nodes.map((node) => {
let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) {
onError?.('003', errorMessages['error003'](nodeType));
nodeType = 'default';
}
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
const clampedPosition = props.nodeExtent
? clampPosition(node.positionAbsolute, props.nodeExtent)
: node.positionAbsolute;
const posX = clampedPosition?.x ?? 0;
const posY = clampedPosition?.y ?? 0;
const posOrigin = getPositionWithOrigin({
x: posX,
y: posY,
width: node.width ?? 0,
height: node.height ?? 0,
origin: node.origin || props.nodeOrigin,
});
const initialized = (!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height);
{nodeIds.map((nodeId) => {
return (
<NodeComponent
key={node.id}
id={node.id}
className={node.className}
style={node.style}
sizeWidth={node.size?.width}
sizeHeight={node.size?.height}
type={nodeType}
data={node.data}
sourcePosition={node.sourcePosition || Position.Bottom}
targetPosition={node.targetPosition || Position.Top}
hidden={node.hidden}
xPos={posX}
yPos={posY}
xPosOrigin={posOrigin.x}
yPosOrigin={posOrigin.y}
// The split of responsibilities between NodeRenderer and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For example, when youre dragging a single node, that node gets
// updated multiple times per second. If `NodeRenderer` were to update
// every time, it would have to re-run the `nodes.map()` loop every
// time. This gets pricey with hundreds of nodes, especially if every
// loop cycle does more than just rendering a JSX element!
//
// As a result of this choice, we took the following implementation
// decisions:
// - NodeRenderer subscribes *only* to node IDs and therefore
// rerender *only* when visible nodes are added or removed.
// - NodeRenderer performs all operations the result of which can be
// shared between nodes (such as creating the `ResizeObserver`
// instance, or subscribing to `selector`). This means extra prop
// drilling into `NodeComponentWrapper`, but it means we need to run
// these operations only once instead of once per node.
// - Any operations that youd normally write inside `nodes.map` are
// moved into `NodeComponentWrapper`. This ensures they are
// memorized so if `NodeRenderer` *has* to rerender, it only
// needs to regenerate the list of nodes, nothing else.
<NodeWrapper
key={nodeId}
id={nodeId}
nodeTypes={props.nodeTypes}
nodeExtent={props.nodeExtent}
nodeOrigin={props.nodeOrigin}
onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave}
onContextMenu={props.onNodeContextMenu}
onDoubleClick={props.onNodeDoubleClick}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
isConnectable={isConnectable}
isFocusable={isFocusable}
resizeObserver={resizeObserver}
dragHandle={node.dragHandle}
zIndex={node[internalsSymbol]?.z ?? 0}
isParent={!!node[internalsSymbol]?.isParent}
noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName}
initialized={initialized}
rfId={props.rfId}
disableKeyboardA11y={props.disableKeyboardA11y}
ariaLabel={node.ariaLabel}
resizeObserver={resizeObserver}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable}
elementsSelectable={elementsSelectable}
onError={onError}
/>
);
})}
@@ -0,0 +1,44 @@
import { useEffect, useMemo, useRef } from 'react';
import { ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore';
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
export default function useResizeObserver() {
const updateNodeDimensions = useStore(selector);
const resizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = new Map();
entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string;
updates.set(id, {
id,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
});
});
updateNodeDimensions(updates);
});
resizeObserverRef.current = observer;
return observer;
}, []);
useEffect(() => {
return () => {
resizeObserverRef?.current?.disconnect();
};
}, []);
return resizeObserver;
}
@@ -1,34 +0,0 @@
import type { ComponentType } from 'react';
import type { NodeProps } from '@xyflow/system';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode';
import type { NodeTypes, NodeTypesWrapped } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypesWrapped {
const standardTypes: NodeTypesWrapped = {
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>),
};
const wrappedTypes = {} as NodeTypesWrapped;
const specialTypes: NodeTypesWrapped = Object.keys(nodeTypes)
.filter((k) => !['input', 'default', 'output', 'group'].includes(k))
.reduce((res, key) => {
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
@@ -12,32 +12,14 @@ import {
} from '@xyflow/system';
import Attribution from '../../components/Attribution';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import SelectionListener from '../../components/SelectionListener';
import StoreUpdater from '../../components/StoreUpdater';
import A11yDescriptions from '../../components/A11yDescriptions';
import GraphView from '../GraphView';
import Wrapper from './Wrapper';
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
const defaultNodeTypes: NodeTypes = {
input: InputNode,
default: DefaultNode,
output: OutputNode,
group: GroupNode,
};
const defaultEdgeTypes: EdgeTypes = {
default: BezierEdge,
straight: StraightEdge,
step: StepEdge,
smoothstep: SmoothStepEdge,
simplebezier: SimpleBezierEdge,
};
import type { ReactFlowProps, ReactFlowRefType } from '../../types';
import useColorModeClass from '../../hooks/useColorModeClass';
const initNodeOrigin: NodeOrigin = [0, 0];
const initSnapGrid: [number, number] = [15, 15];
@@ -59,8 +41,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
defaultNodes,
defaultEdges,
className,
nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes,
nodeTypes,
edgeTypes,
onNodeClick,
onEdgeClick,
onInit,
@@ -169,18 +151,20 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onViewportChange,
width,
height,
colorMode = 'light',
...rest
},
ref
) => {
const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode);
return (
<div
{...rest}
style={{ ...style, ...wrapperStyle }}
ref={ref}
className={cc(['react-flow', className])}
className={cc(['react-flow', className, colorModeClassName])}
data-testid="rf__wrapper"
id={id}
>