import { useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { getMarkerId, elementSelectionKeys, getEdgePosition, errorMessages, getElevatedEdgeZIndex, } from '@xyflow/system'; import { useStoreApi, useStore } from '../../hooks/useStore'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { builtinEdgeTypes, nullPosition } from './utils'; import { EdgeUpdateAnchors } from './EdgeUpdateAnchors'; import type { Edge, EdgeWrapperProps } from '../../types'; export function EdgeWrapper({ id, edgesFocusable, edgesReconnectable, elementsSelectable, onClick, onDoubleClick, onContextMenu, onMouseEnter, onMouseMove, onMouseLeave, reconnectRadius, onReconnect, onReconnectStart, onReconnectEnd, rfId, edgeTypes, noPanClassName, onError, disableKeyboardA11y, }: EdgeWrapperProps): JSX.Element | null { let edge = useStore((s) => s.edgeLookup.get(id)!) as EdgeType; const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions); edge = defaultEdgeOptions ? { ...defaultEdgeOptions, ...edge } : edge; let edgeType = edge.type || 'default'; let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType]; if (EdgeComponent === undefined) { onError?.('011', errorMessages['error011'](edgeType)); edgeType = 'default'; EdgeComponent = builtinEdgeTypes.default; } const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); const isReconnectable = typeof onReconnect !== 'undefined' && (edge.reconnectable || (edgesReconnectable && typeof edge.reconnectable === 'undefined')); const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined')); const edgeRef = useRef(null); const [updateHover, setUpdateHover] = useState(false); const [reconnecting, setReconnecting] = useState(false); const store = useStoreApi(); const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore( useCallback( (store) => { const sourceNode = store.nodeLookup.get(edge.source); const targetNode = store.nodeLookup.get(edge.target); if (!sourceNode || !targetNode) { return { zIndex: edge.zIndex, ...nullPosition, }; } const edgePosition = getEdgePosition({ id, sourceNode, targetNode, sourceHandle: edge.sourceHandle || null, targetHandle: edge.targetHandle || null, connectionMode: store.connectionMode, onError, }); const zIndex = getElevatedEdgeZIndex({ selected: edge.selected, zIndex: edge.zIndex, sourceNode, targetNode, elevateOnSelect: store.elevateEdgesOnSelect, }); return { zIndex, ...(edgePosition || nullPosition), }; }, [edge.source, edge.target, edge.sourceHandle, edge.targetHandle, edge.selected, edge.zIndex] ), shallow ); const markerStartUrl = useMemo( () => (edge.markerStart ? `url('#${getMarkerId(edge.markerStart, rfId)}')` : undefined), [edge.markerStart, rfId] ); const markerEndUrl = useMemo( () => (edge.markerEnd ? `url('#${getMarkerId(edge.markerEnd, rfId)}')` : undefined), [edge.markerEnd, rfId] ); if (edge.hidden || sourceX === null || sourceY === null || targetX === null || targetY === null) { return null; } const onEdgeClick = (event: React.MouseEvent): void => { const { addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState(); 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 onEdgeDoubleClick = onDoubleClick ? (event: React.MouseEvent) => { onDoubleClick(event, { ...edge }); } : undefined; const onEdgeContextMenu = onContextMenu ? (event: React.MouseEvent) => { onContextMenu(event, { ...edge }); } : undefined; const onEdgeMouseEnter = onMouseEnter ? (event: React.MouseEvent) => { onMouseEnter(event, { ...edge }); } : undefined; const onEdgeMouseMove = onMouseMove ? (event: React.MouseEvent) => { onMouseMove(event, { ...edge }); } : undefined; const onEdgeMouseLeave = onMouseLeave ? (event: React.MouseEvent) => { onMouseLeave(event, { ...edge }); } : undefined; const onKeyDown = (event: KeyboardEvent) => { if (!disableKeyboardA11y && elementSelectionKeys.includes(event.key) && isSelectable) { const { unselectNodesAndEdges, addSelectedEdges } = store.getState(); const unselect = event.key === 'Escape'; if (unselect) { edgeRef.current?.blur(); unselectNodesAndEdges({ edges: [edge] }); } else { addSelectedEdges([id]); } } }; return ( {!reconnecting && ( )} {isReconnectable && ( edge={edge} isReconnectable={isReconnectable} reconnectRadius={reconnectRadius} onReconnect={onReconnect} onReconnectStart={onReconnectStart} onReconnectEnd={onReconnectEnd} sourceX={sourceX} sourceY={sourceY} targetX={targetX} targetY={targetY} sourcePosition={sourcePosition} targetPosition={targetPosition} setUpdateHover={setUpdateHover} setReconnecting={setReconnecting} /> )} ); }