I first learned about this issue while working on Primrose.
Things get really hairy in the browser when you start talking about JavaScript, the keyboard, and international support.
For example, there is no way to know what keyboard layout a user is using. With Primrose, I guess based on the user's default language, but even that isn't perfect as a lot of not-US English speakers just show up as "en", with no further locale description, so I also provide a select list with all of the available choices.
It's very difficult to know with certainty what character a user is intending to type. KeyCode 51 with no modifier keys is "3" on most keyboards, except French, where it is "#" (they essentially swap the casing of the number row).
The number pad numbers send different keycodes than the number row numbers, but the number pad arrows (if you turn off the numlock) send the same keycodes as the arrow keys.
In languages with deadkey support for diacritics (such as French and German), the deadkey keyCode isn't sent until the second key is typed. Then, they are sent in rapid succession.
There is absolutely no way to know what is going on with IMEs.
And there is no reliable way to interact with the soft keyboard on mobile devices. Some versions of soft keyboards don't send the arrow key keyCodes. There is no standardization of what keys should be available, so you can't guarantee that your user will easily be able to type your shortcuts (I know of only one keyboard on Android that even has CTRL or ALT). And it's nearly impossible to know how much space the soft keyboard is going to take up on the screen (you can figure it out on Android, eventually. It's impossible on iOS.).
So, you have one of two choices, if you're trying to implement any sort of browser-based application that involves heavy use of the keyboard.
Option 1: You can either create a hidden, surrogate text area in which the user actually types, unbeknownst to them, and run a sync process between the content they type and the content you display. This has several problems: you have to wait for keyUp to activate shortcut commands. The sync process can be costly (especially in the context for which Primrose exists: WebVR) and it is difficult to keep the cursor view in sync when dealing with mouse/touch interactions. Oh, speaking of pointer actions, you have to make sure your surrogate text area is positioned exactly under the displayed text field, with the text appearing the same apparent size as the displayed text, because when it gains focus the browser will scroll the view to it.
But it will work, except for certain use cases. It won't work well on mobile, and it won't play nicely with WebVR (which is the entire point of Primrose, anyway), especially for Asian users using an IME.
So Option 1 isn't a good option.
Option 2: completely reimplement the key input stack, i.e. create all the keyboards and soft keyboards and IMEs your users will ever need. Completely ignore what the OS and the browser tell you, take only the raw keyCodes, and reconstruct what is happening. It's a lot of work, a lot to debug, but at least there is a path to actually solve every problem.