IME セーフな Enter キーによる送信

ウェブ機能: キーボード イベント

多くのチャット インターフェースでは、ユーザーが <textarea> で Enter を押すとメッセージが送信されます。

この方法は、ラテン文字キーボードで直接入力するユーザー(英語など)には有効ですが、インプット メソッド エディタ(IME)を使用して日本語、中国語、韓国語などの言語でテキストを作成するユーザーには有効ではありません。

このような状況では、Enter / Return キーを使用して現在の文字変換候補を確定します。カスタム JavaScript が Enter で keydown をリッスンし、すぐに送信すると、ユーザーが文字を変換している間にユーザーのメッセージが送信され、不完全、断片的、または不正確なメッセージになります。

composition-active チェックは、複数行の <textarea> フィールドだけでなく、Enter に作用するカスタム keydown ハンドラにも適用されます。検索バーや検索バー、インラインの名前変更フィールド、モーダル ダイアログ フィールド、コマンド パレットなど、独自の Enter 処理を行う 1 行の <input> フィールドは、<textarea> と同じ IME 確認の Enter keydown を受け取り、isComposing は true に設定されます。カスタム Enter ハンドラのない <input> からの <form> のネイティブな暗黙的な送信は、明示的に処理する必要はありません。これは、Enter キーストロークが IME によって消費されると、ブラウザが暗黙的な送信を抑制するためです。

実装戦略

カスタムの Enter キー送信を使用する <textarea> または <input> の場合、コンテンツを送信する前に KeyboardEvent のネイティブ isComposing プロパティを確認します。また、event.preventDefault() を呼び出します。Enter のデフォルトのアクションは、<textarea> に改行を挿入し、<form> 内の <input> でブラウザの暗黙的な送信をトリガーします。これにより、2 回目の送信が行われます。

<form id="chat-form">
  <label for="chat-input" class="visually-hidden">Message</label>
  <textarea id="chat-input" placeholder="Type a message..."></textarea>
  <button type="submit" id="send-button">Send</button>
</form>
const textarea = document.getElementById('chat-input');
const form = document.getElementById('chat-form');

textarea.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    // Prevent the default action (newline in a textarea, implicit form submission in an input)
    event.preventDefault();

    // If the user is composing text, return early.
    if (event.isComposing) {
      return;
    }

    form.requestSubmit();
  }
});

1 行の <input> の場合は、!event.shiftKey 条件なしで同じハンドラを使用します。この条件は、Shift+Enter が <textarea> に改行を挿入するためにのみ存在します。

ユーザー補助とテスト

  1. 明示的な送信ボタン: 常に <button type="submit"> 要素を含めます。キーボード ショートカットは補助的なものであり、ネイティブのフォーム送信パスを置き換えるものであってはなりません。
  2. アクセス可能な入力: すべての <input> 要素と <textarea> 要素が、一致する id 属性と for 属性を使用して <label> にプログラムで関連付けられていることを確認します。

代替戦略

最近のバージョンの Safari では、イベントの順序付けの問題(WebKit のバグ 165004)により、基盤となるイベントが逆の順序でディスパッチされたにもかかわらず、Enter keydown の確認前に compositionend がスクリプト ハンドラに配信されていました。keydown ハンドラが実行されるまでに、event.isComposing はすでに false にリセットされているため、標準のチェックだけでは、早すぎる送信を防ぐことができません。

古いバージョンの Safari や他のプラットフォームでクロスブラウザ互換性をサポートする必要がある場合は、次のいずれかのフォールバック戦略を採用します。

event.isComposing と event.keyCode === 229 のチェックを組み合わせることで、Safari の順序が異なる Enter keydown の確認を確実にキャッチできます。これは event.key === 'Enter' ゲートの下にネストされているため、通常の文字レイアウト入力に 229 を使用する可能性のあるモバイル仮想キーボードから保護されます。

textarea.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault();

    // Block submission if composing natively or if keyCode is 229
    if (event.isComposing || event.keyCode === 229) {
      return;
    }

    form.requestSubmit();
  }
});

戦略 2: event.timeStamp ウィンドウの回避策(代替案)

keyCode などの非推奨の API の使用を厳しく禁止しているコードベースの場合や、特定のターゲット環境で 229 チェックに関する既知の問題がある場合は、代わりにブラウザから報告されたイベント ディスパッチ タイムスタンプを追跡できます。

Safari は、確認用の Enter keydown イベントを compositionend イベントに非常に近いタイミング(多くの場合 5 ミリ秒以内、ハンドラの配信順序が逆になるため keydown タイムスタンプがわずかに早い場合もあります)でディスパッチするため、時間差の確認は非常に信頼性が高く、標準的な条件下でモバイル仮想キーボードで誤検出が発生する可能性は低くなります。

let lastCompositionEndAt = null;

textarea.addEventListener('compositionend', (event) => {
  // IMPORTANT: Always use event.timeStamp, not Date.now() or performance.now().
  // Handler-time measurements are vulnerable to drift when the main thread is blocked.
  lastCompositionEndAt = event.timeStamp;
});

textarea.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault();

    if (event.isComposing) {
      return;
    }

    // Block submission if the event occurs within a 50ms window of composition ending.
    // Math.abs handles Safari's inverted event delivery timing bug.
    if (
      lastCompositionEndAt !== null &&
      Math.abs(event.timeStamp - lastCompositionEndAt) < 50
    ) {
      return;
    }

    form.requestSubmit();
  }
});