DEV Community

greymoth
greymoth

Posted on

Your main input handles IME composition. The rename box next to it doesn't.

Almost every app I look at guards its primary text input against IME composition. The search box, the inline rename field, the tag input, the modal next to it: those get forgotten. That's where the same bug keeps living.

I've been sending one-line fixes for this across a bunch of editors and AI tools for a while now, and at this point it's predictable enough that I can usually guess which file the bug is in before I open the repo.

the bug, in 30 seconds

When you type Japanese (or Chinese, or Korean) you don't type final characters. You type romaji, an IME shows a preedit, and you press Enter or Space to confirm the conversion into kanji. That confirming Enter is the same physical Enter your form listens for.

So a user is mid-word, hits Enter to pick the right kanji, and your handler fires onSearch or commitName or handleSave on text that isn't finished yet. No error. No stack trace. CI is green. It only happens with an IME turned on, which most of the maintainers don't have, so it sits there.

The fix is one property. While a composition is active, isComposing is true:

// before
if (e.key === 'Enter' && value.length > 0) {
  onSearch(value);
}

// after
if (e.key === 'Enter' && !e.nativeEvent.isComposing && value.length > 0) {
  onSearch(value);
}
Enter fullscreen mode Exit fullscreen mode

That's the whole thing. (payloadcms/payload#17138, one line.)

the part I actually want to point at

Here's what made me start writing this down. The codebases usually already know about the bug. They just stopped one input short.

In LibreChat the main message textarea is guarded. The fix I sent for the prompt-name field, the labels form, and the dynamic tag input has a comment I left pointing right at it: Ignore the Enter that commits an IME composition (see useTextarea.ts). The knowledge was in the repo. It just never made it to the three smaller inputs sitting beside the composer. (danny-avila/LibreChat#13996)

Trilium was even clearer. It already had a helper, isIMEComposing, living in services/shortcuts, used by the note editor. The board view's card and column title editors just didn't import it. Same repo, same helper, one screen over, unguarded. (TriliumNext/Trilium#10315)

So this isn't really "teams don't know about IME." It's that the guard lives on the input everyone tests, and the secondary inputs are the ones nobody types Japanese into during review.

where it hides

If you go looking, the spots repeat. In Jan it was the add-project and rename-thread dialogs (menloresearch/jan#8359). In Excalidraw it was the search menu's Enter-to-jump-to-next-match (excalidraw/excalidraw#11573). In Twenty it was attachment rename and the AI chat thread title (twentyhq/twenty#22270).

Search, rename, tag/chip, dialog. Four shapes, over and over. The early-return form is what most of them ended up with:

const onKeyDown = (e) => {
  if (e.nativeEvent.isComposing || e.keyCode === 229) {
    return;
  }
  if (e.key === 'Enter') {
    commit();
  }
};
Enter fullscreen mode Exit fullscreen mode

Two things worth knowing if you go to write this yourself.

In React you reach through to e.nativeEvent.isComposing. Every one of these fixes does that rather than trust the synthetic event. And the || e.keyCode === 229 is a legacy fallback: on some code paths the keydown that fires mid-composition reports keyCode 229 instead of setting isComposing. There's also a genuinely fiddly bit at the exact moment composition ends, where isComposing can already read false on the very Enter that confirms, depending on the browser. I haven't found one clean rule that holds everywhere. The belt-and-suspenders check of both is what's survived for me in practice.

finding it in your own app

You don't need a tool. Switch your keyboard to a Japanese IME, then type into every input that does something on Enter and watch what fires before you've confirmed the word. The composer will probably be fine. Try the search box. Try the inline rename. Try the chip input in a settings panel.

Or grep. Find every key === 'Enter' (or your keymap's equivalent) and check each one for a composition guard. The main one will have it. Count how many of the rest don't.

One honest note on numbers, since the links above are the evidence. Two of these have merged as I write this, the payload and Twenty fixes; the rest are still open. I'd rather point at the ones that landed than claim I swept the ecosystem. The shape is identical in all of them, which is sort of the point: the guard sits on the input everyone tests and stops one box short.

It's a small fix. It stays unfixed because it's invisible to the people writing the code, and the people who hit it ten times a day mostly shrug and don't report it. If you ship anything with a text input, it's worth ten minutes with an IME on.

Top comments (2)

Collapse
 
frank_signorini profile image
Frank

This is such a common oversight! I'

Collapse
 
greymothjp profile image
greymoth

Yeah, and it's weirdly consistent once you start looking. The main composer usually guards isComposing fine, then a rename field or command palette right beside it gets missed. Ran into the same gap in a bunch of editors and chat apps this week.