fix(react): ReactFlow comp ref type

This commit is contained in:
moklick
2024-03-04 12:01:26 +01:00
parent 82f40b4a67
commit e972656c3d
5 changed files with 188 additions and 178 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react';
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback, useRef } from 'react';
import {
ReactFlow,
MiniMap,
@@ -45,6 +45,7 @@ const nodeTypes = {
};
const CustomNodeFlow = () => {
const ref = useRef(null);
const [nodes, setNodes] = useState<MyNode[]>([]);
const onNodesChange: OnNodesChange<MyNode> = useCallback(
(changes) =>
@@ -165,6 +166,7 @@ const CustomNodeFlow = () => {
minZoom={0.3}
maxZoom={2}
onBeforeDelete={onBeforeDelete}
ref={ref}
>
<MiniMap<MyNode>
nodeStrokeColor={(n: MyNode): string => {

View File

@@ -3,7 +3,14 @@
* The user can then drag the connection to another handle or node. When the user releases the mouse, we check if the
* connection is valid and if so, we call the onConnect callback.
*/
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import {
memo,
HTMLAttributes,
forwardRef,
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
ForwardedRef,
} from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
@@ -58,190 +65,186 @@ const connectingSelector =
};
};
const HandleComponent = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
type = 'source',
position = Position.Top,
isValidConnection,
isConnectable = true,
isConnectableStart = true,
isConnectableEnd = true,
id,
onConnect,
children,
className,
onMouseDown,
onTouchStart,
...rest
},
ref
) => {
const handleId = id || null;
const isTarget = type === 'target';
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
connectingSelector(nodeId, handleId, type),
shallow
);
function HandleComponent(
{
type = 'source',
position = Position.Top,
isValidConnection,
isConnectable = true,
isConnectableStart = true,
isConnectableEnd = true,
id,
onConnect,
children,
className,
onMouseDown,
onTouchStart,
...rest
}: HandleComponentProps,
ref: ForwardedRef<HTMLDivElement>
) {
const handleId = id || null;
const isTarget = type === 'target';
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
connectingSelector(nodeId, handleId, type),
shallow
);
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
}
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges, setEdges } = store.getState();
setEdges(addEdge(edgeParams, edges));
}
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
};
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges, setEdges } = store.getState();
setEdges(addEdge(edgeParams, edges));
}
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (!nodeId) {
return;
}
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
};
const isMouseTriggered = isMouseEvent(event.nativeEvent);
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (!nodeId) {
return;
}
if (
isConnectableStart &&
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
) {
const currentStore = store.getState();
const isMouseTriggered = isMouseEvent(event.nativeEvent);
if (
isConnectableStart &&
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
) {
const currentStore = store.getState();
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect: currentStore.autoPanOnConnect,
connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode,
nodes: currentStore.nodes,
lib: currentStore.lib,
isTarget,
handleId,
nodeId,
flowId: currentStore.rfId,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform,
});
}
if (isMouseTriggered) {
onMouseDown?.(event as ReactMouseEvent<HTMLDivElement>);
} else {
onTouchStart?.(event as ReactTouchEvent<HTMLDivElement>);
}
};
const onClick = (event: ReactMouseEvent) => {
const {
onClickConnectStart,
onClickConnectEnd,
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
} = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
return;
}
if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib,
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect: currentStore.autoPanOnConnect,
connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode,
nodes: currentStore.nodes,
lib: currentStore.lib,
isTarget,
handleId,
nodeId,
flowId: currentStore.rfId,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform,
});
}
if (isValid && connection) {
onConnectExtended(connection);
}
if (isMouseTriggered) {
onMouseDown?.(event as ReactMouseEvent<HTMLDivElement>);
} else {
onTouchStart?.(event as ReactTouchEvent<HTMLDivElement>);
}
};
onClickConnectEnd?.(event as unknown as MouseEvent);
const onClick = (event: ReactMouseEvent) => {
const {
onClickConnectStart,
onClickConnectEnd,
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
} = store.getState();
store.setState({ connectionClickStartHandle: null });
};
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
return;
}
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${rfId}-${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
noPanClassName,
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connectablestart: isConnectableStart,
connectableend: isConnectableEnd,
clickconnecting: clickConnecting,
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
);
if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
HandleComponent.displayName = 'Handle';
const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib,
});
if (isValid && connection) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionClickStartHandle: null });
};
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${rfId}-${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
noPanClassName,
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connectablestart: isConnectableStart,
connectableend: isConnectableEnd,
clickconnecting: clickConnecting,
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
/**
* The Handle component is the part of a node that can be used to connect nodes.
*/
export const Handle = memo(HandleComponent);
export const Handle = memo(forwardRef(HandleComponent));

View File

@@ -9,7 +9,7 @@ import { StoreUpdater } from '../../components/StoreUpdater';
import { useColorModeClass } from '../../hooks/useColorModeClass';
import { GraphView } from '../GraphView';
import { Wrapper } from './Wrapper';
import type { Edge, Node, ReactFlowProps, ReactFlowRefType } from '../../types';
import type { Edge, Node, ReactFlowProps } from '../../types';
import { defaultViewport as initViewport, defaultNodeOrigin } from './init-values';
const wrapperStyle: CSSProperties = {
@@ -142,7 +142,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
debug,
...rest
}: ReactFlowProps<NodeType, EdgeType>,
ref: ForwardedRef<ReactFlowRefType>
ref: ForwardedRef<HTMLDivElement>
) {
const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode);
@@ -286,4 +286,4 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
);
}
export default forwardRef(ReactFlow) as typeof ReactFlow;
export default forwardRef(ReactFlow);

View File

@@ -509,5 +509,3 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
*/
debug?: boolean;
}
export type ReactFlowRefType = HTMLDivElement;

View File

@@ -1,3 +1,4 @@
import { ComponentType } from 'react';
import {
FitViewParamsBase,
FitViewOptionsBase,
@@ -15,7 +16,13 @@ import {
} from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.';
import { ComponentType } from 'react';
// this is needed, to use generics + forwardRef
declare module 'react' {
function forwardRef<T, P>(
render: (props: P, ref: React.Ref<T>) => React.ReactNode | null
): (props: P & React.RefAttributes<T>) => React.ReactNode | null;
}
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;