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:
parent
53b0b03716
commit
aac32b1b92
@ -166,7 +166,6 @@ export const MentionInput = forwardRef<HTMLDivElement, MentionInputProps>(functi
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
// If the DOM already contains what we want, nothing to do.
|
// If the DOM already contains what we want, nothing to do.
|
||||||
// (Programmatic updates via onChange already set the DOM correctly.)
|
|
||||||
if (getPlainText(el) === value) {
|
if (getPlainText(el) === value) {
|
||||||
isUserInputRef.current = false;
|
isUserInputRef.current = false;
|
||||||
return;
|
return;
|
||||||
@ -176,6 +175,9 @@ export const MentionInput = forwardRef<HTMLDivElement, MentionInputProps>(functi
|
|||||||
const oldCaret = isUserInputRef.current ? getCaretOffset(el) : 0;
|
const oldCaret = isUserInputRef.current ? getCaretOffset(el) : 0;
|
||||||
const oldLen = internalValueRef.current.length;
|
const oldLen = internalValueRef.current.length;
|
||||||
|
|
||||||
|
// Block selectionchange handler from reading wrong cursor position during DOM update
|
||||||
|
(window as any).__mentionBlockSelection = true;
|
||||||
|
|
||||||
// Update DOM
|
// Update DOM
|
||||||
el.innerHTML = value.trim() ? renderToHtml(value) : '';
|
el.innerHTML = value.trim() ? renderToHtml(value) : '';
|
||||||
internalValueRef.current = value;
|
internalValueRef.current = value;
|
||||||
@ -187,6 +189,11 @@ export const MentionInput = forwardRef<HTMLDivElement, MentionInputProps>(functi
|
|||||||
const newLen = value.length;
|
const newLen = value.length;
|
||||||
setCaretAtOffset(el, Math.round(ratio * newLen));
|
setCaretAtOffset(el, Math.round(ratio * newLen));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unblock selectionchange after DOM has settled
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
(window as any).__mentionBlockSelection = false;
|
||||||
|
});
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
/** Handle input changes — extracts plain text from DOM and sends to parent */
|
/** Handle input changes — extracts plain text from DOM and sends to parent */
|
||||||
|
|||||||
@ -83,12 +83,13 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
|
|
||||||
// ─── Track DOM cursor offset → ms.cursorOffset on user navigation ──────
|
// ─── Track DOM cursor offset → ms.cursorOffset on user navigation ──────
|
||||||
// Uses selectionchange event which fires when caret moves via arrows/clicks.
|
// 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 prevCursorRef = useRef(ms.cursorOffset);
|
||||||
const skipCursorTrackingRef = useRef(false);
|
const skipCursorTrackingRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const readCursor = () => {
|
const readCursor = () => {
|
||||||
if (skipCursorTrackingRef.current) return;
|
if (skipCursorTrackingRef.current) return;
|
||||||
|
if ((window as any).__mentionBlockSelection) return;
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user