From 17e878c8b86cd3044e8be76f9ffb69bbd3613fbb Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Sat, 18 Apr 2026 00:51:14 +0800 Subject: [PATCH] fix(room): fix Enter-on-category via React state update instead of DOM manipulation Problem: dispatchEvent('input') doesn't trigger React's onChange in React 17+. Solution: pass onCategoryEnter callback from ChatInputArea to MentionPopover. When Enter is pressed on a category, MentionPopover calls onCategoryEnter(category) which directly calls React setState (onDraftChange, setCursorPosition, setShowMentionPopover) inside ChatInputArea, properly triggering re-render. --- src/components/room/MentionPopover.tsx | 23 +++++---------- src/components/room/RoomChatPanel.tsx | 41 +++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/components/room/MentionPopover.tsx b/src/components/room/MentionPopover.tsx index 9942b22..c4dd9ea 100644 --- a/src/components/room/MentionPopover.tsx +++ b/src/components/room/MentionPopover.tsx @@ -62,6 +62,8 @@ interface MentionPopoverProps { onSelect: (newValue: string, newCursorPosition: number) => void; textareaRef: React.RefObject; onOpenChange: (open: boolean) => void; + /** Called when Enter is pressed on a category — ChatInputArea handles the state update */ + onCategoryEnter: (category: string) => void; } // Category configuration with icons and colors @@ -373,6 +375,7 @@ export function MentionPopover({ onSelect, textareaRef, onOpenChange, + onCategoryEnter, }: MentionPopoverProps) { const [position, setPosition] = useState({ top: 0, left: 0 }); const [selectedIndex, setSelectedIndex] = useState(0); @@ -385,6 +388,8 @@ export function MentionPopover({ const selectedIndexRef = useRef(selectedIndex); const handleSelectRef = useRef<(s: MentionSuggestion) => void>(() => {}); const closePopoverRef = useRef<() => void>(() => {}); + const onCategoryEnterRef = useRef(onCategoryEnter); + onCategoryEnterRef.current = onCategoryEnter; // Parse mention state const mentionState = useMemo(() => { @@ -632,23 +637,9 @@ export function MentionPopover({ if (!item) return; if (item.type === 'category') { - // Enter a category: append ':' to textarea to trigger next-level list + // Enter a category: let ChatInputArea handle the state update e.preventDefault(); - const category = item.category; - const textarea = textareaRef.current; - if (!textarea) return; - const curPos = textarea.selectionStart; - const val = textarea.value; - // Replace @xxx with @xxx: to enter the category - const textBefore = val.substring(0, ms.startPos); - const afterPartial = val.substring(ms.startPos + ms.category.length + 1); // skip '@' + category - const newVal = textBefore + '@' + category + ':' + afterPartial; - const newPos = ms.startPos + 1 + category.length + 1; // after '@ai:' - textarea.value = newVal; - textarea.setSelectionRange(newPos, newPos); - textarea.focus(); - // Dispatch input event so React picks up the change - textarea.dispatchEvent(new Event('input', { bubbles: true })); + onCategoryEnterRef.current(item.category!); } else if (item.type === 'item') { // Insert the mention e.preventDefault(); diff --git a/src/components/room/RoomChatPanel.tsx b/src/components/room/RoomChatPanel.tsx index f8a7632..e239cfb 100644 --- a/src/components/room/RoomChatPanel.tsx +++ b/src/components/room/RoomChatPanel.tsx @@ -35,6 +35,8 @@ const MENTION_POPOVER_KEYS = ['Enter', 'Tab', 'ArrowUp', 'ArrowDown']; export interface ChatInputAreaHandle { insertMention: (id: string, label: string, type: 'user' | 'ai') => void; + /** Insert a category prefix (e.g. 'ai') into the textarea and trigger React onChange */ + insertCategory: (category: string) => void; } interface ChatInputAreaProps { @@ -131,6 +133,26 @@ const ChatInputArea = memo(function ChatInputArea({ } }, 0); }, + insertCategory: (category: string) => { + // Enter a category: e.g. @ai → @ai: + if (!textareaRef.current) return; + const textarea = textareaRef.current; + const textBefore = textarea.value.substring(0, textarea.selectionStart); + const atMatch = textBefore.match(MENTION_PATTERN); + if (!atMatch) return; + const [fullMatch] = atMatch; + const startPos = textarea.selectionStart - fullMatch.length; + const before = textarea.value.substring(0, startPos); + const afterPartial = textarea.value.substring(startPos + fullMatch.length); + const newValue = before + '@' + category + ':' + afterPartial; + const newCursorPos = startPos + 1 + category.length + 1; // after '@ai:' + // Directly update React state to trigger mention detection + onDraftChange(newValue); + setCursorPosition(newCursorPos); + const textBefore2 = newValue.substring(0, newCursorPos); + const match = textBefore2.match(MENTION_PATTERN); + setShowMentionPopover(!!match); + }, })); const handleChange = (e: React.ChangeEvent) => { @@ -261,7 +283,24 @@ const ChatInputArea = memo(function ChatInputArea({ onSelect={handleMentionSelect} textareaRef={textareaRef} onOpenChange={setShowMentionPopover} - onMentionConfirm={handleMentionSelect} + onCategoryEnter={(category: string) => { + // Enter a category: @ai → @ai: to show next-level items + if (!textareaRef.current) return; + const textarea = textareaRef.current; + const textBefore = textarea.value.substring(0, textarea.selectionStart); + const atMatch = textBefore.match(MENTION_PATTERN); + if (!atMatch) return; + const [fullMatch] = atMatch; + const startPos = textarea.selectionStart - fullMatch.length; + const before = textarea.value.substring(0, startPos); + const afterPartial = textarea.value.substring(startPos + fullMatch.length); + const newValue = before + '@' + category + ':' + afterPartial; + const newCursorPos = startPos + 1 + category.length + 1; + onDraftChange(newValue); + setCursorPosition(newCursorPos); + const match2 = newValue.substring(0, newCursorPos).match(MENTION_PATTERN); + setShowMentionPopover(!!match2); + }} /> )}