IME-veilige enter-to-submit

In veel chatinterfaces wordt het bericht verstuurd als de gebruiker op Enter drukt in een <textarea>.

Dit werkt voor gebruikers die typen met directe invoer via een Latijns toetsenbord (bijvoorbeeld Engels), maar niet voor gebruikers die typen met een Input Method Editor (IME) om tekst samen te stellen in talen zoals Japans, Chinees of Koreaans.

In deze contexten wordt de toets Enter/Return gebruikt om de huidige kandidaat voor tekenconversie te bevestigen. Als aangepaste JavaScript luistert naar keydown op Enter en onmiddellijk verstuurt, wordt het bericht van de gebruiker verstuurd terwijl deze nog tekens converteert. Dit leidt tot onvolledige, gefragmenteerde of onjuiste berichten.

De controle op actieve samenstelling is van toepassing op elke aangepaste keydown-handler die reageert op Enter, niet alleen op velden met meerdere regels <textarea>. <input>-velden met één regel en hun eigen Enter-verwerking, zoals zoek- en vindbalken, inline velden voor naam wijzigen, modale dialoogvelden en opdrachtpaletten, krijgen dezelfde IME-bevestigende Enter-keydown als een <textarea>, waarbij isComposing is ingesteld op true. Voor de systeemeigen impliciete indiening van een <form> vanuit een <input> zonder aangepaste Enter-handler is geen expliciete verwerking nodig, omdat browsers impliciete indiening onderdrukken als de Enter-toetsaanslag wordt verbruikt door een IME.

Implementatiestrategie

Voor een <textarea> of <input> met aangepaste enter-to-submit controleer je de native isComposing-property van de KeyboardEvent voordat je de content indient. Bel ook event.preventDefault(): de standaardactie van Enter voegt een nieuwe regel in een <textarea> in en in een <input> in een <form> activeert deze de impliciete indiening van de browser, waardoor een tweede keer wordt ingediend.

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

Gebruik voor een <input> met één regel dezelfde handler zonder de voorwaarde !event.shiftKey. Deze bestaat alleen omdat Shift+Enter een nieuwe regel invoegt in een <textarea>.

Toegankelijkheid en testen

  1. Expliciete verzendknop: Voeg altijd een <button type="submit">-element toe. Sneltoetsen zijn hulpmiddelen. Ze mogen de standaardpaden voor het indienen van formulieren niet vervangen.
  2. Toegankelijke invoer: Zorg dat alle <input>- en <textarea>-elementen programmatisch zijn gekoppeld aan een <label> met overeenkomende id- en for-kenmerken.

Reservestrategieën

In recente versies van Safari leverde een probleem met de volgorde van gebeurtenissen (WebKit-bug 165004) compositionend aan scripthandlers vóór de bevestigende Enter keydown, ook al werden de onderliggende gebeurtenissen in de omgekeerde volgorde verzonden. Toen de keydown-handler werd uitgevoerd, was event.isComposing al gereset naar false. Dit betekent dat de standaardcontrole alleen niet voldoende was om voortijdige indiening te voorkomen.

Als u compatibiliteit tussen verschillende browsers wilt ondersteunen voor oudere versies van Safari en andere platforms, gebruikt u een van de volgende reservestrategieën:

Door event.isComposing te koppelen aan een controle voor event.keyCode === 229, kun je op betrouwbare wijze de onjuiste bevestiging van de Enter-toetsaanslag door Safari onderscheppen. Omdat dit is genest onder de event.key === 'Enter'-poort, is het veilig voor mobiele virtuele toetsenborden die 229 kunnen gebruiken voor normale invoer van tekenindelingen.

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

Strategie 2: De tijdelijke oplossing met een periode van event.timeStamp (alternatief)

Voor codebases die het gebruik van beëindigde API's zoals keyCode strikt verbieden, of als er bekende problemen zijn met de 229-controle in specifieke doelomgevingen, kunt u in plaats daarvan het tijdstempel voor de verzending van gebeurtenissen bijhouden dat door de browser wordt gerapporteerd.

Omdat Safari de bevestigende Enter keydown-gebeurtenis extreem dicht bij de compositionend-gebeurtenis verzendt (vaak binnen 5 ms en soms met de keydown-tijdstempel iets eerder vanwege de omkering van de volgorde van de handler-levering), is het controleren van het tijdsverschil zeer betrouwbaar en is het onwaarschijnlijk dat er onder standaardomstandigheden valse positieven worden geactiveerd op mobiele virtuele toetsenborden.

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