fix(web): 点表情后光标不再跳出输入框,需要重新点击才能回车发送

点表情面板里的emoji这个操作本身没有做"点完之后把焦点还给输入框"
这件事——emoji按钮是个真实的<button>,点击后浏览器会把焦点停在
这个按钮上,导致输入框失焦,这时候按回车自然没反应(键盘事件根本
没送到输入框),要用户重新点一下输入框才能继续打字/发送。

参照"@"提及按钮已有的"点完之后把焦点和光标位置还给输入框"这套写法
(insertMentionTrigger/pickMention),给emoji选择也接上同一套逻辑,
并且改成插入到光标所在位置,而不是永远追加到文本末尾。

顺手把这三处(含原来的两处"@"相关逻辑)从 requestAnimationFrame
换成 setTimeout(fn, 0):实测确认 requestAnimationFrame 在页面处于
后台/不可见状态时完全不会触发(浏览器的标准省电行为),而这个"点击
按钮后把焦点还给输入框"的操作没有理由依赖页面必须处于前台可见状态
才生效,setTimeout 不受这个限制。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-10 21:49:57 +08:00
co-authored by Claude Sonnet 5
parent 4e1f1cbf0f
commit 76156a1c75
+28 -8
View File
@@ -361,14 +361,18 @@ export function ConversationView() {
setMentionTrigger(null);
setEmojiOpen(false);
// Matches insertMentionTrigger's own restore below -- without an explicit setSelectionRange
// (deferred a frame so the textarea's DOM value has actually updated to `inserted + after`
// first), the cursor doesn't reliably land right after the just-inserted "@Name ", making it
// awkward to keep typing; a bare focus() alone doesn't move the caret to a specific offset.
// (deferred so the textarea's DOM value has actually updated to `inserted + after` first), the
// cursor doesn't reliably land right after the just-inserted "@Name ", making it awkward to keep
// typing; a bare focus() alone doesn't move the caret to a specific offset. Deferred via
// setTimeout(0), not requestAnimationFrame -- rAF callbacks are paused outright while the tab is
// backgrounded/hidden (confirmed live: an rAF scheduled right after a click never fired at all
// in a hidden tab, a setTimeout(0) scheduled the same way fired normally), and this restore needs
// to run regardless of whether the tab happens to be foregrounded at that exact moment.
const el = textareaRef.current;
requestAnimationFrame(() => {
setTimeout(() => {
el?.focus();
el?.setSelectionRange(inserted.length, inserted.length);
});
}, 0);
};
/** README §4.3's composer "@" toolbar icon -- same effect as typing '@' by hand, for groups. */
@@ -381,10 +385,10 @@ export function ConversationView() {
setDraft(next);
setMentionTrigger({ start: pos + insertion.length - 1, query: '' });
setEmojiOpen(false);
requestAnimationFrame(() => {
setTimeout(() => {
el?.focus();
el?.setSelectionRange(pos + insertion.length, pos + insertion.length);
});
}, 0);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -717,8 +721,24 @@ export function ConversationView() {
<div className="absolute bottom-full left-0 z-20 mb-2">
<EmojiPanel
onPick={(emoji) => {
setDraft(draft + emoji);
// Same restore-focus-and-caret dance as insertMentionTrigger/pickMention above
// -- clicking the emoji button moves DOM focus to it like any button click,
// and without this the textarea stays unfocused until the user clicks back
// into it (confirmed by a user report: Enter didn't send right after picking
// an emoji because the textarea itself had lost focus). Inserts at the actual
// caret position rather than always appending to the end, matching how a
// mention insertion already behaves, so the two don't disagree about where
// "here" is. setTimeout(0), not requestAnimationFrame -- see insertMentionTrigger's
// own comment for why (rAF never fires at all while the tab is hidden).
const el = textareaRef.current;
const pos = el?.selectionStart ?? draft.length;
const next = draft.slice(0, pos) + emoji + draft.slice(pos);
setDraft(next);
setEmojiOpen(false);
setTimeout(() => {
el?.focus();
el?.setSelectionRange(pos + emoji.length, pos + emoji.length);
}, 0);
}}
/>
</div>