日本語入力のEnterでフォームが誤送信される問題を直した話|Safari・React・Vue対応

日本語入力のEnterでフォームが誤送信される問題を直した話|Safari・React・Vue対応 Web Development
この記事は約17分で読めます。

Rapls AI Chatbot のチャット入力フォームを実装していて、また見覚えのあるバグに当たりました。

日本語で「ありがとうございます」と打って、変換を確定するために Enter を押した瞬間、まだ送るつもりのない文字列でチャットが送信されてしまったのです。

原因は、IME の変換確定に使う Enter と、フォーム送信用の Enter を、こちらの実装が区別できていなかったことでした。

「見覚えのある」と書いたのは、これが初めてではなかったからです。以前、Xserver 上で Long Polling を使った WordPress 向けのチャットシステムを試作していたときにも、同じ症状を踏んでいました。そのときは、その場しのぎで抑えて終わらせていたのですが、Rapls AI Chatbot を作っている途中で、また同じ問題が顔を出しました。

「また君か……」と思いました。

ただ、今回は自作プラグインの入力欄で起きた問題です。ユーザーが日本語でメッセージを書いている最中に勝手に送信されるのは、チャット UI としてかなり困ります。そこで、今回はちゃんと切り分けることにしました。

最初は KeyboardEvent.isComposing を見れば十分だと思っていました。MDN Web Docs でも、isComposingcompositionstart の後から compositionend の前までの合成セッション中かどうかを示すプロパティとして説明されています。MDN: KeyboardEvent.isComposing

ところが、実際に Safari でも確認してみると、isComposing だけでは止めきれないケースがありました。最終的に、私の環境では compositionend の時刻を記録して、その直後の Enter を捨てる実装に落ち着きました。

この記事では、IME 確定 Enter でフォームが誤送信される問題について、私が実際に試したこと、ブラウザごとの挙動差、最終的に採用したコードをまとめます。

日本語入力の変換確定Enterでチャットが誤送信される

先に結論だけ書くと

IME の変換確定 Enter とフォーム送信 Enter の区別は、isComposing だけでは取りこぼす場合があります。私の検証環境では、keydown 側で isComposing を見るだけでなく、compositionend の時刻も記録し、その直後(今回は 50ms 以内)の Enter を無視する二段ガードが一番安定しました。

検証時の環境:macOS 14 / Chrome 137 / Safari 18 / Firefox 138 / WordPress 6.9.4 / Cocoon 2.9.0 / 確認時期:2026年5月。この記事のブラウザ差分は、あくまで私の手元で確認した結果です。OS、IME、ブラウザバージョンによって挙動が変わる可能性があります。

何が起きていたか

入力フォームに、こんなシンプルな処理を書いていたとします。

英語入力だけなら、これでも大きな問題は起きにくいです。Enter を押したら送信される。それだけです。

しかし、日本語入力では話が変わります。

たとえば「ありがとう」と入力し、Space で変換候補を出し、Enter で確定する。この Enter は、本来「フォームを送信するための Enter」ではありません。IME に対して「この変換で確定します」と伝えるための Enter です。

ところが、フォーム側のコードはその区別をしていません。e.key === 'Enter' だけを見ているので、変換確定の Enter でも sendMessage() が走ってしまいます。

IMEの変換確定Enterがフォーム送信として扱われる仕組み

ユーザーから見ると、「まだ変換を確定しただけなのに、勝手に送信された」ように見えます。チャット欄、検索バー、コマンドパレット、問い合わせフォームなど、Enter で何かを実行する UI では同じ問題が起こり得ます。

私の環境で再現した手順

  1. 対象の入力欄を開く
  2. 日本語入力モードに切り替える
  3. 「ありがとう」と入力する
  4. Space を押して変換候補を出す
  5. Enter を押して変換を確定する
  6. この時点で送信処理が走れば、誤送信が起きている

ポイントは、英字入力だけでは再現しにくいことです。実際の IME、つまり macOS 標準 IME、Google 日本語入力、Windows IME、iOS/Android の日本語入力で確認しないと、この問題は見落としやすいです。

isComposing だけでは取りこぼしました

最初に試したのは、よく紹介されている isComposing チェックです。

KeyboardEvent.isComposing は、キーボードイベントが文字の合成セッション中に発火したかどうかを示す読み取り専用プロパティです。MDN では、compositionstart の後から compositionend の前までを合成中として説明しています。MDN: KeyboardEvent.isComposing

そのため、最初は「Enter が押されたときに e.isComposing が true なら無視すればいい」と考えました。

Chrome では、この考え方でかなり素直に止まりました。ところが、Safari で試すと、まだ送信が走るケースがありました。

