From 2730991ca9df7543be3bf2020cdf02ff7979fa22 Mon Sep 17 00:00:00 2001 From: jainarjav80-sys Date: Thu, 9 Jul 2026 01:16:56 +0530 Subject: [PATCH 1/2] fix: resolve node handle misalignment during live edits --- src/components/CustomReactFlowNode.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/CustomReactFlowNode.tsx b/src/components/CustomReactFlowNode.tsx index f612998c..3272ed7c 100644 --- a/src/components/CustomReactFlowNode.tsx +++ b/src/components/CustomReactFlowNode.tsx @@ -1,6 +1,6 @@ -import { Handle } from "@xyflow/react"; +import { Handle, useUpdateNodeInternals } from "@xyflow/react"; import type { RFNodeData } from "../utils/processAST"; -import { useContext, useLayoutEffect, useRef, useState } from "react"; +import { useContext, useEffect, useLayoutEffect, useRef, useState } from "react"; import { AppContext } from "../contexts/AppContext"; const CustomNode = ({ data, id, selected }: { data: RFNodeData; id: string; selected: boolean }) => { @@ -12,6 +12,14 @@ const CustomNode = ({ data, id, selected }: { data: RFNodeData; id: string; sele const [handleOffsets, setHandleOffsets] = useState>( {} ); + const updateNodeInternals = useUpdateNodeInternals(); + + useEffect(() => { + const timer = setTimeout(() => { + updateNodeInternals(id); + }, 0); + return () => clearTimeout(timer); + }, [handleOffsets, id, updateNodeInternals]); useLayoutEffect(() => { const container = Object.values(rowRefs.current)[0]?.offsetParent; From c337101eef644ca4a6fd0d4a7ff91f2cc4308152 Mon Sep 17 00:00:00 2001 From: jainarjav80-sys Date: Mon, 13 Jul 2026 20:41:32 +0530 Subject: [PATCH 2/2] fix: resolve React Flow handle misalignment due to zoom scaling --- .changeset/fix-handle-misalignment.md | 5 ++ src/components/CustomReactFlowNode.tsx | 63 +++++++++++++++++++------- 2 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 .changeset/fix-handle-misalignment.md diff --git a/.changeset/fix-handle-misalignment.md b/.changeset/fix-handle-misalignment.md new file mode 100644 index 00000000..1bfd9109 --- /dev/null +++ b/.changeset/fix-handle-misalignment.md @@ -0,0 +1,5 @@ +--- +"json-schema-studio": patch +--- + +fix: resolve node handle misalignment during live edits by using ResizeObserver and unscaled offsets diff --git a/src/components/CustomReactFlowNode.tsx b/src/components/CustomReactFlowNode.tsx index 3272ed7c..2afd6226 100644 --- a/src/components/CustomReactFlowNode.tsx +++ b/src/components/CustomReactFlowNode.tsx @@ -9,37 +9,66 @@ const CustomNode = ({ data, id, selected }: { data: RFNodeData; id: string; sele const rowRefs = useRef< Record >({}); + const nodeRef = useRef(null); const [handleOffsets, setHandleOffsets] = useState>( {} ); const updateNodeInternals = useUpdateNodeInternals(); useEffect(() => { - const timer = setTimeout(() => { - updateNodeInternals(id); - }, 0); - return () => clearTimeout(timer); - }, [handleOffsets, id, updateNodeInternals]); + if (!nodeRef.current) return; - useLayoutEffect(() => { - const container = Object.values(rowRefs.current)[0]?.offsetParent; - if (!(container instanceof HTMLElement)) return; + const updateOffsets = () => { + const container = nodeRef.current; + if (!container) return; - const containerRect = container.getBoundingClientRect(); - const offsets: Record = {}; + const offsets: Record = {}; + let changed = false; - for (const [key, row] of Object.entries(rowRefs.current)) { - if (!row) continue; + for (const [key, row] of Object.entries(rowRefs.current)) { + if (!row) continue; - const rect = row.getBoundingClientRect(); - offsets[key] = rect.top + rect.height / 2 - containerRect.top; - } + // Compute local unscaled coordinates by climbing offsetParents up to the container + let top = 0; + let el: HTMLElement | null = row; + while (el && el !== container) { + top += el.offsetTop; + el = el.offsetParent as HTMLElement | null; + } + + offsets[key] = top + row.offsetHeight / 2; + } + + setHandleOffsets((prev) => { + if (Object.keys(prev).length !== Object.keys(offsets).length) changed = true; + else { + for (const k in offsets) { + if (prev[k] !== offsets[k]) { + changed = true; + break; + } + } + } + if (changed) { + // React state update triggers re-render, we notify React Flow immediately after + setTimeout(() => updateNodeInternals(id), 0); + return offsets; + } + return prev; + }); + }; + + // Use ResizeObserver to catch layout changes regardless of data changes or word wraps + const observer = new ResizeObserver(updateOffsets); + observer.observe(nodeRef.current); + updateOffsets(); // Run initially - setHandleOffsets(offsets); - }, [data.nodeData]); + return () => observer.disconnect(); + }, [id, updateNodeInternals, data.nodeData]); return (