fix(desktop): bots group chat sends message on IME composition Enter

macOS Chinese pinyin IME: pressing Enter to confirm a candidate word in
the group-chat composer submitted the draft as a message mid-composition.
The GroupMentionInput onKeyDown checked only `event.key === 'Enter' &&
!event.shiftKey` with no IME guard, unlike the core composer which guards
isComposing + keyCode 229 (#44135).

Add the same guard to the three Enter handlers in the bots plugin:
- GroupMentionInput (group composer + reply box) — the reported bug
- GroupClarifyCard free-text answer input — same premature-submit
- skill-hub search input — same premature-trigger

Closes #93528
This commit is contained in:
quexiaolong
2026-08-24 12:08:03 +08:00
committed by Teknium
parent ed8ee9a871
commit 7e67f64fce
3 changed files with 44 additions and 0 deletions
@@ -8951,6 +8951,10 @@ function HubSkillsSection({ forProfile, onInstalled }) {
value: query,
onChange: event => setQuery(event.target.value),
onKeyDown: event => {
// IME guard: Enter confirming a composed word must not search.
if (event.nativeEvent?.isComposing || event.keyCode === 229) {
return
}
if (event.key === 'Enter') {
event.preventDefault()
void search()
@@ -11859,6 +11863,14 @@ function GroupMentionInput({ members, onChange, onSubmitDraft, value, ...inputPr
},
onClick: event => refreshToken(event.target),
onKeyDown: event => {
// IME composition guard (same as the core composer): Enter here
// confirms the composed Chinese/Japanese/Korean text — it must not
// insert a mention nor submit the draft. nativeEvent.isComposing
// covers Chromium; keyCode 229 covers macOS Chinese IMEs that fire
// Enter after compositionend with isComposing already false.
if (event.nativeEvent?.isComposing || event.keyCode === 229) {
return
}
if (open) {
if (event.key === 'ArrowDown') {
event.preventDefault()
@@ -12045,6 +12057,10 @@ function GroupClarifyCard({ entry, members }) {
setDrafts(prev => ({ ...prev, [q.qid]: value }))
},
onKeyDown: event => {
// IME guard: Enter confirming a composed word must not submit.
if (event.nativeEvent?.isComposing || event.keyCode === 229) {
return
}
if (event.key === 'Enter' && questions.length === 1) {
event.preventDefault()
void submit()
@@ -31,6 +31,33 @@ test('room composer is a textarea with Enter=submit, Shift+Enter=newline (#89884
assert.match(component, /insert\(options\[active\]\.handle\)/)
})
test('room composer swallows IME composition Enters before submit/mention (#93528)', () => {
// macOS Chinese IME: Enter that confirms a candidate word fires a keydown
// the composer mistook for a send — the message went out mid-composition.
// The guard must run BEFORE the popover branch AND the submit branch, and
// cover both Chromium's isComposing flag and the keyCode 229 legacy
// VK_PROCESSKEY that macOS IMEs emit after compositionend.
const start = source.indexOf('function GroupMentionInput')
const nextFn = source.indexOf('\nfunction ', start + 1)
const component = source.slice(start, nextFn)
const guard = component.indexOf('event.nativeEvent?.isComposing || event.keyCode === 229')
assert.ok(guard >= 0, 'IME guard present in GroupMentionInput')
const afterGuard = component.slice(guard)
// Guard returns immediately (no mention insert, no submit)...
assert.match(afterGuard.slice(0, 80), /return/)
// ...and it sits before both the popover branch and the submit branch.
assert.ok(afterGuard.indexOf('if (open) {') > 0, 'guard precedes popover branch')
assert.ok(component.indexOf("event.key === 'Enter' && !event.shiftKey") > guard, 'guard precedes submit branch')
})
test('clarify free-text input swallows IME composition Enters (#93528)', () => {
const clarify = source.slice(source.indexOf('function GroupClarifyCard'), source.indexOf('function openGroupChat'))
const guard = clarify.indexOf('event.nativeEvent?.isComposing || event.keyCode === 229')
assert.ok(guard >= 0, 'IME guard present in GroupClarifyCard')
assert.ok(clarify.indexOf("event.key === 'Enter' && questions.length === 1") > guard, 'guard precedes submit branch')
})
test('both room composers wire onSubmitDraft (#89884)', () => {
assert.match(source, /onSubmitDraft: submit,/)
assert.match(source, /onSubmitDraft: \(\) => submitReply\(id\),/)
@@ -0,0 +1 @@
ShonnQ