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.
This commit is contained in:
parent
14de80b24b
commit
17e878c8b8
@ -62,6 +62,8 @@ interface MentionPopoverProps {
|
||||
onSelect: (newValue: string, newCursorPosition: number) => void;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
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<PopoverPosition>({ 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<MentionState | null>(() => {
|
||||
@ -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();
|
||||
|
||||
@ -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<HTMLTextAreaElement>) => {
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user