Pengiriman dengan tombol Enter yang aman untuk IME

Fitur web: Peristiwa keyboard

Banyak antarmuka chat mengirimkan pesan saat pengguna menekan Enter di <textarea>.

Hal ini berfungsi untuk pengguna yang mengetik dengan input keyboard Latin langsung (misalnya, bahasa Inggris), tetapi tidak berfungsi untuk pengguna yang mengetik dengan Editor Metode Input (IME) untuk menulis teks dalam bahasa seperti Jepang, China, atau Korea.

Dalam konteks ini, tombol Enter/Return digunakan untuk mengonfirmasi kandidat konversi karakter saat ini. Jika JavaScript kustom memproses keydown di Enter dan langsung mengirimkannya, pesan pengguna akan dikirim saat mereka masih mengonversi karakter, sehingga menghasilkan pesan yang tidak lengkap, terfragmentasi, atau salah.

Pemeriksaan composition-active berlaku untuk pengendali keydown kustom apa pun yang bertindak pada Enter, tidak hanya untuk kolom <textarea> multibaris. Kolom <input> satu baris dengan penanganan Enter-nya sendiri, seperti kotak penelusuran dan temukan, kolom ganti nama inline, kolom dialog modal, dan palet perintah, menerima keydown Enter yang mengonfirmasi IME yang sama seperti <textarea>, dengan isComposing ditetapkan ke true. Pengiriman implisit native <form> dari <input> tanpa pengendali Enter kustom tidak memerlukan penanganan eksplisit, karena browser menyembunyikan pengiriman implisit saat penekanan tombol Enter digunakan oleh IME.

Strategi penerapan

Untuk <textarea> atau <input> dengan enter-to-submit kustom, periksa properti isComposing native dari KeyboardEvent sebelum mengirimkan konten. Panggil juga event.preventDefault(): tindakan default Enter menyisipkan baris baru di <textarea>, dan di <input> di dalam <form>, tindakan ini memicu pengiriman implisit browser, yang akan mengirimkan untuk kedua kalinya.

<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();
  }
});

Untuk <input> satu baris, gunakan pengendali yang sama tanpa kondisi !event.shiftKey, yang hanya ada karena Shift+Enter menyisipkan baris baru di <textarea>.

Aksesibilitas dan pengujian

  1. Tombol kirim yang jelas: Selalu sertakan elemen <button type="submit">. Pintasan keyboard adalah alat bantu; pintasan tersebut tidak boleh menggantikan jalur pengiriman formulir native.
  2. Input yang dapat diakses: Pastikan semua elemen <input> dan <textarea> dikaitkan secara terprogram dengan <label> menggunakan atribut id dan for yang cocok.

Strategi penggantian

Dalam versi Safari terbaru, masalah pengurutan peristiwa (bug WebKit 165004) mengirimkan compositionend ke pengendali skrip sebelum mengonfirmasi Enter keydown, meskipun peristiwa yang mendasarinya dikirim dalam urutan yang berlawanan. Saat pengendali keydown berjalan, event.isComposing sudah direset ke false, yang berarti pemeriksaan standar saja akan gagal mencegah pengiriman prematur.

Jika Anda perlu mendukung kompatibilitas lintas browser di versi Safari yang lebih lama dan platform lainnya, gunakan salah satu strategi penggantian berikut:

Dengan menyambungkan event.isComposing dengan pemeriksaan untuk event.keyCode === 229, Anda dapat dengan andal menangkap penekanan tombol Enter yang mengonfirmasi ketidakteraturan Safari. Karena ini disusun bertingkat di bawah gerbang event.key === 'Enter', ini aman dari keyboard virtual seluler yang mungkin menggunakan 229 untuk entri tata letak karakter normal.

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();
  }
});

Strategi 2: Solusi periode event.timeStamp (alternatif)

Untuk codebase yang melarang penggunaan API yang tidak digunakan lagi seperti keyCode, atau jika ada masalah umum terkait pemeriksaan 229 di lingkungan target tertentu, Anda dapat melacak stempel waktu pengiriman peristiwa yang dilaporkan browser.

Karena Safari mengirimkan peristiwa keydown Enter konfirmasi sangat dekat dengan peristiwa compositionend (sering kali dalam waktu 5 milidetik, dan terkadang dengan stempel waktu keydown sedikit lebih awal karena inversi urutan pengiriman pengendali), memeriksa perbedaan waktu sangat andal dan tidak mungkin memicu positif palsu pada keyboard virtual seluler dalam kondisi standar.

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();
  }
});