fix: @-mention autocomplete selection and dismissal
- Use a ref for mentionState so selectMention always reads the latest value (prevents stale-closure "blink" on click/Enter/Tab) - Add explicit space key handling to dismiss the popup immediately - Move Escape handler outside filteredMentions check so it always works - Sync mentionStateRef on all state transitions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -138,8 +138,9 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const dragDepthRef = useRef(0);
|
const dragDepthRef = useRef(0);
|
||||||
|
|
||||||
// Mention state
|
// Mention state (ref kept in sync so callbacks always see the latest value)
|
||||||
const [mentionState, setMentionState] = useState<MentionState | null>(null);
|
const [mentionState, setMentionState] = useState<MentionState | null>(null);
|
||||||
|
const mentionStateRef = useRef<MentionState | null>(null);
|
||||||
const [mentionIndex, setMentionIndex] = useState(0);
|
const [mentionIndex, setMentionIndex] = useState(0);
|
||||||
const mentionActive = mentionState !== null && mentions && mentions.length > 0;
|
const mentionActive = mentionState !== null && mentions && mentions.length > 0;
|
||||||
|
|
||||||
@@ -194,10 +195,12 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
// Mention detection: listen for selection changes and input events
|
// Mention detection: listen for selection changes and input events
|
||||||
const checkMention = useCallback(() => {
|
const checkMention = useCallback(() => {
|
||||||
if (!mentions || mentions.length === 0 || !containerRef.current) {
|
if (!mentions || mentions.length === 0 || !containerRef.current) {
|
||||||
|
mentionStateRef.current = null;
|
||||||
setMentionState(null);
|
setMentionState(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = detectMention(containerRef.current);
|
const result = detectMention(containerRef.current);
|
||||||
|
mentionStateRef.current = result;
|
||||||
if (result) {
|
if (result) {
|
||||||
setMentionState(result);
|
setMentionState(result);
|
||||||
setMentionIndex(0);
|
setMentionIndex(0);
|
||||||
@@ -224,20 +227,24 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
|
|
||||||
const selectMention = useCallback(
|
const selectMention = useCallback(
|
||||||
(option: MentionOption) => {
|
(option: MentionOption) => {
|
||||||
if (!mentionState) return;
|
// Read from ref to avoid stale-closure issues (selectionchange can
|
||||||
|
// update state between the last render and this callback firing).
|
||||||
|
const state = mentionStateRef.current;
|
||||||
|
if (!state) return;
|
||||||
const current = latestValueRef.current;
|
const current = latestValueRef.current;
|
||||||
const next = applyMention(current, mentionState.query, option);
|
const next = applyMention(current, state.query, option);
|
||||||
if (next !== current) {
|
if (next !== current) {
|
||||||
latestValueRef.current = next;
|
latestValueRef.current = next;
|
||||||
ref.current?.setMarkdown(next);
|
ref.current?.setMarkdown(next);
|
||||||
onChange(next);
|
onChange(next);
|
||||||
}
|
}
|
||||||
|
mentionStateRef.current = null;
|
||||||
setMentionState(null);
|
setMentionState(null);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
ref.current?.focus(undefined, { defaultSelection: "rootEnd" });
|
ref.current?.focus(undefined, { defaultSelection: "rootEnd" });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[mentionState, onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
function hasFilePayload(evt: DragEvent<HTMLDivElement>) {
|
function hasFilePayload(evt: DragEvent<HTMLDivElement>) {
|
||||||
@@ -264,32 +271,43 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mention keyboard navigation
|
// Mention keyboard handling
|
||||||
if (mentionActive && filteredMentions.length > 0) {
|
if (mentionActive) {
|
||||||
if (e.key === "ArrowDown") {
|
// Space dismisses the popup (let the character be typed normally)
|
||||||
e.preventDefault();
|
if (e.key === " ") {
|
||||||
e.stopPropagation();
|
mentionStateRef.current = null;
|
||||||
setMentionIndex((prev) => Math.min(prev + 1, filteredMentions.length - 1));
|
setMentionState(null);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setMentionIndex((prev) => Math.max(prev - 1, 0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Enter" || e.key === "Tab") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
selectMention(filteredMentions[mentionIndex]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Escape always dismisses
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
mentionStateRef.current = null;
|
||||||
setMentionState(null);
|
setMentionState(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Arrow / Enter / Tab only when there are filtered results
|
||||||
|
if (filteredMentions.length > 0) {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setMentionIndex((prev) => Math.min(prev + 1, filteredMentions.length - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setMentionIndex((prev) => Math.max(prev - 1, 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Enter" || e.key === "Tab") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
selectMention(filteredMentions[mentionIndex]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDragEnter={(evt) => {
|
onDragEnter={(evt) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user