ここで分かったのは、変換確定 Enter のイベント順序が、ブラウザごとにきれいに揃っていないということです。

isComposingがtrueからfalseに切り替わるタイミングと、確定EnterがChrome系とSafariで異なる位置に届くことを示すタイムライン図

ブラウザ別にイベント順序を確認しました

原因を確認するために、keydowncompositionstartcompositionupdatecompositionendinputkeyup をすべて console.log に出しました。

DevTools ConsoleでIME関連イベントの発火順序を確認している画面

私の環境では、Chrome / Edge / Firefox では、おおむねこちらの順序になりました。

順序 イベント isComposing 見え方
1 keydown(Enter) true まだ合成中として扱える
2 compositionend 変換確定
3 input 確定文字列が反映される
4 keyup(Enter) false キーを離す

この順序なら、keydown 時点で e.isComposing が true なので、単純な isComposing チェックでも止められます。

一方、私の Safari 18 の環境では、こちらの順序になるケースを確認しました。

順序 イベント isComposing 見え方
1 compositionend 先に変換が終わる
2 input 確定文字列が反映される
3 keydown(Enter) false 送信用Enterのように見えてしまう
4 keyup(Enter) false キーを離す

Chrome系ブラウザとSafariでIME確定時のイベント発火順序が異なることを示す比較図

この場合、keydown が届いた時点では、すでに compositionend が終わっています。つまり e.isComposing は false です。

そのため、コードから見ると「普通の Enter が押された」と判断され、送信処理を通してしまいます。

ここで、isComposing 単独では足りないと判断しました。

最終的に採用したのは compositionend の時刻を見る方法です

私の手元で安定したのは、isComposing に加えて、compositionend の直後に来た Enter を無視する方法です。

isComposingとcompositionendの時刻を使った二段ガードの判定フロー

この実装では、Enter を受け取ったあとに、次の順番で判定しています。

  1. Enter 以外なら何もしない
  2. e.isComposing が true なら、IME 合成中なので送信しない
  3. compositionend から 50ms 未満なら、変換確定 Enter とみなして送信しない
  4. どちらにも当てはまらなければ、通常の送信 Enter として扱う

Chrome 系では主に isComposing が効き、Safari では主に compositionend 直後の時間ガードが効く、という分担になりました。

50ms にした理由

50ms という数字は、仕様で決まっている値ではありません。私の環境で何度かログを取ったところ、Safari では compositionend から Enter の keydown までが、おおむね数ms〜20ms程度に収まっていました。そこに余裕を見て、今回は 50ms にしました。

ブラウザ 私の環境で確認した傾向 効いた対策
Chrome 137 keydown 時点で isComposing が true になりやすい isComposing でガード
Safari 18 compositionend 後に Enter の keydown が届くケースを確認 直近 50ms の Enter を無視
Firefox 138 Chrome 系に近い挙動を確認 isComposing でガード

compositionendの発火時刻と確定Enterのkeydownまでの時間が、Safariでは5〜20msに収まることを示す測定図

この値を大きくしすぎると、変換確定後にユーザーがすぐもう一度 Enter を押した場合、その送信操作まで止めてしまう可能性があります。逆に小さすぎると Safari 側の取りこぼしが出る可能性があります。

私のチャット UI では 50ms で違和感はありませんでした。ただし、これは私の環境での実装メモです。実運用では、対象ユーザーの環境で 30ms、50ms、80ms くらいを試して決めるのが安全だと思います。

input イベント側で送信しないようにしました

もうひとつ意識したのは、合成中の input イベントで送信ロジックを動かさないことです。

IME 入力中の input イベントでは、inputTypeinsertCompositionText などになることがあります。入力値の反映や候補表示なら input でよいのですが、送信のような確定操作まで混ぜると、イベントの見通しが悪くなります。

私の場合、送信トリガーは keydown に寄せ、input は入力値の追従だけにしました。その方が、あとからログを見たときにも「どこで送信されたか」を追いやすかったです。

React では nativeEvent を見るのが安全でした

React で同じ実装をするときは、少し注意が必要でした。

React のイベントハンドラに渡ってくるイベントは、ブラウザのイベントをそのまま渡しているのではなく、SyntheticEvent というラッパーです。React の旧公式ドキュメントでも、SyntheticEvent はブラウザのネイティブイベントを包むクロスブラウザ用のラッパーであり、必要に応じて nativeEvent から元のイベントにアクセスできると説明されています。React: SyntheticEvent

Reactのイベントハンドラに渡されるSyntheticEventの内部構造と、isComposingプロパティを参照する正しい経路を示す図

そのため、私は React では e.isComposing ではなく、e.nativeEvent.isComposing を見る形にしました。

