diff --git a/packages/react/src/components/Nodes/NodeWrapper.tsx b/packages/react/src/components/Nodes/NodeWrapper.tsx new file mode 100644 index 00000000..22dddceb --- /dev/null +++ b/packages/react/src/components/Nodes/NodeWrapper.tsx @@ -0,0 +1,263 @@ +import { useEffect, useRef, memo, type MouseEvent, type KeyboardEvent, ComponentType } from 'react'; +import cc from 'classcat'; +import { + clampPosition, + elementSelectionKeys, + errorMessages, + getPositionWithOrigin, + internalsSymbol, + isInputDOMNode, + NodeProps, + type XYPosition, +} from '@xyflow/system'; + +import { useStore, 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 { handleNodeClick } from './utils'; +import type { NodeWrapperProps } 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 }, +}; + +const NodeWrapper = ({ + id, + onClick, + onMouseEnter, + onMouseMove, + onMouseLeave, + onContextMenu, + onDoubleClick, + nodesDraggable, + elementsSelectable, + nodesConnectable, + nodesFocusable, + resizeObserver, + noDragClassName, + noPanClassName, + disableKeyboardA11y, + rfId, + nodeTypes, + nodeExtent, + nodeOrigin, + onError, +}: NodeWrapperProps) => { + const node = useStore((s) => s.nodeLookup.get(id)!); + + let nodeType = node.type || 'default'; + + if (!nodeTypes[nodeType]) { + onError?.('003', errorMessages['error003'](nodeType)); + nodeType = 'default'; + } + + 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 store = useStoreApi(); + const nodeRef = useRef(null); + const prevSourcePosition = useRef(node.sourcePosition); + const prevTargetPosition = useRef(node.targetPosition); + const prevType = useRef(nodeType); + + const updatePositions = useUpdateNodePositions(); + + useEffect(() => { + if (nodeRef.current && !node.hidden) { + const currNode = nodeRef.current; + resizeObserver?.observe(currNode); + + return () => resizeObserver?.unobserve(currNode); + } + }, [node.hidden]); + + useEffect(() => { + // when the user programmatically changes the source or handle position, we re-initialize the node + const typeChanged = prevType.current !== nodeType; + const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition; + const targetPosChanged = prevTargetPosition.current !== node.targetPosition; + + if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) { + if (typeChanged) { + prevType.current = nodeType; + } + if (sourcePosChanged) { + prevSourcePosition.current = node.sourcePosition; + } + if (targetPosChanged) { + prevTargetPosition.current = node.targetPosition; + } + store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]])); + } + }, [id, nodeType, node.sourcePosition, node.targetPosition]); + + const dragging = useDrag({ + nodeRef, + disabled: node.hidden || !isDraggable, + noDragClassName, + handleSelector: node.dragHandle, + nodeId: id, + isSelectable, + }); + + if (node.hidden) { + return null; + } + + const NodeComponent = (nodeTypes[nodeType] || nodeTypes.default) as ComponentType; + const width = node.width ?? undefined; + const height = node.height ?? undefined; + const computedWidth = node.computed?.width; + const computedHeight = node.computed?.height; + const positionAbsolute = nodeExtent + ? clampPosition(node.computed?.positionAbsolute, nodeExtent) + : node.computed?.positionAbsolute || { x: 0, y: 0 }; + const positionAbsoluteOrigin = getPositionWithOrigin({ + x: positionAbsolute.x, + y: positionAbsolute.y, + width: computedWidth ?? width ?? 0, + height: computedHeight ?? height ?? 0, + origin: node.origin || nodeOrigin, + }); + const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); + const zIndex = node[internalsSymbol]?.z ?? 0; + const isParent = !!node[internalsSymbol]?.isParent; + const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; + + const onMouseEnterHandler = + onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node }); + const onMouseMoveHandler = + onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node }); + const onMouseLeaveHandler = + onMouseLeave === undefined ? undefined : (event: MouseEvent) => onMouseLeave(event, { ...node }); + const onContextMenuHandler = + onContextMenu === undefined ? undefined : (event: MouseEvent) => onContextMenu(event, { ...node }); + const onDoubleClickHandler = + onDoubleClick === undefined ? undefined : (event: MouseEvent) => onDoubleClick(event, { ...node }); + + const onSelectNodeHandler = (event: MouseEvent) => { + const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); + + if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) { + // this handler gets called by XYDrag on drag start when selectNodesOnDrag=true + // here we only need to call it when selectNodesOnDrag=false + handleNodeClick({ + id, + store, + nodeRef, + }); + } + + if (onClick) { + onClick(event, { ...node }); + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (isInputDOMNode(event.nativeEvent)) { + return; + } + + if (elementSelectionKeys.includes(event.key) && isSelectable) { + const unselect = event.key === 'Escape'; + + handleNodeClick({ + id, + store, + unselect, + nodeRef, + }); + } else if ( + !disableKeyboardA11y && + isDraggable && + node.selected && + Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key) + ) { + store.setState({ + ariaLiveMessage: `Moved selected node ${event.key + .replace('Arrow', '') + .toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`, + }); + + updatePositions({ + x: arrowKeyDiffs[event.key].x, + y: arrowKeyDiffs[event.key].y, + isShiftPressed: event.shiftKey, + }); + } + }; + + return ( +
+ + + +
+ ); +}; + +NodeWrapper.displayName = 'NodeWrapper'; + +export default memo(NodeWrapper); diff --git a/packages/react/src/components/Nodes/utils.ts b/packages/react/src/components/Nodes/utils.ts index b9a82189..26b1f246 100644 --- a/packages/react/src/components/Nodes/utils.ts +++ b/packages/react/src/components/Nodes/utils.ts @@ -1,22 +1,9 @@ -import type { MouseEvent, RefObject } from 'react'; +import type { RefObject } from 'react'; import type { StoreApi } from 'zustand'; -import type { Node, ReactFlowState } from '../../types'; +import type { ReactFlowState } from '../../types'; import { errorMessages } from '@xyflow/system'; -export function getMouseHandler( - id: string, - getState: StoreApi['getState'], - handler?: (event: MouseEvent, node: Node) => void -) { - return handler === undefined - ? handler - : (event: MouseEvent) => { - const node = getState().nodeLookup.get(id)!; - handler(event, { ...node }); - }; -} - // this handler is called by // 1. the click handler when node is not draggable or selectNodesOnDrag = false // or diff --git a/packages/react/src/components/Nodes/wrapNode.tsx b/packages/react/src/components/Nodes/wrapNode.tsx deleted file mode 100644 index 330364c6..00000000 --- a/packages/react/src/components/Nodes/wrapNode.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { useEffect, useRef, memo, type ComponentType, type MouseEvent, type KeyboardEvent } from 'react'; -import cc from 'classcat'; -import { elementSelectionKeys, isInputDOMNode, type NodeProps, type XYPosition } from '@xyflow/system'; - -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 type { WrapNodeProps } 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, - xPosOrigin, - yPosOrigin, - positionAbsoluteX, - positionAbsoluteY, - selected, - onClick, - onMouseEnter, - onMouseMove, - onMouseLeave, - onContextMenu, - onDoubleClick, - style, - className, - isDraggable, - isSelectable, - isConnectable, - isFocusable, - sourcePosition, - targetPosition, - hidden, - resizeObserver, - dragHandle, - zIndex, - isParent, - noDragClassName, - noPanClassName, - initialized, - disableKeyboardA11y, - ariaLabel, - rfId, - width, - height, - }: 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) => { - const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); - - if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) { - // this handler gets called by XYDrag on drag start when selectNodesOnDrag=true - // here we only need to call it when selectNodesOnDrag=false - handleNodeClick({ - id, - store, - nodeRef, - }); - } - - if (onClick) { - const node = store.getState().nodes.find((n) => n.id === id)!; - onClick(event, { ...node }); - } - }; - - const onKeyDown = (event: KeyboardEvent) => { - if (isInputDOMNode(event.nativeEvent)) { - return; - } - - if (elementSelectionKeys.includes(event.key) && isSelectable) { - const unselect = event.key === 'Escape'; - - handleNodeClick({ - id, - store, - unselect, - nodeRef, - }); - } 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: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`, - }); - - updatePositions({ - x: arrowKeyDiffs[event.key].x, - y: arrowKeyDiffs[event.key].y, - isShiftPressed: event.shiftKey, - }); - } - }; - - 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(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]])); - } - }, [id, type, sourcePosition, targetPosition]); - - const dragging = useDrag({ - nodeRef, - disabled: hidden || !isDraggable, - noDragClassName, - handleSelector: dragHandle, - nodeId: id, - isSelectable, - }); - - if (hidden) { - return null; - } - - return ( -
- - - -
- ); - }; - - NodeWrapper.displayName = 'NodeWrapper'; - - return memo(NodeWrapper); -}; diff --git a/packages/react/src/container/NodeRenderer/NodeComponentWrapper.tsx b/packages/react/src/container/NodeRenderer/NodeComponentWrapper.tsx deleted file mode 100644 index f833ff08..00000000 --- a/packages/react/src/container/NodeRenderer/NodeComponentWrapper.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { memo, type ComponentType } from 'react'; -import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system'; - -import { useStore } from '../../hooks/useStore'; -import type { ReactFlowState, WrapNodeProps } from '../../types'; -import { NodeRendererProps } from '.'; - -function NodeComponentWrapper(props: { - id: string; - nodeExtent: NodeRendererProps['nodeExtent']; - nodeTypes: NodeRendererProps['nodeTypes']; - nodeOrigin: NodeRendererProps['nodeOrigin']; - onNodeClick: NodeRendererProps['onNodeClick']; - onNodeMouseEnter: NodeRendererProps['onNodeMouseEnter']; - onNodeMouseMove: NodeRendererProps['onNodeMouseMove']; - onNodeMouseLeave: NodeRendererProps['onNodeMouseLeave']; - onNodeContextMenu: NodeRendererProps['onNodeContextMenu']; - onNodeDoubleClick: NodeRendererProps['onNodeDoubleClick']; - noDragClassName: NodeRendererProps['noDragClassName']; - noPanClassName: NodeRendererProps['noPanClassName']; - rfId: NodeRendererProps['rfId']; - disableKeyboardA11y: NodeRendererProps['disableKeyboardA11y']; - resizeObserver: ResizeObserver | null; - nodesDraggable: boolean; - nodesConnectable: boolean; - nodesFocusable: boolean; - elementsSelectable: boolean; - onError: ReactFlowState['onError']; -}) { - const node = useStore((s) => s.nodeLookup.get(props.id)); - if (!node) return null; - - let nodeType = node.type || 'default'; - - if (!props.nodeTypes[nodeType]) { - props.onError?.('003', errorMessages['error003'](nodeType)); - - nodeType = 'default'; - } - - const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; - const isDraggable = !!(node.draggable || (props.nodesDraggable && typeof node.draggable === 'undefined')); - const isSelectable = !!(node.selectable || (props.elementsSelectable && typeof node.selectable === 'undefined')); - const isConnectable = !!(node.connectable || (props.nodesConnectable && typeof node.connectable === 'undefined')); - const isFocusable = !!(node.focusable || (props.nodesFocusable && typeof node.focusable === 'undefined')); - - const absolutePositionClamped = props.nodeExtent - ? clampPosition(node.computed?.positionAbsolute, props.nodeExtent) - : node.computed?.positionAbsolute || { x: 0, y: 0 }; - - const posOrigin = getPositionWithOrigin({ - x: absolutePositionClamped.x, - y: absolutePositionClamped.y, - width: node.computed?.width ?? node.width ?? 0, - height: node.computed?.height ?? node.height ?? 0, - origin: node.origin || props.nodeOrigin, - }); - const initialized = (!!node.computed?.width && !!node.computed?.height) || (!!node.width && !!node.height); - - return ( -