import { useEffect, useRef, memo } from 'react'; import type { ComponentType, MouseEvent, KeyboardEvent } from 'react'; import cc from 'classcat'; import { useStoreApi } from '../../hooks/useStore'; import { Provider } from '../../contexts/NodeIdContext'; import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions'; import useDrag from '../../hooks/useDrag'; import useUpdateNodePositions from '../../hooks/useUpdateNodePositions'; import { getMouseHandler, handleNodeClick } from './utils'; import { elementSelectionKeys } from '../../utils'; import type { NodeProps, WrapNodeProps, XYPosition } from '../../types'; export const arrowKeyDiffs: Record = { ArrowUp: { x: 0, y: -1 }, ArrowDown: { x: 0, y: 1 }, ArrowLeft: { x: -1, y: 0 }, ArrowRight: { x: 1, y: 0 }, }; export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ id, type, data, xPos, yPos, xPosOrigin, yPosOrigin, selected, onClick, onMouseEnter, onMouseMove, onMouseLeave, onContextMenu, onDoubleClick, style, className, isDraggable, isSelectable, isConnectable, isFocusable, selectNodesOnDrag, sourcePosition, targetPosition, hidden, resizeObserver, dragHandle, zIndex, isParent, noDragClassName, noPanClassName, initialized, disableKeyboardA11y, ariaLabel, rfId, }: WrapNodeProps) => { const store = useStoreApi(); const nodeRef = useRef(null); const prevSourcePosition = useRef(sourcePosition); const prevTargetPosition = useRef(targetPosition); const prevType = useRef(type); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const updatePositions = useUpdateNodePositions(); const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter); const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove); const onMouseLeaveHandler = getMouseHandler(id, store.getState, onMouseLeave); const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu); const onDoubleClickHandler = getMouseHandler(id, store.getState, onDoubleClick); const onSelectNodeHandler = (event: MouseEvent) => { if (isSelectable && (!selectNodesOnDrag || !isDraggable)) { // this handler gets called within the drag start event when selectNodesOnDrag=true handleNodeClick({ id, store, }); } if (onClick) { const node = store.getState().nodeInternals.get(id)!; onClick(event, { ...node }); } }; const onKeyDown = (event: KeyboardEvent) => { const { snapGrid, snapToGrid } = store.getState(); if (elementSelectionKeys.includes(event.key) && isSelectable) { const unselect = event.key === 'Escape'; if (unselect) { nodeRef.current?.blur(); } handleNodeClick({ id, store, unselect, }); } else if ( !disableKeyboardA11y && isDraggable && selected && Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key) ) { store.setState({ ariaLiveMessage: `Moved selected node ${event.key .replace('Arrow', '') .toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`, }); // by default a node moves 5px on each key press, or 20px if shift is pressed // if snap grid is enabled, we use that for the velocity. const xVelo = snapToGrid ? snapGrid[0] : 5; const yVelo = snapToGrid ? snapGrid[1] : 5; const factor = event.shiftKey ? 4 : 1; updatePositions({ x: arrowKeyDiffs[event.key].x * xVelo * factor, y: arrowKeyDiffs[event.key].y * yVelo * factor, }); } }; useEffect(() => { if (nodeRef.current && !hidden) { const currNode = nodeRef.current; resizeObserver?.observe(currNode); return () => resizeObserver?.unobserve(currNode); } }, [hidden]); useEffect(() => { // when the user programmatically changes the source or handle position, we re-initialize the node const typeChanged = prevType.current !== type; const sourcePosChanged = prevSourcePosition.current !== sourcePosition; const targetPosChanged = prevTargetPosition.current !== targetPosition; if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) { if (typeChanged) { prevType.current = type; } if (sourcePosChanged) { prevSourcePosition.current = sourcePosition; } if (targetPosChanged) { prevTargetPosition.current = targetPosition; } store.getState().updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]); } }, [id, type, sourcePosition, targetPosition]); const dragging = useDrag({ nodeRef, disabled: hidden || !isDraggable, noDragClassName, handleSelector: dragHandle, nodeId: id, isSelectable, selectNodesOnDrag, }); if (hidden) { return null; } return (
); }; NodeWrapper.displayName = 'NodeWrapper'; return memo(NodeWrapper); };