Envoi sécurisé par IME avec la touche Entrée

Fonctionnalités Web : Événements de clavier

De nombreuses interfaces de chat envoient leur message lorsque l'utilisateur appuie sur Enter dans un <textarea>.

Cela fonctionne pour les utilisateurs qui saisissent du texte directement avec un clavier latin (par exemple, en anglais), mais pas pour ceux qui utilisent un éditeur de mode de saisie (IME) pour rédiger du texte dans des langues comme le japonais, le chinois ou le coréen.

Dans ces contextes, la touche Enter/Return permet de confirmer le candidat à la conversion de caractères actuel. Si un code JavaScript personnalisé écoute keydown sur Enter et envoie immédiatement le message, celui-ci est envoyé alors que l'utilisateur est encore en train de convertir les caractères, ce qui entraîne des messages incomplets, fragmentés ou incorrects.

La vérification de l'état actif de la composition s'applique à tout gestionnaire keydown personnalisé qui agit sur Enter, et pas seulement aux champs <textarea> multilignes. Les champs <input> sur une seule ligne avec leur propre gestion de Enter, tels que les barres de recherche, les champs de renommage intégrés, les champs de boîte de dialogue modale et les palettes de commandes, reçoivent la même touche Enter de confirmation de l'IME qu'un <textarea>, avec isComposing défini sur true. L'envoi implicite natif d'un <form> à partir d'un <input> sans gestionnaire Enter personnalisé ne nécessite aucune gestion explicite, car les navigateurs suppriment l'envoi implicite lorsque la frappe Enter est consommée par un IME.

Stratégie d'implémentation

Pour un <textarea> ou un <input> avec une fonctionnalité personnalisée d'envoi par appui sur la touche Entrée, vérifiez la propriété isComposing native de l'KeyboardEvent avant d'envoyer le contenu. Appelez également event.preventDefault() : l'action par défaut de Enter insère un saut de ligne dans un <textarea>, et dans un <input> à l'intérieur d'un <form>, elle déclenche l'envoi implicite du navigateur, qui enverrait une deuxième fois.

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

Pour un <input> sur une seule ligne, utilisez le même gestionnaire sans la condition !event.shiftKey, qui n'existe que parce que Shift+Enter insère un saut de ligne dans un <textarea>.

Accessibilité et tests

  1. Bouton d'envoi explicite : incluez toujours un élément <button type="submit">. Les raccourcis clavier sont des aides. Ils ne doivent pas remplacer les chemins d'envoi de formulaires natifs.
  2. Entrées accessibles : assurez-vous que tous les éléments <input> et <textarea> sont associés de manière programmatique à un <label> à l'aide des attributs id et for correspondants.

Stratégies de remplacement

Dans les versions récentes de Safari, un problème d'ordre des événements (bug WebKit 165004) transmettait compositionend aux gestionnaires de scripts avant de confirmer Enter keydown, même si les événements sous-jacents étaient distribués dans l'ordre inverse. Au moment où le gestionnaire keydown s'est exécuté, event.isComposing avait déjà été réinitialisé sur false, ce qui signifie que la vérification standard seule n'aurait pas permis d'empêcher l'envoi prématuré.

Si vous devez assurer la compatibilité entre les anciennes versions de Safari et d'autres plates-formes, adoptez l'une des stratégies de remplacement suivantes :

En associant event.isComposing à une vérification de event.keyCode === 229, vous pouvez détecter de manière fiable la confirmation de la touche Enter par Safari. Comme il est imbriqué sous la porte event.key === 'Enter', il est protégé des claviers virtuels mobiles qui pourraient utiliser 229 pour la saisie de la disposition des caractères normaux.

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

Stratégie 2 : Solution de contournement de la fenêtre event.timeStamp (alternative)

Pour les codebases qui interdisent strictement l'utilisation d'API obsolètes comme keyCode, ou si des problèmes connus sont liés à la vérification 229 dans des environnements ciblés spécifiques, vous pouvez suivre le code temporel d'envoi de l'événement signalé par le navigateur.

Comme Safari distribue l'événement keydown de confirmation Enter très près de l'événement compositionend (souvent dans un délai de 5 ms, et parfois avec un code temporel keydown légèrement antérieur en raison de l'inversion de l'ordre de distribution du gestionnaire), la vérification de la différence de temps est très fiable et ne devrait pas déclencher de faux positifs sur les claviers virtuels mobiles dans des conditions standards.

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