import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useCallback } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { getMarkerId, elementSelectionKeys, XYHandle, type Connection, getEdgePosition } from '@xyflow/system'; import { useStoreApi, useStore } from '../../hooks/useStore'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { EdgeAnchor } from './EdgeAnchor'; import { getMouseHandler } from './utils'; import type { EdgeProps, WrapEdgeProps } from '../../types'; export default (EdgeComponent: ComponentType) => { const EdgeWrapper = ({ id, className, type, data, onClick, onEdgeDoubleClick, selected, animated, label, labelStyle, labelShowBg, labelBgStyle, labelBgPadding, labelBgBorderRadius, style, source, target, isSelectable, hidden, sourceHandleId, targetHandleId, onContextMenu, onMouseEnter, onMouseMove, onMouseLeave, edgeUpdaterRadius, onEdgeUpdate, onEdgeUpdateStart, onEdgeUpdateEnd, markerEnd, markerStart, rfId, ariaLabel, isFocusable, isUpdatable, pathOptions, interactionWidth, }: WrapEdgeProps): JSX.Element | null => { const edgeRef = useRef(null); const [updateHover, setUpdateHover] = useState(false); const [updating, setUpdating] = useState(false); const store = useStoreApi(); const edgePosition = useStore( useCallback( (state) => { const sourceNode = state.nodeLookup.get(source); const targetNode = state.nodeLookup.get(target); if (!sourceNode || !targetNode) { return null; } return getEdgePosition({ id, sourceNode, targetNode, sourceHandle: sourceHandleId || null, targetHandle: targetHandleId || null, connectionMode: state.connectionMode, onError: state.onError, }); }, [source, target] ), shallow ); const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]); const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]); if (hidden || !edgePosition) { return null; } const onEdgeClick = (event: React.MouseEvent): void => { const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState(); const edge = edges.find((e) => e.id === id); if (!edge) { return; } if (isSelectable) { store.setState({ nodesSelectionActive: false }); if (edge.selected && multiSelectionActive) { unselectNodesAndEdges({ nodes: [], edges: [edge] }); edgeRef.current?.blur(); } else { addSelectedEdges([id]); } } if (onClick) { onClick(event, edge); } }; const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick); const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu); const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter); const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove); const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave); const handleEdgeUpdater = (event: React.MouseEvent, isSourceHandle: boolean) => { // avoid triggering edge updater if mouse btn is not left if (event.button !== 0) { return; } const { autoPanOnConnect, domNode, edges, isValidConnection, connectionMode, connectionRadius, lib, onConnectStart, onConnectEnd, cancelConnection, nodes, panBy, updateConnection, } = store.getState(); const nodeId = isSourceHandle ? target : source; const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null; const handleType = isSourceHandle ? 'target' : 'source'; const isTarget = isSourceHandle; const edge = edges.find((e) => e.id === id)!; setUpdating(true); onEdgeUpdateStart?.(event, edge, handleType); const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => { setUpdating(false); onEdgeUpdateEnd?.(evt, edge, handleType); }; const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); XYHandle.onPointerDown(event.nativeEvent, { autoPanOnConnect, connectionMode, connectionRadius, domNode, handleId, nodeId, nodes, isTarget, edgeUpdaterType: handleType, lib, cancelConnection, panBy, isValidConnection, onConnect: onConnectEdge, onConnectStart, onConnectEnd, onEdgeUpdateEnd: _onEdgeUpdateEnd, updateConnection, getTransform: () => store.getState().transform, }); }; const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent): void => handleEdgeUpdater(event, true); const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent): void => handleEdgeUpdater(event, false); const onEdgeUpdaterMouseEnter = () => setUpdateHover(true); const onEdgeUpdaterMouseOut = () => setUpdateHover(false); const inactive = !isSelectable && !onClick; const onKeyDown = (event: KeyboardEvent) => { if (elementSelectionKeys.includes(event.key) && isSelectable) { const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState(); const unselect = event.key === 'Escape'; if (unselect) { edgeRef.current?.blur(); unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] }); } else { addSelectedEdges([id]); } } }; return ( {!updating && ( )} {isUpdatable && ( <> {(isUpdatable === 'source' || isUpdatable === true) && ( )} {(isUpdatable === 'target' || isUpdatable === true) && ( )} )} ); }; EdgeWrapper.displayName = 'EdgeWrapper'; return memo(EdgeWrapper); };