ReactのIME入力バグはcomposition中のkeydownをguardする
Reactのsearch box、command palette、autocomplete、rename fieldが英語入力では動くのに日本語入力でおかしい場合、keyboard handlerを見ます。
IMEユーザーは、最終文字を1キーずつ直接入力していません。まず変換中のtextをcomposeし、その後commitします。そのcomposition中にもbrowserはkeyboard eventを発火します。そこで Enter、ArrowDown、Escape を通常shortcutとして扱うと、未確定textをsubmitしたり、dropdownを誤選択したり、popupを早く閉じたりします。
症状
壊れたautocompleteはこうなりがちです。
function SearchBox() {
const [selectedIndex, setSelectedIndex] = useState(0);
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === "ArrowDown") {
setSelectedIndex((index) => index + 1);
event.preventDefault();
}
if (event.key === "Enter") {
submitSelectedItem();
}
}
return <input onKeyDown={handleKeyDown} />;
}
Latin keyboardでは動きます。しかし日本語IME変換中は、commit前に同じkey eventが来ることがあります。
composition中はguardする
isComposing と互換用の keyCode === 229 を見ます。
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.nativeEvent.isComposing || event.keyCode === 229) {
return;
}
if (event.key === "ArrowDown") {
setSelectedIndex((index) => Math.min(index + 1, items.length - 1));
event.preventDefault();
}
if (event.key === "Enter") {
submitSelectedItem();
event.preventDefault();
}
}
isComposing が読みやすい現代的なsignalです。keyCode === 229 は、IME関連eventの互換fallbackとしてまだ役に立ちます。
final textはcompositionend後に扱う
commit後にsearchやvalidationを走らせるなら、composition endで処理します。
function SearchBox() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(event) => setQuery(event.currentTarget.value)}
onCompositionEnd={(event) => {
setQuery(event.currentTarget.value);
runSearch(event.currentTarget.value);
}}
onKeyDown={handleKeyDown}
/>
);
}
textのsource of truthをkeydownだけにしないことです。input/changeとcomposition eventがtextを管理し、keydownはcompositionが終わった後のnavigationだけを担当します。
text input周辺のshortcutを全部見る
1つのinputだけ直して、別componentが残ることがよくあります。
確認対象です。
- search box
- autocomplete dropdown
- command palette
- rename field
- tag editor
- chat input
- editable table cell
Enterでsubmitするform
selection、commit、submit、popup closeを行うshortcutは、composition中にskipします。
accessibilityを壊さない
IME対応とkeyboard accessibilityは対立しません。ルールはこうです。
composition中:
text compositionがkeyを所有する
composition後:
component keyboard shortcutが効く
commit後は、矢印キーでlistを移動し、Enterで確定できる必要があります。
テストケース
実IMEで確認します。
- inputに日本語を入力する
- candidate windowが開いている間に矢印キーを押す
- app側dropdownが早く動かないことを確認する
- IME textをcommitする
- もう一度矢印キーを押す
- app側dropdownが通常通り動くことを確認する
- commit後にEnterを押す
- preeditではなくfinal textがsubmitされることを確認する
handler logicは自動テストできますが、この問題はbrowser event orderingが本体なので、実IMEの手動確認も必要です。