fix: respect IME composition in Enter submit flows

This commit is contained in:
vansour
2026-04-15 23:12:47 +08:00
parent 96c4102aa7
commit 74dee6b665
4 changed files with 73 additions and 6 deletions

View File

@@ -446,7 +446,12 @@ $('msg').addEventListener('keydown',e=>{
if(e.key==='ArrowDown'){e.preventDefault();navigateCmdDropdown(1);return;} if(e.key==='ArrowDown'){e.preventDefault();navigateCmdDropdown(1);return;}
if(e.key==='Tab'){e.preventDefault();selectCmdDropdownItem();return;} if(e.key==='Tab'){e.preventDefault();selectCmdDropdownItem();return;}
if(e.key==='Escape'){e.preventDefault();hideCmdDropdown();return;} if(e.key==='Escape'){e.preventDefault();hideCmdDropdown();return;}
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();selectCmdDropdownItem();return;} if(e.key==='Enter'&&!e.shiftKey){
if(e.isComposing){return;}
e.preventDefault();
selectCmdDropdownItem();
return;
}
} }
// Send key: respect user preference. // Send key: respect user preference.
// On touch-primary devices (software keyboard), default to Enter = newline // On touch-primary devices (software keyboard), default to Enter = newline
@@ -454,6 +459,7 @@ $('msg').addEventListener('keydown',e=>{
// The 'ctrl+enter' setting also uses this behavior (Enter = newline). // The 'ctrl+enter' setting also uses this behavior (Enter = newline).
// Users can override in Settings by explicitly choosing 'enter' mode. // Users can override in Settings by explicitly choosing 'enter' mode.
if(e.key==='Enter'){ if(e.key==='Enter'){
if(e.isComposing){return;}
const _mobileDefault=matchMedia('(pointer:coarse)').matches&&window._sendKey==='enter'; const _mobileDefault=matchMedia('(pointer:coarse)').matches&&window._sendKey==='enter';
if(window._sendKey==='ctrl+enter'||_mobileDefault){ if(window._sendKey==='ctrl+enter'||_mobileDefault){
if(e.ctrlKey||e.metaKey){e.preventDefault();send();} if(e.ctrlKey||e.metaKey){e.preventDefault();send();}

View File

@@ -671,7 +671,12 @@ function renderSessionListFromCache(){
setTimeout(()=>{ if(_renamingSid===null) renderSessionListFromCache(); },50); setTimeout(()=>{ if(_renamingSid===null) renderSessionListFromCache(); },50);
}; };
inp.onkeydown=e2=>{ inp.onkeydown=e2=>{
if(e2.key==='Enter'){e2.preventDefault();e2.stopPropagation();finish(true);} if(e2.key==='Enter'){
if(e2.isComposing){return;}
e2.preventDefault();
e2.stopPropagation();
finish(true);
}
if(e2.key==='Escape'){e2.preventDefault();e2.stopPropagation();finish(false);} if(e2.key==='Escape'){e2.preventDefault();e2.stopPropagation();finish(false);}
}; };
// onblur: cancel only -- no accidental saves // onblur: cancel only -- no accidental saves
@@ -888,7 +893,11 @@ function _startProjectCreate(bar, addBtn){
} }
}; };
inp.onkeydown=(e)=>{ inp.onkeydown=(e)=>{
if(e.key==='Enter'){e.preventDefault();finish(true);} if(e.key==='Enter'){
if(e.isComposing){return;}
e.preventDefault();
finish(true);
}
if(e.key==='Escape'){e.preventDefault();finish(false);} if(e.key==='Escape'){e.preventDefault();finish(false);}
}; };
inp.onblur=()=>finish(false); inp.onblur=()=>finish(false);
@@ -910,7 +919,11 @@ function _startProjectRename(proj, chip){
} }
}; };
inp.onkeydown=(e)=>{ inp.onkeydown=(e)=>{
if(e.key==='Enter'){e.preventDefault();finish(true);} if(e.key==='Enter'){
if(e.isComposing){return;}
e.preventDefault();
finish(true);
}
if(e.key==='Escape'){e.preventDefault();finish(false);} if(e.key==='Escape'){e.preventDefault();finish(false);}
}; };
inp.onblur=()=>finish(false); inp.onblur=()=>finish(false);

