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
This commit is contained in:
ZhenYi 2026-04-18 11:49:03 +08:00
parent 53b0b03716
commit aac32b1b92
2 changed files with 10 additions and 2 deletions

View File

@ -166,7 +166,6 @@ export const MentionInput = forwardRef<HTMLDivElement, MentionInputProps>(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<HTMLDivElement, MentionInputProps>(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<HTMLDivElement, MentionInputProps>(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 */

View File

@ -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();