Keyboard shortcuts in React, and how they help the annotation team

Cogito’s Annotation Team has been requesting keyboard shortcuts for some time to help improve the efficiency of their workflows, and we have just added them to our Annotation Tool. This article walks you through the process of adding keyboard shortcuts to a React application, and explains how they will help annotators in the new version of the tool. We use the react-hotkeys library, which we refer to in the post below.

Shortcuts in the Annotation Tool

First, let’s take a look at how shortcuts work in the Annotation Tool.

Before the shortcuts were added, all annotation had to be done using the mouse. Now, the user interface (UI) displays a list of shortcuts and their key bindings, and users can change them dynamically. Each annotator can customize their own shortcuts, and they are saved in the database so they persist after reloading the page.

Meeting with the annotation team

Before adding shortcuts to the Annotation Tool, we had to find out what features the Annotation Team wanted. We set up a meeting with the whole team, giving annotators the opportunity to voice any issues they had with the current workflows in the Annotation Tool.

Some of the most commonly requested features were:

  • Playing / pausing the audio
  • Skipping back and forth
  • An easier way to create and modify intervals on the audio player

After adding these features, we experimented with different methods of modifying intervals, until we came up with our final product. Here’s an example of how someone might use these shortcuts to annotate a series of intervals:

Adding shortcuts to your own app

Now that we’ve seen how keyboard shortcuts are used within the Annotation Tool, let’s take a look at how they can be added to your own React application.

Installation

Assuming you already have a React application, all you need to install is react-hotkeys.

npm install react-hotkeys --save

Using react-hotkeys

react-hotkeys provides several components that are useful for implementing keyboard shortcuts into your React app.

  • The GlobalHotKeys component allows you to trigger actions with key events that happen anywhere in the document.
  • The HotKeys component only triggers actions with key events that happen in one of their descendants.

In our Annotation Tool, we use GlobalHotKeys, since we want our shortcuts to be usable from anywhere on the page.

keyMaps and handlers

In order to use either of these components, you must pass them both a keyMap and a handlers prop.

Within a keyMap, actions are linked to key bindings, and within handlers, actions are linked to functions that are called when the corresponding key combination is detected.

The keyMap can be defined with just a key mapping or with additional data that is used to display key bindings in the UI. Shortcuts could be a single key, a combination of keys pressed at the same time (written as `”a+b+c”`), or a sequence of keys pressed and released one after the other (written as `”a b c”`).

Handlers are simply defined as actions linked to the functions they will call.

If you don’t need dynamic shortcuts, you can stop here and simply add a line like this anywhere in your code, passing it the appropriate keyMap and handlers.

Note that it is perfectly acceptable to have multiple GlobalHotKeys components with separate keyMaps and handlers within your code. They are still all able to resolve their individual actions, and if there are any duplicate keyMaps or handlers, whichever GlobalHotKeys was mounted first takes precedence.

Preventing default behavior

Some key combinations already have default behavior assigned to them by the browser. For example, pressing `”space”` scrolls down a page, and `”Control+p”` prints the page. If we want to prevent this default behavior from occurring, we can use this function to intercept the event before it gets handled, and prevent the default handler from being activated.

We can use this by wrapping our handlers in it wherever they are defined:

Dynamic shortcuts

In order to change shortcuts dynamically, we need several things:

  • A UI element that allows us to view and edit shortcuts
  • A way to store the shortcuts so they persist between sessions

UI for changing shortcuts

For the UI, we use Material-UI to style the table, but you can use any other framework as well, or do your own custom styling. If you want to use Material-UI, it can be installed like this:

npm install @material-ui/core

First, create a dialog to edit shortcuts.

It should look something like this:

Next, create a dialog to show while a shortcut is being changed:

It should look like this:

Create a button to press to edit shortcuts, and a Popover that will display the current key bindings when hovering over it:

They should look like this:

Now, wherever you want these components to show up, you need to define the following in order to keep track of the state of the shortcut UI while changing shortcuts:

And finally, render the UI components like so:

Storing the shortcuts

How the shortcuts are stored and retrieved largely depends on your database implementation, but we can go over some generic details here.

First, create a default keyMap somewhere. This allows shortcuts to operate without users having to define them first.

It typically makes sense to store the current keyMap in some kind of global store, so any HotKeys component that needs it is able to access it. When retrieving shortcuts from the database, we can do something like this to make sure that a keyMap is defined for our HotKeys components.

Additionally, we need to retrieve the shortcuts from the database whenever the page is opened, so we can add something like this to our code, where `retrieveShortcuts` makes the appropriate API calls to the backend.

Whenever we change the shortcuts locally, we also want them to be uploaded to the database, so we add a line like this within the `cancelListening` function in `showChangeShortcutDialog` (it has already been added in the code snippet above).

Putting it all together

Once we have our keyMap stored in the database, all that’s left to do is add GlobalHotKeys components wherever handlers are defined. The `allowChanges` prop allows the keyMap and handlers to be changed dynamically.

Opportunities at Cogito

If you are based in the US or Ireland and are interested in opportunities at Cogito, please check out our careers page! We have an office optional policy that encourages remote work and collaboration!

Acknowledgements

This article was written by Ben Knower during his internship at Cogito — while being mentored by Jacob Duyon. Thanks also to Richard Brutti and Matthew Roddy for the detailed proofreading and feedback.