fix(react): node observation #4064 (#4182)

* fix(react): node observation #4064

* chore(react): cleanup useNodeObserver

* chore(react): cleanup useNodeObserver
This commit is contained in:
Moritz Klack
2024-04-18 18:00:49 +02:00
committed by GitHub
parent 4a7ba3f6ea
commit 973a3ddd35
4 changed files with 94 additions and 77 deletions
@@ -185,6 +185,7 @@ const CustomNodeFlow = () => {
maxZoom={5} maxZoom={5}
snapToGrid={snapToGrid} snapToGrid={snapToGrid}
fitView fitView
onlyRenderVisibleElements
> >
<Controls /> <Controls />
<Panel position="bottom-right"> <Panel position="bottom-right">
@@ -1,4 +1,4 @@
import { useEffect, useRef, type MouseEvent, type KeyboardEvent } from 'react'; import { type MouseEvent, type KeyboardEvent } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { import {
@@ -18,6 +18,7 @@ import { useDrag } from '../../hooks/useDrag';
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { handleNodeClick } from '../Nodes/utils'; import { handleNodeClick } from '../Nodes/utils';
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
import { useNodeObserver } from './useNodeObserver';
import type { InternalNode, Node, NodeWrapperProps } from '../../types'; import type { InternalNode, Node, NodeWrapperProps } from '../../types';
export function NodeWrapper<NodeType extends Node>({ export function NodeWrapper<NodeType extends Node>({
@@ -68,58 +69,8 @@ export function NodeWrapper<NodeType extends Node>({
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined')); const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
const store = useStoreApi(); const store = useStoreApi();
const nodeRef = useRef<HTMLDivElement>(null); const hasDimensions = nodeHasDimensions(node);
const prevSourcePosition = useRef(node.sourcePosition); const nodeRef = useNodeObserver({ node, nodeType, hasDimensions, resizeObserver });
const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType);
const nodeDimensions = getNodeDimensions(node);
const inlineDimensions = getNodeInlineStyleDimensions(node);
const initialized = nodeHasDimensions(node);
const hasHandleBounds = !!node.internals.handleBounds;
const moveSelectedNodes = useMoveSelectedNodes();
useEffect(() => {
const currNode = nodeRef.current;
return () => {
if (currNode) {
resizeObserver?.unobserve(currNode);
}
};
}, []);
useEffect(() => {
if (nodeRef.current && !node.hidden) {
const currNode = nodeRef.current;
if (!initialized || !hasHandleBounds) {
resizeObserver?.unobserve(currNode);
resizeObserver?.observe(currNode);
}
}
}, [node.hidden, initialized, hasHandleBounds]);
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().updateNodeInternals(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]]));
}
}, [id, nodeType, node.sourcePosition, node.targetPosition]);
const dragging = useDrag({ const dragging = useDrag({
nodeRef, nodeRef,
disabled: node.hidden || !isDraggable, disabled: node.hidden || !isDraggable,
@@ -128,20 +79,21 @@ export function NodeWrapper<NodeType extends Node>({
nodeId: id, nodeId: id,
isSelectable, isSelectable,
}); });
const moveSelectedNodes = useMoveSelectedNodes();
if (node.hidden) { if (node.hidden) {
return null; return null;
} }
const positionAbsolute = nodeExtent const nodeDimensions = getNodeDimensions(node);
? clampPosition(node.internals.positionAbsolute, nodeExtent) const inlineDimensions = getNodeInlineStyleDimensions(node);
: node.internals.positionAbsolute || { x: 0, y: 0 }; const clampedPosition = nodeExtent
? clampPosition(internals.positionAbsolute, nodeExtent)
: internals.positionAbsolute;
const positionWithOrigin = getPositionWithOrigin({ const positionWithOrigin = getPositionWithOrigin({
x: positionAbsolute.x, ...clampedPosition,
y: positionAbsolute.y, ...nodeDimensions,
width: nodeDimensions.width,
height: nodeDimensions.height,
origin: node.origin || nodeOrigin, origin: node.origin || nodeOrigin,
}); });
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
@@ -188,7 +140,7 @@ export function NodeWrapper<NodeType extends Node>({
store.setState({ store.setState({
ariaLiveMessage: `Moved selected node ${event.key ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '') .replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`, .toLowerCase()}. New position, x: ${~~clampedPosition.x}, y: ${~~clampedPosition.y}`,
}); });
moveSelectedNodes({ moveSelectedNodes({
@@ -221,7 +173,7 @@ export function NodeWrapper<NodeType extends Node>({
zIndex: internals.z, zIndex: internals.z,
transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`, transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none', pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden', visibility: hasDimensions ? 'visible' : 'hidden',
...node.style, ...node.style,
...inlineDimensions, ...inlineDimensions,
}} }}
@@ -244,8 +196,8 @@ export function NodeWrapper<NodeType extends Node>({
id={id} id={id}
data={node.data} data={node.data}
type={nodeType} type={nodeType}
positionAbsoluteX={positionAbsolute.x} positionAbsoluteX={clampedPosition.x}
positionAbsoluteY={positionAbsolute.y} positionAbsoluteY={clampedPosition.y}
selected={node.selected} selected={node.selected}
isConnectable={isConnectable} isConnectable={isConnectable}
sourcePosition={node.sourcePosition} sourcePosition={node.sourcePosition}
@@ -0,0 +1,71 @@
import { useEffect, useRef } from 'react';
import type { InternalNode } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
/**
* Hook to handle the resize observation + internal updates for the passed node.
*
* @internal
* @returns nodeRef - reference to the node element
*/
export function useNodeObserver({
node,
nodeType,
hasDimensions,
resizeObserver,
}: {
node: InternalNode;
nodeType: string;
hasDimensions: boolean;
resizeObserver: ResizeObserver | null;
}) {
const store = useStoreApi();
const nodeRef = useRef<HTMLDivElement | null>(null);
const observedNode = useRef<HTMLDivElement | null>(null);
const prevSourcePosition = useRef(node.sourcePosition);
const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType);
const isInitialized = hasDimensions && !!node.internals.handleBounds && !node.hidden;
useEffect(() => {
if (nodeRef.current && (!isInitialized || observedNode.current !== nodeRef.current)) {
if (observedNode.current) {
resizeObserver?.unobserve(observedNode.current);
}
resizeObserver?.observe(nodeRef.current);
observedNode.current = nodeRef.current;
}
}, [isInitialized]);
useEffect(() => {
return () => {
if (observedNode.current) {
resizeObserver?.unobserve(observedNode.current);
observedNode.current = null;
}
};
}, []);
useEffect(() => {
if (nodeRef.current) {
// when the user programmatically changes the source or handle position, we need to update the internals
// to make sure the edges are updated correctly
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
if (typeChanged || sourcePosChanged || targetPosChanged) {
prevType.current = nodeType;
prevSourcePosition.current = node.sourcePosition;
prevTargetPosition.current = node.targetPosition;
store
.getState()
.updateNodeInternals(new Map([[node.id, { id: node.id, nodeElement: nodeRef.current, force: true }]]));
}
}
}, [node.id, nodeType, node.sourcePosition, node.targetPosition]);
return nodeRef;
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useState } from 'react';
import { ReactFlowState } from '../../types'; import { ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
@@ -8,16 +8,13 @@ const selector = (s: ReactFlowState) => s.updateNodeInternals;
export function useResizeObserver() { export function useResizeObserver() {
const updateNodeInternals = useStore(selector); const updateNodeInternals = useStore(selector);
const resizeObserverRef = useRef<ResizeObserver>(); const [resizeObserver] = useState(() => {
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') { if (typeof ResizeObserver === 'undefined') {
return null; return null;
} }
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { return new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = new Map<string, InternalNodeUpdate>(); const updates = new Map<string, InternalNodeUpdate>();
entries.forEach((entry: ResizeObserverEntry) => { entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string; const id = entry.target.getAttribute('data-id') as string;
updates.set(id, { updates.set(id, {
@@ -28,17 +25,13 @@ export function useResizeObserver() {
updateNodeInternals(updates); updateNodeInternals(updates);
}); });
});
resizeObserverRef.current = observer;
return observer;
}, []);
useEffect(() => { useEffect(() => {
return () => { return () => {
resizeObserverRef?.current?.disconnect(); resizeObserver?.disconnect();
}; };
}, []); }, [resizeObserver]);
return resizeObserver; return resizeObserver;
} }