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}