Muchas interfaces de chat envían su mensaje cuando el usuario presiona Enter en un <textarea>.
Esto funciona para los usuarios que escriben con un teclado latino directo (p.ej., inglés), pero no para los que escriben con un editor de método de entrada (IME) para redactar texto en idiomas como japonés, chino o coreano.
En estos contextos, la tecla Enter/Return se usa para confirmar el candidato actual de conversión de caracteres. Si el JavaScript personalizado escucha keydown en Enter y envía el mensaje de inmediato, este se envía mientras el usuario aún está convirtiendo caracteres, lo que genera mensajes incompletos, fragmentados o incorrectos.
La verificación de composición activa se aplica a cualquier controlador keydown personalizado que actúe en Enter, no solo a los campos <textarea> de varias líneas. Los campos <input> de una sola línea con su propio control de Enter, como las barras de búsqueda y búsqueda, los campos de cambio de nombre intercalados, los campos de diálogo modal y las paletas de comandos, reciben la misma tecla Enter de confirmación de IME que un <textarea>, con isComposing establecido en true. El envío implícito nativo de un <form> desde un <input> sin un controlador Enter personalizado no necesita un control explícito, ya que los navegadores suprimen el envío implícito cuando un IME consume la pulsación de tecla Enter.
Estrategia de implementación
Para un <textarea> o <input> con envío personalizado con la tecla Intro, verifica la propiedad nativa isComposing del KeyboardEvent antes de enviar el contenido. También llama a event.preventDefault(): la acción predeterminada de Enter inserta una línea nueva en un <textarea>, y en un <input> dentro de un <form>, activa el envío implícito del navegador, que enviaría una segunda vez.
<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();
}
});
Para un <input> de una sola línea, usa el mismo controlador sin la condición !event.shiftKey, que solo existe porque Shift+Enter inserta una línea nueva en un <textarea>.
Accesibilidad y pruebas
- Botón de envío explícito: Incluye siempre un elemento
<button type="submit">. Las combinaciones de teclas son ayudas; no deben reemplazar las rutas de envío de formularios nativas. - Entradas accesibles: Asegúrate de que todos los elementos
<input>y<textarea>estén asociados de forma programática con un<label>usando atributosidyforque coincidan.
Estrategias de resguardo
En las versiones recientes de Safari, un problema de orden de eventos (error 165004 de WebKit) entregó compositionend a los controladores de secuencias de comandos antes de confirmar Enter keydown, aunque los eventos subyacentes se enviaron en el orden opuesto. Cuando se ejecutó el controlador de keydown, event.isComposing ya se había restablecido a false, lo que significa que la verificación estándar por sí sola no habría evitado el envío prematuro.
Si necesitas admitir la compatibilidad entre navegadores en versiones anteriores de Safari y otras plataformas, adopta una de las siguientes estrategias de resguardo:
Estrategia 1: Agrega una verificación de keyCode === 229 (recomendado)
Si vinculas event.isComposing con una verificación de event.keyCode === 229, puedes detectar de forma confiable la confirmación de Enter de Safari fuera de orden. Como está anidado en la puerta event.key === 'Enter', está protegido de los teclados virtuales móviles que podrían usar 229 para la entrada de diseño de caracteres 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();
}
});
Estrategia 2: Solución alternativa de la ventana de event.timeStamp (alternativa)
En el caso de las bases de código que prohíben estrictamente el uso de APIs obsoletas, como keyCode, o si hay problemas conocidos con la verificación de 229 en entornos específicos, puedes hacer un seguimiento de la marca de tiempo de envío de eventos que informa el navegador.
Debido a que Safari envía el evento keydown de confirmación Enter muy cerca del evento compositionend (a menudo, en un plazo de 5 ms y, a veces, con la marca de tiempo de keydown un poco antes debido a la inversión del orden de entrega del controlador), verificar la diferencia de tiempo es muy confiable y es poco probable que active falsos positivos en los teclados virtuales de dispositivos móviles en condiciones estándar.
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();
}
});