Browse Source

fix: suppress hover state during canvas panning to prevent cascading re-renders

Replace broken `.react-flow__pane.dragging` DOM query guard with a shared
`isPanningRef` set by ReactFlow's onMoveStart/onMoveEnd callbacks. Clear
hoveredNodeId when panning starts. Use a derived boolean selector in
useVideoAutoplay so hovering node A doesn't re-render video nodes B–E.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
2adf82f70b
  1. 6
      src/components/WorkflowCanvas.tsx
  2. 5
      src/components/nodes/BaseNode.tsx
  3. 4
      src/hooks/useVideoAutoplay.ts

6
src/components/WorkflowCanvas.tsx

@ -264,6 +264,9 @@ const findScrollableAncestor = (target: HTMLElement, deltaX: number, deltaY: num
return null; return null;
}; };
/** Shared ref so child components (BaseNode) can check panning state without re-rendering */
export const isPanningRef = { current: false };
export function WorkflowCanvas() { export function WorkflowCanvas() {
const { nodes, edges, groups, isModalOpen, showQuickstart, navigationTarget, canvasNavigationSettings, dimmedNodeIds } = const { nodes, edges, groups, isModalOpen, showQuickstart, navigationTarget, canvasNavigationSettings, dimmedNodeIds } =
useWorkflowStore(useShallow((state) => ({ useWorkflowStore(useShallow((state) => ({
@ -294,6 +297,7 @@ export function WorkflowCanvas() {
const setShortcutsDialogOpen = useWorkflowStore((state) => state.setShortcutsDialogOpen); const setShortcutsDialogOpen = useWorkflowStore((state) => state.setShortcutsDialogOpen);
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
const clearWorkflow = useWorkflowStore((state) => state.clearWorkflow); const clearWorkflow = useWorkflowStore((state) => state.clearWorkflow);
const setHoveredNodeId = useWorkflowStore((state) => state.setHoveredNodeId);
const openAnnotationModal = useAnnotationStore((state) => state.openModal); const openAnnotationModal = useAnnotationStore((state) => state.openModal);
const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter } = useReactFlow(); const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter } = useReactFlow();
const { show: showToast } = useToast(); const { show: showToast } = useToast();
@ -1953,6 +1957,8 @@ export function WorkflowCanvas() {
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={handleConnect} onConnect={handleConnect}
onConnectEnd={handleConnectEnd} onConnectEnd={handleConnectEnd}
onMoveStart={() => { isPanningRef.current = true; setHoveredNodeId(null); }}
onMoveEnd={() => { isPanningRef.current = false; }}
onNodeDragStop={handleNodeDragStop} onNodeDragStop={handleNodeDragStop}
onSelectionChange={handleSelectionChange} onSelectionChange={handleSelectionChange}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}

5
src/components/nodes/BaseNode.tsx

@ -3,6 +3,7 @@
import { ReactNode, useCallback, useRef, useLayoutEffect } from "react"; import { ReactNode, useCallback, useRef, useLayoutEffect } from "react";
import { Node, NodeResizer, OnResize, useReactFlow } from "@xyflow/react"; import { Node, NodeResizer, OnResize, useReactFlow } from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { isPanningRef } from "@/components/WorkflowCanvas";
import { getMediaDimensions, calculateAspectFitSize } from "@/utils/nodeDimensions"; import { getMediaDimensions, calculateAspectFitSize } from "@/utils/nodeDimensions";
const DEFAULT_NODE_DIMENSION = 300; const DEFAULT_NODE_DIMENSION = 300;
@ -296,11 +297,11 @@ export function BaseNode({
${className} ${className}
`} `}
onMouseEnter={() => { onMouseEnter={() => {
if (document.querySelector('.react-flow__pane.dragging')) return; if (isPanningRef.current) return;
setHoveredNodeId(id); setHoveredNodeId(id);
}} }}
onMouseLeave={() => { onMouseLeave={() => {
if (document.querySelector('.react-flow__pane.dragging')) return; if (isPanningRef.current) return;
setHoveredNodeId(null); setHoveredNodeId(null);
}} }}
> >

4
src/hooks/useVideoAutoplay.ts

@ -22,9 +22,7 @@ export function useVideoAutoplay(
): React.RefObject<HTMLVideoElement | null> { ): React.RefObject<HTMLVideoElement | null> {
const videoRef = useRef<HTMLVideoElement | null>(null); const videoRef = useRef<HTMLVideoElement | null>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hoveredNodeId = useWorkflowStore((s) => s.hoveredNodeId); const isHovered = useWorkflowStore((s) => s.hoveredNodeId === nodeId);
const isHovered = hoveredNodeId === nodeId;
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;

Loading…
Cancel
Save