Type きょう into a search box, press the spacebar to convert it to 今日, and press Enter to accept the kanji. On a lot of sites the search fires right then — on きょう, or on nothing, or it submits the whole form. You wanted to pick a word. The page heard go.
If you only ever type English you will never reproduce this, because you never compose. That is exactly why it ships. The person who wrote the handler pressed Enter a thousand times and it always meant submit.
The Enter that confirms is the same Enter you're listening for
An IME turns keystrokes into candidate text and waits for you to confirm. The confirming keypress is usually Enter. The problem is that your keydown listener sees that Enter too, and by default it can't tell "commit this conversion" apart from "submit the form."
The browser does leave you a tell. While the IME is composing, a keydown carries isComposing === true, and — going further back — reports keyCode === 229 instead of the real key. The Enter that closes the conversion is a composing keydown. The Enter you actually want, the one after the word is settled, is not.
The fix is a guard clause
Bail out of the handler while composition is in flight:
input.addEventListener("keydown", (e) => {
if (e.isComposing || e.keyCode === 229) return; // still converting
if (e.key === "Enter") submit();
});
isComposing is the modern, readable check. keyCode === 229 covers browsers old enough not to set it. Keeping both costs nothing and the second one has saved me on a stock Android WebView more than once.
React hides the flag one level down
React wraps the DOM event, and on the synthetic event isComposing is not reliably populated. The value you want is on the native event:
- onKeyDown={(e) => { if (e.key === "Enter") search(); }}
+ onKeyDown={(e) => {
+ if (e.nativeEvent.isComposing) return;
+ if (e.key === "Enter") search();
+ }}
Same bug, same one-line fix, just reached through nativeEvent. This is the version I paste into most codebases, because most of them are React and most of them read e.isComposing, find it undefined, and quietly do nothing.
Tracking composition yourself
If you'd rather hold the state explicitly — say you toggle other behavior during composition — the events are compositionstart and compositionend:
let composing = false;
el.addEventListener("compositionstart", () => (composing = true));
el.addEventListener("compositionend", () => (composing = false));
el.addEventListener("keydown", (e) => {
if (composing) return;
if (e.key === "Enter") submit();
});
Where it stops
The flag approach has one sharp edge worth knowing. Browsers don't agree on the order of the last two events. In some, compositionend fires before the confirming keydown, so your composing flag is already false and the Enter leaks through as a submit — the exact bug you were fixing. That is why I lead with the per-event isComposing / keyCode 229 check: it reads the state of the keypress itself instead of a flag you have to keep in sync.
And the honest limit: none of this proves your form works in Japanese. It proves this one keypress does. The only way to know the rest holds is to actually type Japanese into it — which is the thing that never happens in a test suite written by someone who doesn't.
Top comments (1)
This is a great breakdown! I've run