All Articles

Composition Events

Image from Unsplash by Mike Enerio
Image from Unsplash by Mike Enerio

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:

Typing English characters

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.

Typing Chinese characters without the use of composition events

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 “你”:

Typing Chinese characters with the use of composition events

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.