As part of a large refactor we recently completed for one of our clients, Fitbot, we needed to handle keyboard shortcuts. For example, Command+Enter should trigger a ‘save’ action, Shift+Enter should add a workout item, Escape should close the currently open dialog. Fitbot is an Ember.JS application, so we started by looking on Ember Observer for an existing keyboard manager service. Most of them are deprecated and point to Ember Keyboard. We installed it and it worked well for some of the shortcuts we needed. But we ran into issues in the following scenarios:

When Enter is the execution key on the shortcut

is the execution key on the shortcut Usage with textareas and inputs

Those two issues were show stoppers for us. We had a choice at this point to either debug and open pull requests on Ember Keyboard, or roll our own solution. We chose the latter for two main reasons:

An Ember service seems to be the ideal surface on which to manage keyboard events, and Ember Keyboard is a mixin.

We thought the API for declaring shortcut bindings could be clearer and more flexible.

Listening to keyboard events is a global affair

Keyboard event are global, therefore they should be managed globally. Using an Ember service object to manage key events was a perfect fit. The service could then be injected into routes and components or wherever an action needed to be triggered via a shortcut.

Scoping shortcut event handlers

While keyboard events are global, the DOM element the shortcut event is bound to can be scoped down to a specific selector. An example where this is necessary is if you have the same component rendered multiple times simultaneously and each instance of the component is listening for the same shortcut. The shortcut should listen to — for example — only those events that are triggered on its element.

Precedence for competing shortcuts

While scoping allows the ability to target a specific selector with a shortcut, the ability to set precedence on a shortcut allows the ability to disambiguate multiple shortcuts with the same keys that are bound to the same selector at the same time. For example, a route and a component that’s rendered on that route both listen on the document for the escape key to be triggered.

Not all keys execute

In digging into keyboard events, it became clear there are two types of keys for our purposes: execution keys and modifier keys. Modifier keys are the keys that do not terminate the shortcut and can be held indefinitely without clearing any of the other ‘down’ keys. Execution keys are the inverse: all those that clear themselves from ‘down’ keys on their keyup event. For example in the shortcut Command+Shift+S , when Command is held and Shift is held, Shift would not clear itself or the Command key from ‘down’ keys on its keyup event. When S ’s keyup event is fired, it clears itself from ‘down’ keys.

Separate up and down actions

Most of the time the actions for shortcuts can be triggered as soon as the keydown event is triggered. However, there are cases where the action for the keydown event should be different from the action for keyup . One example is holding the shift key to select multiple things. On the keydown event, we want to turn on something that listens for click events to select, and on keyup we want to turn that listener off.

Some events are lost in the void

We discovered several scenarios where browsers don’t fire a keyup event. This was problematic because we use the keyup event to clear the ‘down’ keys that we track globally. The scenarios we found were using they keyboard to change the focus from our application to another tab, window, or application. On macOS examples are:

Command+9 to focus the last tab in a window (assuming our application is not on the last tab).

to focus the last tab in a window (assuming our application is not on the last tab). Command+~ to switch away from the window our application is on to another window in the same browser.

to switch away from the window our application is on to another window in the same browser. Command+tab to switch away from the browser to another application.

Our solution to this was twofold:

Clear all ‘down’ keys whenever the browser’s visibility changes to hidden . Clear all ‘down’ execution keys on a set interval and restart the interval every time there’s a match. We only want to clear the execution keys to allow for the rapid firing of the same shortcut. For example if the shortcut for ‘paste’ was Command+V , and you wanted to hold the Command key down and just press and release the V key a bunch of times, we don’t want to inadvertently clear the ‘down’ Command key.

A keyboard manager is born

ember-key-manager is the addon that resulted from the above requirements and discovery. The up-to-date documentation for it lives on its repository, but here are example usages in a component and a route:

Give it a shot if you need to manage keyboard shortcuts in a Ember.JS application. Any and all feedback is welcome!

Acknowledgements and appreciations

Thanks to Fitbot for posing us with the challenges that led us to build ember-key-manager , for sponsoring the development work and the writing of this post! Thank you, thank you.