fix(room): make handleSelect read DOM directly instead of stale props
Root cause: MentionPopover's native keydown listener fires before React state updates, so handleSelect read stale inputValue/cursorPosition props and silently returned early. Fix: handleSelect now reads textarea.value/selectionStart directly from the DOM, matching the approach already used in ChatInputArea's handleMentionSelect. No more stale closure.
This commit is contained in:
parent
3cd5b3003c
commit
b8a61b0802
@ -62,10 +62,6 @@ interface MentionPopoverProps {
|
|||||||
onSelect: (newValue: string, newCursorPosition: number) => void;
|
onSelect: (newValue: string, newCursorPosition: number) => void;
|
||||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
/** Mutable ref: write the confirm fn here so ChatInputArea can call it on Enter */
|
|
||||||
onMentionConfirmRef?: React.MutableRefObject<() => void>;
|
|
||||||
/** Simpler alternative to onMentionConfirmRef — parent provides a callback directly */
|
|
||||||
onMentionConfirm?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category configuration with icons and colors
|
// Category configuration with icons and colors
|
||||||
@ -377,8 +373,6 @@ export function MentionPopover({
|
|||||||
onSelect,
|
onSelect,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onMentionConfirmRef,
|
|
||||||
onMentionConfirm,
|
|
||||||
}: MentionPopoverProps) {
|
}: MentionPopoverProps) {
|
||||||
const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0 });
|
const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0 });
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
@ -509,11 +503,20 @@ export function MentionPopover({
|
|||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(suggestion: MentionSuggestion) => {
|
(suggestion: MentionSuggestion) => {
|
||||||
const ms = mentionStateRef.current;
|
if (!textareaRef.current || suggestion.type !== 'item') return;
|
||||||
if (!ms || suggestion.type !== 'item') return;
|
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
const cursorPos = textarea.selectionStart;
|
||||||
|
const textBefore = textarea.value.substring(0, cursorPos);
|
||||||
|
const atMatch = textBefore.match(/@([^:@\s<]*)(:([^\s<]*))?$/);
|
||||||
|
if (!atMatch) return;
|
||||||
|
|
||||||
|
const [fullMatch] = atMatch;
|
||||||
|
const startPos = cursorPos - fullMatch.length;
|
||||||
|
|
||||||
|
const before = textarea.value.substring(0, startPos);
|
||||||
|
const after = textarea.value.substring(cursorPos);
|
||||||
|
|
||||||
const before = inputValue.slice(0, ms.startPos);
|
|
||||||
const after = inputValue.slice(cursorPosition);
|
|
||||||
const html = buildMentionHtml(
|
const html = buildMentionHtml(
|
||||||
suggestion.category!,
|
suggestion.category!,
|
||||||
suggestion.mentionId!,
|
suggestion.mentionId!,
|
||||||
@ -521,23 +524,17 @@ export function MentionPopover({
|
|||||||
);
|
);
|
||||||
const spacer = ' ';
|
const spacer = ' ';
|
||||||
const newValue = before + html + spacer + after;
|
const newValue = before + html + spacer + after;
|
||||||
const newCursorPos = ms.startPos + html.length + spacer.length;
|
const newCursorPos = startPos + html.length + spacer.length;
|
||||||
|
|
||||||
onSelect(newValue, newCursorPos);
|
onSelect(newValue, newCursorPos);
|
||||||
closePopover();
|
closePopover();
|
||||||
},
|
},
|
||||||
[inputValue, cursorPosition, onSelect, closePopover],
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[onSelect, closePopover],
|
||||||
);
|
);
|
||||||
|
|
||||||
handleSelectRef.current = handleSelect;
|
handleSelectRef.current = handleSelect;
|
||||||
|
|
||||||
// Register confirm fn so ChatInputArea can call it on Enter
|
|
||||||
if (onMentionConfirmRef) {
|
|
||||||
onMentionConfirmRef.current = () => {
|
|
||||||
const item = visibleSuggestionsRef.current[selectedIndexRef.current];
|
|
||||||
if (item) handleSelectRef.current(item);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position calculation
|
// Position calculation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mentionState || !textareaRef.current) return;
|
if (!mentionState || !textareaRef.current) return;
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import { RoomThreadPanel } from './RoomThreadPanel';
|
|||||||
|
|
||||||
const MENTION_PATTERN = /@([^:@\s]*)(:([^\s]*))?$/;
|
const MENTION_PATTERN = /@([^:@\s]*)(:([^\s]*))?$/;
|
||||||
const MENTION_POPOVER_KEYS = ['Enter', 'Tab', 'ArrowUp', 'ArrowDown'];
|
const MENTION_POPOVER_KEYS = ['Enter', 'Tab', 'ArrowUp', 'ArrowDown'];
|
||||||
export const mentionConfirmRef = { current: (() => {}) as () => void };
|
|
||||||
|
|
||||||
|
|
||||||
export interface ChatInputAreaHandle {
|
export interface ChatInputAreaHandle {
|
||||||
@ -52,8 +51,6 @@ interface ChatInputAreaProps {
|
|||||||
draft: string;
|
draft: string;
|
||||||
onDraftChange: (content: string) => void;
|
onDraftChange: (content: string) => void;
|
||||||
onClearDraft: () => void;
|
onClearDraft: () => void;
|
||||||
/** Called when Enter is pressed with mention popover open — parent triggers mention selection */
|
|
||||||
onMentionConfirm?: () => void;
|
|
||||||
ref?: React.Ref<ChatInputAreaHandle>;
|
ref?: React.Ref<ChatInputAreaHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +68,6 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
draft,
|
draft,
|
||||||
onDraftChange,
|
onDraftChange,
|
||||||
onClearDraft,
|
onClearDraft,
|
||||||
onMentionConfirm,
|
|
||||||
ref,
|
ref,
|
||||||
}: ChatInputAreaProps) {
|
}: ChatInputAreaProps) {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@ -161,9 +157,7 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
|
|
||||||
if (hasMention && MENTION_POPOVER_KEYS.includes(e.key) && !e.ctrlKey && !e.shiftKey) {
|
if (hasMention && MENTION_POPOVER_KEYS.includes(e.key) && !e.ctrlKey && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if ((e.key === 'Enter' || e.key === 'Tab') && onMentionConfirm) {
|
// Enter/Tab is handled by MentionPopover's native keydown listener
|
||||||
onMentionConfirm();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -609,7 +603,6 @@ export function RoomChatPanel({ room, isAdmin, onClose, onDelete }: RoomChatPane
|
|||||||
draft={draft}
|
draft={draft}
|
||||||
onDraftChange={setDraft}
|
onDraftChange={setDraft}
|
||||||
onClearDraft={clearDraft}
|
onClearDraft={clearDraft}
|
||||||
onMentionConfirm={() => { console.log('[RoomChatPanel] onMentionConfirm called'); mentionConfirmRef.current(); }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user