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;
|
onSelect: (newValue: string, newCursorPosition: number) => void;
|
||||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
onOpenChange: (open: boolean) => void;
|
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
|
// Category configuration with icons and colors
|
||||||
@ -373,6 +375,7 @@ export function MentionPopover({
|
|||||||
onSelect,
|
onSelect,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
onCategoryEnter,
|
||||||
}: 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);
|
||||||
@ -385,6 +388,8 @@ export function MentionPopover({
|
|||||||
const selectedIndexRef = useRef(selectedIndex);
|
const selectedIndexRef = useRef(selectedIndex);
|
||||||
const handleSelectRef = useRef<(s: MentionSuggestion) => void>(() => {});
|
const handleSelectRef = useRef<(s: MentionSuggestion) => void>(() => {});
|
||||||
const closePopoverRef = useRef<() => void>(() => {});
|
const closePopoverRef = useRef<() => void>(() => {});
|
||||||
|
const onCategoryEnterRef = useRef(onCategoryEnter);
|
||||||
|
onCategoryEnterRef.current = onCategoryEnter;
|
||||||
|
|
||||||
// Parse mention state
|
// Parse mention state
|
||||||
const mentionState = useMemo<MentionState | null>(() => {
|
const mentionState = useMemo<MentionState | null>(() => {
|
||||||
@ -632,23 +637,9 @@ export function MentionPopover({
|
|||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
if (item.type === 'category') {
|
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();
|
e.preventDefault();
|
||||||
const category = item.category;
|
onCategoryEnterRef.current(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 }));
|
|
||||||
} else if (item.type === 'item') {
|
} else if (item.type === 'item') {
|
||||||
// Insert the mention
|
// Insert the mention
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -35,6 +35,8 @@ const MENTION_POPOVER_KEYS = ['Enter', 'Tab', 'ArrowUp', 'ArrowDown'];
|
|||||||
|
|
||||||
export interface ChatInputAreaHandle {
|
export interface ChatInputAreaHandle {
|
||||||
insertMention: (id: string, label: string, type: 'user' | 'ai') => void;
|
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 {
|
interface ChatInputAreaProps {
|
||||||
@ -131,6 +133,26 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
}
|
}
|
||||||
}, 0);
|
}, 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>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
@ -261,7 +283,24 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
onSelect={handleMentionSelect}
|
onSelect={handleMentionSelect}
|
||||||
textareaRef={textareaRef}
|
textareaRef={textareaRef}
|
||||||
onOpenChange={setShowMentionPopover}
|
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>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user