Yesterday, I tackled an issue affecting clients relying on input method editors (IMEs).
Specifically, users who wished to insert Chinese (Mandarin, Cantonese, etc) characters with the Cangjie keyboard found themselves triggering text submissions prematurely.
Consider the following React component:
function Textarea(props) {
// `setOutputMessage` updates the state for an
// `outputMessage` that is rendered below
// this `<Textarea />` component
const { setOutputMessage } = props;
const [message, setMessage] = useState("");
const onChange = event => setMessage(event.target.value);
const onKeyDown = event => {
// React specifically to Enter key
if (event.key === "Enter") {
event.preventDefault();
setOutputMessage(message);
setMessage("");
}
};
return (
<textarea
className="Textarea"
onChange={onChange}
onKeyDown={onKeyDown}
rows="5"
value={message}
/>
);
}
For users who don’t depend on IMEs to type, this input component works perfectly:
However, for users who do depend on IMEs, there’s a problem. Notice that the component’s onKeyDown
handler specifically reacts to the Enter key:
const onKeyDown = event => {
if (event.key === "Enter") {
// ...
}
};
Certain IMEs, such as the Cangjie keyboard, also wait on the user to tap on Enter before confirming their selected character. In other words, two listeners are waiting to be triggered on the same event!
If an IME is active, hitting the Enter key once will simultaneously:
- Submit the intermedial text (“人弓火”)
- Set the selected character (“你”) to be the updated input text
Hitting Enter another time will submit “你”, the updated input text.
Both “人弓火” and “你” take turns to be displayed as output. This doesn’t seem right –– the input submission should only occur once, and we just want the character “你” to be displayed.
Intuitively, we expect the first Enter keystroke to simply update the input with the selected character, “你”. Subsequently, the second keystroke should just submit “你”:
Fortunately, this isn’t terribly difficult to implement. We can achieve this behaviour with composition events.
Whenever an IME is activated, updated or disengaged, a composition event is fired. Let’s listen to these events and add a couple of handlers for them:
function Textarea(props) {
const { setOutputMessage } = props;
const [message, setMessage] = useState("");
// Keep track of composition state
const [compoActive, setCompoActive] = useState(false);
const onChange = event => setMessage(event.target.value);
// Handle IME activation
const onCompoStart = event => setCompoActive(true);
// Handle IME deactivation
const onCompoEnd = event => setCompoActive(false);
const onKeyDown = event => {
// React specifically to Enter key AND
// check if there is an ongoing composition
if (event.key === "Enter" && !compoActive) {
event.preventDefault();
setOutputMessage(message);
setMessage("");
}
};
return (
<textarea
className="Textarea"
onChange={onChange}
onCompositionStart={onCompoStart}
onCompositionEnd={onCompoEnd}
onKeyDown={onKeyDown}
rows="5"
value={message}
/>
);
Our input components should now gel with our users’ IMEs as expected. If you’re interested in a working demo, feel free to check out this sandbox.
As a native English speaker, it’s been way too easy for me to neglect the needs of users who work with different languages. When building products for the web, it’s important that we pay attention to the needs of our wider audience and support those who communicate via different languages, abilities, and levels of connectivities.