DEV Community

Kenji
Kenji

Posted on AI-assisted

Confirming an English word with Enter sent the chat

The chat sends on Enter. Shift+Enter inserts a newline. With a Chinese IME, Enter is how you keep an English word you typed in that mode, instead of a converted candidate. The field still receives a keydown whose key is Enter. The handler called send, and the word went out as a message.

This is the textarea (or input) event, not a component API. compositionstart, compositionend, blur, and keydown are on the element. A React prop with the same name is just that listener.

isComposing on the keydown is enough in Chromium. The keydown that confirms the composition still has it set, so the handler can return and let the IME write the string. Safari runs the events the other way. compositionend happens first, and the Enter keydown after it already has isComposing false. A check that only reads the event treats that keydown as a normal send.

Keep a flag, set true on compositionstart. On compositionend, clear it with setTimeout(0), not in the listener itself. The Enter keydown is still on the stack when the timeout is queued, so the flag is true and the handler returns. The timeout runs after that turn. The next Enter is a separate keypress, and that one sends.

Leave keyCode === 229 out of the check. During composition, 229 means the key belongs to the IME. Some Android keyboards also report 229 for a plain Enter, and then Enter would never send. isComposing and the flag are the two signals.

compositionend does not always arrive. Switch input method, or leave the field mid-composition, and the flag can stay true. Enter then stops sending until some later composition ends. blur sets the flag false.

Returning early also skips preventDefault, so the IME can still commit the string. A Send button never hits keydown. A click sends whatever is already in the field. Characters still in the preedit are not in that value yet. After the composition ends, Shift+Enter inserts a newline, same as before.

The listeners

const composingRef = useRef(false);

<textarea
  onCompositionStart={() => {
    composingRef.current = true;
  }}
  onCompositionEnd={() => {
    setTimeout(() => {
      composingRef.current = false;
    }, 0);
  }}
  onBlur={() => {
    composingRef.current = false;
  }}
  onKeyDown={(e) => {
    if (e.nativeEvent.isComposing || composingRef.current) return;
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  }}
/>
Enter fullscreen mode Exit fullscreen mode

Without React, add the same four listeners with addEventListener. Read event.isComposing on the keydown. The flag is the piece Safari needs, because isComposing is already false by the time that Enter arrives.

I dispatched the events on a live textarea. Enter during compositionstart did not call preventDefault. Enter in the same turn as compositionend did not either. After the timeout, Enter did.

Top comments (0)