From 126ffda4fe2bc61192caac528a2fb62576ff07cb Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Sat, 18 Apr 2026 11:27:37 +0800 Subject: [PATCH] fix(frontend): skip cursor tracking effect during deferred cursor updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: when onCategoryEnter scheduled a deferred setCursorOffset, the tracking effect ran BEFORE setTimeout and read the OLD DOM cursor position (still "@a" → position 2), overwriting the new cursor position of 4 in a subsequent render. Fix: add skipCursorTrackingRef flag. Set it before setTimeout fires, clear it when setTimeout executes. The tracking effect checks the flag and skips its update during the flush window. Same fix applied to insertCategory imperative handle. --- src/components/room/RoomChatPanel.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/room/RoomChatPanel.tsx b/src/components/room/RoomChatPanel.tsx index 94b18c4..fd1af43 100644 --- a/src/components/room/RoomChatPanel.tsx +++ b/src/components/room/RoomChatPanel.tsx @@ -83,7 +83,11 @@ const ChatInputArea = memo(function ChatInputArea({ // Track DOM cursor offset → ms.cursorOffset on every render const prevCursorRef = useRef(ms.cursorOffset); + // Set when a deferred cursor update is scheduled; cleared when it fires. + // Prevents the tracking effect from overwriting a programmatically-set cursor. + const skipCursorTrackingRef = useRef(false); useEffect(() => { + if (skipCursorTrackingRef.current) return; const el = containerRef.current; if (!el) return; const sel = window.getSelection(); @@ -131,7 +135,9 @@ const ChatInputArea = memo(function ChatInputArea({ const newCursorPos = startPos + 1 + category.length + 1; onDraftChangeRef.current(newValue); ms.setValue(newValue); + skipCursorTrackingRef.current = true; setTimeout(() => { + skipCursorTrackingRef.current = false; ms.setCursorOffset(newCursorPos); ms.setShowMentionPopover(!!newValue.substring(0, newCursorPos).match(/@([^:@\s]*)(:([^\s]*))?$/)); }, 0); @@ -249,8 +255,14 @@ const ChatInputArea = memo(function ChatInputArea({ const newCursorPos = startPos + 1 + category.length + 1; onDraftChangeRef.current(newValue); ms.setValue(newValue); - // Defer cursor update until after DOM has flushed the new mention value - setTimeout(() => ms.setCursorOffset(newCursorPos), 0); + // Defer cursor update until after DOM has flushed the new mention value. + // Skip tracking effect during the flush window to avoid it overwriting + // the deferred cursor with the old DOM position. + skipCursorTrackingRef.current = true; + setTimeout(() => { + skipCursorTrackingRef.current = false; + ms.setCursorOffset(newCursorPos); + }, 0); }} suggestions={ms.suggestions} selectedIndex={ms.selectedIndex}