From aac32b1b92f9686b020101ddf7a71765cdb0f431 Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Sat, 18 Apr 2026 11:49:03 +0800 Subject: [PATCH] fix(frontend): block selectionchange during MentionInput DOM updates Root cause: when MentionInput updates innerHTML to reflect value changes, the browser resets the selection to position 0. The selectionchange event handler reads this wrong cursor position and sets ms.cursorOffset=0, breaking mentionState calculation for the popover. Fix: - MentionInput sets window.__mentionBlockSelection=true before innerHTML - Clears it via requestAnimationFrame after caret is restored - selectionchange handler skips cursor reading when flag is set --- src/components/room/MentionInput.tsx | 9 ++++++++- src/components/room/RoomChatPanel.tsx | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/room/MentionInput.tsx b/src/components/room/MentionInput.tsx index e12681f..a6a46e2 100644 --- a/src/components/room/MentionInput.tsx +++ b/src/components/room/MentionInput.tsx @@ -166,7 +166,6 @@ export const MentionInput = forwardRef(functi if (!el) return; // If the DOM already contains what we want, nothing to do. - // (Programmatic updates via onChange already set the DOM correctly.) if (getPlainText(el) === value) { isUserInputRef.current = false; return; @@ -176,6 +175,9 @@ export const MentionInput = forwardRef(functi const oldCaret = isUserInputRef.current ? getCaretOffset(el) : 0; const oldLen = internalValueRef.current.length; + // Block selectionchange handler from reading wrong cursor position during DOM update + (window as any).__mentionBlockSelection = true; + // Update DOM el.innerHTML = value.trim() ? renderToHtml(value) : ''; internalValueRef.current = value; @@ -187,6 +189,11 @@ export const MentionInput = forwardRef(functi const newLen = value.length; setCaretAtOffset(el, Math.round(ratio * newLen)); } + + // Unblock selectionchange after DOM has settled + requestAnimationFrame(() => { + (window as any).__mentionBlockSelection = false; + }); }, [value]); /** Handle input changes — extracts plain text from DOM and sends to parent */ diff --git a/src/components/room/RoomChatPanel.tsx b/src/components/room/RoomChatPanel.tsx index 9b3152d..edf03d9 100644 --- a/src/components/room/RoomChatPanel.tsx +++ b/src/components/room/RoomChatPanel.tsx @@ -83,12 +83,13 @@ const ChatInputArea = memo(function ChatInputArea({ // ─── Track DOM cursor offset → ms.cursorOffset on user navigation ────── // Uses selectionchange event which fires when caret moves via arrows/clicks. - // Does NOT fire during programmatic updates, avoiding cursor jumps. + // Ignores programmatic DOM updates (blocked via window.__mentionBlockSelection). const prevCursorRef = useRef(ms.cursorOffset); const skipCursorTrackingRef = useRef(false); useEffect(() => { const readCursor = () => { if (skipCursorTrackingRef.current) return; + if ((window as any).__mentionBlockSelection) return; const el = containerRef.current; if (!el) return; const sel = window.getSelection();