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

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

日本語で「ありがとうございます」と打って、変換を確定するために Enter を押した。その瞬間、まだ送るつもりのない文字列でチャットが送信されてしまう。こんな経験はありませんか。Rapls AI Chatbot のチャット入力フォームを実装していて、私はまたこのバグに当たりました。原因は、IME の変換確定に使う Enter と、フォーム送信用の Enter を、こちらの実装が区別できていなかったことです。「また」と書いたのは、これが初めてではなかったからです。以前、Xserver 上で Long Polling を使った WordPress 向けのチャットシステムを試作していたときにも、同じ症状を踏んでいました。そのときは、その場しのぎで抑えて終わらせていたのですが、Rapls AI Chatbot を作っている途中で、また同じ問題が顔を出した。「また君か……」と思いました。

先にこの記事の中身を2枚の図で見せておきます。1枚目が実際に誤送信が起きている様子、2枚目が、今回いちばん大事だった「Chrome と Safari でイベントの順序が違う」という核心です。

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

Chrome系 vs Safari のイベント順序差。Safariでは確定EnterのkeydownがisComposing=falseで届く。

今回は自作プラグインの入力欄で起きた問題です。ユーザーが日本語でメッセージを書いている最中に勝手に送信されるのは、チャット UI としてかなり困ります。そこで、今回はちゃんと切り分けることにしました。最初は KeyboardEvent.isComposing を見れば十分だと思っていました。ところが Safari でも確認してみると、isComposing だけでは止めきれないケースがあった。最終的に、私の環境では compositionend の時刻を記録して、その直後の 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() が走ってしまう。ユーザーから見ると、まだ変換を確定しただけなのに勝手に送信された、ように見えます。チャット欄、検索バー、コマンドパレット、問い合わせフォームなど、Enter で何かを実行する UI では同じ問題が起こり得ます。

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

私の環境では、対象の入力欄を開いて、日本語入力モードに切り替え、「ありがとう」と入力し、Space で変換候補を出し、Enter で確定する。この時点で送信処理が走れば、誤送信が起きている、という手順で再現しました。見落としやすいのは、英字入力だけでは再現しにくいことです。実際の IME、つまり macOS 標準 IME、Google 日本語入力、Windows IME、iOS / Android の日本語入力で確認しないと、この問題は素通りしてしまいます。余談ですが、テストを英語キーボードだけで済ませる癖がついていると、この手の「日本語入力でだけ起きるバグ」は本当に見つかりません。私は一度それで痛い目を見ているので、日本語入力での確認だけは省かないようにしています。

最初に試した isComposing チェックは、Safari で取りこぼしました

最初に試したのは、よく紹介されている 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 では、おおむね次の順序になりました。keydown(Enter、isComposing は true)、compositionend(変換確定)、input(確定文字列が反映)、keyup(Enter、isComposing は false)。この順序なら、keydown 時点で e.isComposing が true なので、単純な isComposing チェックでも止められます。表にすると、こうです。

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

一方、私の Safari 18 の環境では、順序が入れ替わるケースを確認しました。compositionend(先に変換が終わる)、input(確定文字列が反映)、keydown(Enter、ここで isComposing は false)、keyup(Enter)。つまり keydown が届いた時点で、すでに compositionend が終わっている。だから e.isComposing は false です。コードから見ると「普通の Enter が押された」と判断され、送信処理を通してしまう。これが Safari での取りこぼしの正体でした。

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

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

ここで、isComposing 単独では足りない、と判断しました。Chrome で動いたから直った、と思い込んでいたら、Safari でそのまま誤送信が残っていたわけです。

最終的に、compositionend の時刻を見る二段ガードに落ち着きました

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

Enter→isComposing→compositionend50ms→送信の二段ガード判定フロー