タイムスタンプの保持には useState ではなく useRef を使いました。この値は画面表示に使うものではなく、イベントハンドラ内で最新値を参照したいだけです。useState にすると再レンダリングが増え、タイミング判定の用途としては扱いづらくなります。

Vue では @keydown.enter だけに頼らない

Vue 3 の Composition API で書くなら、こんな形にしました。

Vue には @keyup.enter@keydown.enter のようなキー修飾子があります。公式ドキュメントでも、@keyup.enter のように特定のキーのときだけハンドラを呼ぶ書き方が紹介されています。Vue: Key Modifiers

ただし、今回のように IME の合成状態を見たい場合は、単に @keydown.enter だけで済ませず、ハンドラ内で e.isComposingcompositionend 直後の判定を入れた方が安全でした。

チャットUIでは Shift + Enter も忘れない

Rapls AI Chatbot のようなチャット UI では、Enter で送信、Shift + Enter で改行、という操作にしています。

この場合も、IME 判定を先に行い、そのあとで Shift + Enter を改行として通すようにしました。

長文入力欄なら、Enter は改行、Cmd/Ctrl + Enter で送信の方が自然なこともあります。

ここは UI の性格で決めます。短いチャットなら Enter 送信でもよいですが、問い合わせフォームや長文メモでは、Enter を送信に割り当てると入力ミスが増えやすいです。

モバイルでは keydown だけに寄せない

モバイル、特に iOS Safari では、ソフトウェアキーボードの「return」や「送信」キーが、デスクトップのような keydown として扱いやすいとは限りません。

そこで、モバイルではフォームの submit イベントでも拾えるようにしておく方が安定しました。

デスクトップはkeydownイベント、モバイルはsubmitイベントで送信を拾い分ける構成図

enterkeyhint は、仮想キーボードの Enter キーにどのようなラベルやアイコンを表示するかを伝える HTML のグローバル属性です。MDN では、sendsearchdoneenter などの値が説明されています。MDN: enterkeyhint グローバル属性

チャット欄なら enterkeyhint="send"、検索欄なら enterkeyhint="search" を指定しておくと、スマホのキーボード上でも意図が伝わりやすくなります。

iOS Safariのソフトウェアキーボード右下キーがenterkeyhint属性の値で変化する様子を示すスクリーンショット比較

自分のフォームで確認するときのチェック項目

実装したあと、私は次の項目を確認しました。

確認項目 OKの状態
日本語入力中に Enter で変換確定 送信されない
変換確定後にもう一度 Enter 送信される
Shift + Enter 改行される
Safari 変換確定 Enter が送信扱いにならない
Chrome / Firefox 通常送信とIME確定が分かれる
iOS / Android フォーム submit でも送信できる

IME Enter誤送信を防ぐためのブラウザ・IME・デバイス・操作別の動作確認チェックリスト

特に Safari の確認は外さない方がいいです。Chrome だけで動いた状態を見て「直った」と思うと、私のようにあとで Safari に引っかかる可能性があります。

今回の実装で学んだこと

今回の問題は、表面的にはとても小さなバグです。Enter を押したら送信される。日本語入力中だけ、それが少し困る。最初はそのくらいに見えました。

でも実際に追ってみると、IME、ブラウザ、イベント順序、React の SyntheticEvent、モバイルの仮想キーボードまで関係していました。

最終的に、私の実装では次の形にしています。

  1. keydown で Enter を検知する
  2. isComposing が true なら送信しない
  3. compositionend 直後の Enter も送信しない
  4. React では e.nativeEvent.isComposing を見る
  5. モバイルでは submit イベントも使う
  6. enterkeyhint でスマホのキー表示を整える

これで、私の検証環境では IME 確定 Enter による誤送信を防げました。

最後に

IME 確定 Enter の誤送信は、コードだけ見ると小さな問題に見えます。でも、日本語で入力するユーザーにとっては、かなりストレスの大きいバグです。

私も最初は isComposing だけで直ると思っていました。ところが、Safari で取りこぼしがあり、最終的には compositionend の時刻も見る形に落ち着きました。

同じ症状が出ている場合は、まず keydowncompositionstartcompositionupdatecompositionendinputkeyupconsole.log に流してみるのがおすすめです。自分の環境でイベントがどの順番で届いているかを見るだけで、かなり原因を絞れます。

私自身、前に一度この問題を踏んだときは、ちゃんと整理しないまま終わらせてしまいました。今回あらためて向き合ったことで、次に同じ入力欄を作るときは、最初からこのガードを入れられます。同じところで半日止まっている方の助けになれば嬉しいです。

関連記事

参考にした公式ドキュメント

コメント

タイトルとURLをコピーしました