味噌汁のイラスト

フォームの Enter 送信で isComposing と keyCode === 229 を併用する理由

IME の変換確定に使う Enter キーで、書きかけのメッセージが送信されることがあります。isComposing と非推奨の keyCode === 229 を併用して、変換中の Enter を送信処理から除外する理由を解説します。

チャット UI などで Enter キーを押したらメッセージを送信する機能を実装するとき、IME を利用するユーザーへの配慮が必要です。日本語や中国語などの入力では、文字を組み立てるために変換候補を選んでから Enter キーで確定します。単純に event.key === "Enter" だけで送信すると、ユーザーは文字を確定したつもりなのに、書きかけのメッセージが送られてしまいます。

!v(https://videos.ctfassets.net/in6v9lxmm5c8/2PwLZE8dzwixEuoNf1cpSy/07cc88ad3df13884ccbb665917df1bec/ime-enter-submit-3.mp4 482x176)

変換を確定するつもりで押した Enter キーによって、書きかけのメッセージがそのまま送信されてしまっています。

この問題への対策として、次のようなコードを見かけることがあります。

input.addEventListener("keydown", (event) => {
  if (event.isComposing || event.keyCode === 229) return;
  if (event.key === "Enter") {
    // 送信処理...
  }
});

isComposing は、IME で文字を組み立てている間に発生するイベントかどうかを判定するプロパティです。keyCode === 229 は、IME が処理したキー入力で使われてきた値です。どちらも変換中の Enter キーを送信処理から除外するために使われます。

isComposing だけで IME の変換中かどうかを表せるのに、なぜ keyCode も確認するのでしょうか。さらに言えば keyCode は非推奨の API です。この記事では、フォームやチャットの Enter 送信を題材に、2 つの判定を組み合わせる理由と実装時の注意点を説明します。

Enter キーは送信だけでなく変換確定にも使われる

IME(Input Method Editor)は、キー入力を組み合わせて日本語などの文字を入力する仕組みです。たとえば「にほんご」と入力し、「日本語」という候補を選び、Enter キーで確定します。

ブラウザは、このような文字の組み立てを composition セッションとして扱います。UI Events 仕様では、次のイベントが定義されています。

  • compositionstart: 文字の組み立てが始まる
  • compositionupdate: 組み立て中の文字列が更新される
  • compositionend: 組み立てが完了、またはキャンセルされる

キーボードイベントの KeyboardEvent.isComposing は、そのイベントが compositionstart と対応する compositionend の間に発生したかどうかを表します。

そのため、独自の Enter 送信ではまず isComposing を確認し、文字を組み立てている間は送信処理へ進まないようにします。

input.addEventListener("keydown", (event) => {
  if (event.isComposing) return; // 変換中は送信しない
  if (event.key === "Enter") {
    // 送信処理...
  }
});

ただし、ブラウザの実装によっては、IME の変換確定に使う Enter キーが isComposing: false で通知されるという問題が報告されています。

isComposing だけでは確定用の Enter を見分けられない環境がある

仕様が示すイベント順序では、composition セッションを終了させる keydownisComposing: true で通知され、その後に compositionend が発生するとされています。つまり、IME が変換を確定する Enter キーは、isComposing: truekeydown として受け取れるはずです。

一方、WebKit の不具合報告 #165004には、Safari が確定用の keydown より先に compositionend を送っていたことが記録されています。この場合、確定用の Enter を受け取った時点でセッションが終わっているため、isComposingfalse になってしまうのです。

そのため、Safari では isComposing だけでは確定用の Enter を見分けられず、書きかけのメッセージが送信されてしまうことがあったのです。

ここで isComposing に頼らず、compositionstartcompositionend イベントでフラグを更新し、変換中かどうかを自分で管理すればよいのではないかと考えるかもしれません。

let composing = false;
 
input.addEventListener("compositionstart", () => {
  composing = true;
});
 
input.addEventListener("compositionend", () => {
  composing = false;
});
 
input.addEventListener("keydown", (event) => {
  if (composing) return; // 変換中は送信しない
  if (event.key === "Enter") {
    // 送信処理...
  }
});

しかし、この方法でも同じ問題が起こります。フラグを false にする compositionend イベントが確定用の keydown より先に発生してしまうため、keydown を受け取った時点ではフラグがすでに変更されてしまっているからです。問題の原因はイベントの順序そのものにあるので、composition イベントを自分で管理しても回避できないのです。

keyCode === 229 を併用する

keyCode === 229 は、IME が処理するキー入力で使われてきた値です。2010 年の W3C の提案資料には、IME がキー入力を処理している keydown では 229 を返すという計算手順が書かれています。また、Windows の仮想キーコードでは VK_PROCESSKEY0xE5(10 進数で 229)として定義されています。

ただし、現在の UI Events 仕様の legacy keyboard eventsでは、keyCode はシステムや実装に依存する値として扱われています。正式に規定されないまま各ブラウザが独自に実装してきた経緯があり、仕様はその実態を非規範的に記述するにとどめているためです。

Safari の問題でこの値が役立つのは、isComposingfalse になっていても、確定用の keydownkeyCode229 なら独自の送信処理を止められるからです。この回避策は、先ほどの WebKit の不具合報告でも説明されています。

そのため、以下のように isComposingkeyCode === 229 を併用するコードが推奨されているのです。

if (event.isComposing || event.keyCode === 229) {
  return;
}

以下のデモを Safari で実行してみると、isComposingevent.keyCode の値がそれぞれどのように変化するかを確認できます。

Note

Safari のイベント順序の問題には、すでに WebKit 側の修正があります。WebKit Bug #311717。そのため Safari のバージョンによっては既に修正が取り込まれている場合があります。

フォームの暗黙的送信を使えば IME の判定をブラウザに任せられる

ここまでは、keydown で Enter キーを検知して送信する場合の対策を説明しました。しかし、通常のフォームであれば、そもそも Enter キーを JavaScript で検知する必要はありません。ブラウザが備える暗黙的送信(implicit submission)を利用できます。

暗黙的送信とは、テキスト入力欄で Enter キーを押すなどの操作によって、送信ボタンを直接クリックしなくてもフォームを送信する仕組みです。HTML 仕様では、フォームの中で最初に現れる送信ボタンをデフォルトボタン(default button)と呼び、暗黙的送信に対応するブラウザは、そのボタンが無効化されていなければ click イベントを発生させるとされています。送信処理は通常の submit イベントで受け取ります。

たとえば、単一行の入力欄と送信ボタンを持つフォームなら、次のように実装できます。<form> 要素で入力要素を囲み、type="submit" のボタンを配置してフォームとして扱うのがポイントです。

<form id="message-form">
  <label for="message">メッセージ</label>
  <input id="message" name="message" type="text" required />
  <button type="submit">送信</button>
</form>
<p id="result" role="status"></p>
 
<script>
  const form = document.querySelector("#message-form");
  const result = document.querySelector("#result");
 
  form.addEventListener("submit", (event) => {
    event.preventDefault();
    const data = new FormData(form);
    result.textContent = `送信内容: ${data.get("message")}`;
  });
</script>

このコードには、keydown のリスナーも isComposingkeyCode の判定もありません。変換確定の Enter を IME の操作として扱うか、フォームの送信として扱うかはブラウザに任せ、アプリケーションは submit イベントが発生したときだけ処理します。独自のキーイベント処理が変換確定を送信と誤認する原因そのものをなくせるのです。

ただし、textarea の Enter は通常は改行として扱われます。チャット UI のように「Enter で送信、Shift+Enter で改行」という独自の操作を実装する場合は、暗黙的送信だけでは実現できないため、ここまで説明した IME の判定が必要になります。まずは標準のフォーム操作で要件を満たせるかを検討し、独自の Enter 送信が必要な場合にキーイベントを扱うとよいでしょう。

まとめ

  • Enter キーは IME の変換確定にも使われるため、独自の Enter 送信では文字の組み立て中かどうかを確認する
  • isComposing は composition セッション中のキーボードイベントかどうかを表し、IME 変換中の判定に使える
  • Safari では確定用の keydown より先に compositionend が発生する問題が報告されており、isComposingfalse になることがある
  • 上記の問題を回避するため、keyCode === 229 も併用して IME が処理するキー入力かどうかを判定する
  • 通常のフォームでは暗黙的送信と submit イベントを利用することで、独自の Enter 判定や IME の状態管理をブラウザに任せられる

参考

記事の理解度チェック

以下の問題に答えて、記事の理解を深めましょう。

IME 変換中の判定のために非推奨の API keyCode を併用する理由は何ですか?

  • ブラウザの実装によっては isComposing が false になることがあるため

    正解!

    Safari などの一部ブラウザでは、IME の変換確定 Enter が isComposing: false で通知されることがあります。

  • keyCode は IME の種類を判別するために必要だから

    もう一度考えてみましょう

    keyCode は IME の種類を判別する目的では使われません。

  • isComposing は最新の API でありまだ対応していないブラウザがあるため

    もう一度考えてみましょう

    isComposing は広くサポートされています。

  • keyCode は Enter キーの押下を検知するために必要だから

    もう一度考えてみましょう

    Enter キーの押下は event.key で検知できます。

単一行の入力欄を持つ通常のフォームで Enter キーによる送信を実装するとき、まず検討すべき方法はどれですか?

  • form 要素と送信ボタンを配置し、暗黙的送信と submit イベントを利用する

    正解!

    変換確定の Enter を送信として扱うかどうかの判断をブラウザに任せられるため、アプリケーション側で IME の状態を管理する必要がなくなります。

  • input 要素の keydown で Enter キーを検知して送信する

    もう一度考えてみましょう

    独自のキーイベント処理は、変換確定の Enter を送信と誤認する原因になります。

  • compositionend イベントが発生したときに送信処理を実行する

    もう一度考えてみましょう

    compositionend は変換が確定したことを表すイベントであり、送信したいという意図とは対応しません。

  • keydown ではなく keyup で Enter キーを検知して送信する

    もう一度考えてみましょう

    keyup に変えても、そのキー入力が変換確定によるものかどうかを判別できるわけではありません。