View File

@@ -767,6 +767,7 @@ function _ensureAppDialogBindings(){
return; return;
} }
if(e.key==='Enter'){ if(e.key==='Enter'){
if(e.isComposing) return;
const target=e.target; const target=e.target;
const isTextarea=target&&target.tagName==='TEXTAREA'; const isTextarea=target&&target.tagName==='TEXTAREA';
if(!isTextarea){ if(!isTextarea){
@@ -1399,7 +1400,7 @@ function editMessage(btn) {
bar.querySelector('.msg-edit-cancel').onclick = () => cancelEdit(row, originalText, body); bar.querySelector('.msg-edit-cancel').onclick = () => cancelEdit(row, originalText, body);
ta.addEventListener('keydown', e => { ta.addEventListener('keydown', e => {
if(e.key==='Enter' && !e.shiftKey) { e.preventDefault(); bar.querySelector('.msg-edit-send').click(); } if(e.key==='Enter' && !e.shiftKey) { if(e.isComposing) return; e.preventDefault(); bar.querySelector('.msg-edit-send').click(); }
if(e.key==='Escape') { e.preventDefault(); cancelEdit(row, originalText, body); } if(e.key==='Escape') { e.preventDefault(); cancelEdit(row, originalText, body); }
}); });
} }
@@ -1719,7 +1720,11 @@ function _renderTreeItems(container, entries, depth){
inp.replaceWith(nameEl); inp.replaceWith(nameEl);
}; };
inp.onkeydown=(e2)=>{ inp.onkeydown=(e2)=>{
if(e2.key==='Enter'){e2.preventDefault();finish(true);} if(e2.key==='Enter'){
if(e2.isComposing){return;}
e2.preventDefault();
finish(true);
}
if(e2.key==='Escape'){e2.preventDefault();finish(false);} if(e2.key==='Escape'){e2.preventDefault();finish(false);}
}; };
inp.onblur=()=>finish(false); inp.onblur=()=>finish(false);

View File

@@ -0,0 +1,43 @@
import pathlib
import re
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
def test_boot_chat_enter_send_respects_ime_composition():
assert re.search(
r"if\(e\.key==='Enter'\)\{\s*if\(e\.isComposing\)\{return;\}",
BOOT_JS,
), "Chat composer Enter handler must ignore IME composition Enter in static/boot.js"
assert re.search(
r"if\(e\.key==='Enter'&&!e\.shiftKey\)\{\s*if\(e\.isComposing\)\{return;\}",
BOOT_JS,
), "Command dropdown Enter handler must ignore IME composition Enter in static/boot.js"
def test_ui_enter_submit_paths_respect_ime_composition():
assert re.search(
r"document\.addEventListener\('keydown',e=>\{[\s\S]*?if\(e\.key==='Enter'\)\{\s*if\(e\.isComposing\) return;",
UI_JS,
), \
"App dialog Enter handler must ignore IME composition Enter in static/ui.js"
assert "if(e.key==='Enter' && !e.shiftKey) { if(e.isComposing) return; e.preventDefault();" in UI_JS, \
"Message edit Enter-to-save handler must ignore IME composition Enter in static/ui.js"
assert re.search(
r"inp\.onkeydown=\(e2\)=>\{\s*if\(e2\.key==='Enter'\)\{\s*if\(e2\.isComposing\)\{return;\}",
UI_JS,
), \
"Workspace rename Enter handler must ignore IME composition Enter in static/ui.js"
def test_sessions_enter_submit_paths_respect_ime_composition():
matches = re.findall(
r"if\(e2?\.key==='Enter'\)\{\s*if\(e2?\.isComposing\)\{return;\}",
SESSIONS_JS,
)
assert len(matches) >= 3, \
"Session and project rename/create Enter handlers must ignore IME composition Enter in static/sessions.js"