この実装では、Enter を受け取ったあとに、こう判定しています。まず Enter 以外なら何もしない。次に e.isComposing が true なら、IME 合成中なので送信しない。さらに compositionend から 50ms 未満なら、変換確定 Enter とみなして送信しない。どれにも当てはまらなければ、通常の送信 Enter として扱う。結果として、Chrome 系では主に isComposing が効き、Safari では主に compositionend 直後の時間ガードが効く、という分担になりました。片方だけでは片方のブラウザを取りこぼすので、二段にして両方を拾っている、という形です。

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

50ms という数字は、仕様で決まっている値ではありません。私の環境で何度かログを取ったところ、Safari では compositionend から Enter の keydown までが、おおむね数ms 〜 20ms 程度に収まっていました。そこに余裕を見て、今回は 50ms にしています。ブラウザごとの傾向を表にすると、Chrome 137 は keydown 時点で isComposing が true になりやすいので isComposing でガードでき、Safari 18 は compositionend 後に Enter の keydown が届くケースがあるので直近 50ms の Enter を無視し、Firefox 138 は Chrome 系に近い挙動でした。

ブラウザ 私の環境で確認した傾向 効いた対策
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 では e.nativeEvent.isComposing を見るのが安全でした

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

Reactのイベントハンドラに渡されるSyntheticEventの内部構造と、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 で送信を拾い分ける、という二系統にしておく、という考え方です。

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

enterkeyhint は、仮想キーボードの Enter キーにどのようなラベルやアイコンを表示するかを伝える HTML のグローバル属性です。MDN では、sendsearchdoneenter などの値が説明されています(MDN: enterkeyhint グローバル属性)。チャット欄なら enterkeyhint="send"、検索欄なら enterkeyhint="search" を指定しておくと、スマホのキーボード上でも意図が伝わりやすくなります。

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

自分のフォームで、最後にこの項目を確認しました

実装したあと、私は次の項目を確認しました。日本語入力中に Enter で変換確定したら送信されないこと、変換確定後にもう一度 Enter を押したら送信されること、Shift + Enter で改行されること、Safari で変換確定 Enter が送信扱いにならないこと、Chrome / Firefox で通常送信と IME 確定が分かれること、iOS / Android でフォーム submit でも送信できること。表にすると、こうです。

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

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

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

今回の実装で学んだことを、整理しておきます

今回の問題は、表面的にはとても小さなバグです。Enter を押したら送信される。日本語入力中だけ、それが少し困る。最初はそのくらいに見えました。でも実際に追ってみると、IME、ブラウザ、イベント順序、React の SyntheticEvent、モバイルの仮想キーボードまで関係していました。最終的に、私の実装では keydown で Enter を検知し、isComposing が true なら送信せず、compositionend 直後の Enter も送信せず、React では e.nativeEvent.isComposing を見て、モバイルでは submit イベントも使い、enterkeyhint でスマホのキー表示を整える、という形にしています。これで、私の検証環境では IME 確定 Enter による誤送信を防げました。同じ症状が出ている場合は、まず keydowncompositionstartcompositionupdatecompositionendinputkeyupconsole.log に流してみるのがおすすめです。自分の環境でイベントがどの順番で届いているかを見るだけで、かなり原因を絞れます。

「また君か……」と、もう言わずに済むように

冒頭の「ありがとうございます」に戻ります。変換を確定しようとした Enter で、まだ書き終えていないメッセージが送信されてしまう。あの瞬間の「また君か……」を、私は前に一度踏んだのに、整理しないまま終わらせていました。だから今回また顔を出したわけです。IME 確定 Enter の誤送信は、コードだけ見ると小さな問題に見えます。でも、日本語で入力するユーザーにとっては、かなりストレスの大きいバグです。私も最初は isComposing だけで直ると思っていました。ところが Safari で取りこぼしがあり、最終的には compositionend の時刻も見る形に落ち着いた。今回ちゃんと向き合ったので、次に同じ入力欄を作るときは、最初からこのガードを入れられます。「また君か」と言わずに済むように、このメモを残しておきます。同じところで半日止まっている方の助けになれば嬉しいです。

関連記事

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

コメント

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