Robust Keyboard interactions for Stimulus actions
Summary
With the JS library @github/hotkey, you can easily step up your hotkey gameLesson
Disclaimer: I saw this practice used in Hey, the amazing Email and Calendar App by 37signals and copied this pattern for my own use, because it looked very elegant and maintainable. This is possible because they don't minify their JS, avoid a build step and therefore enable Read Source (DHH even encourages people to do this and I'm very thankful to get a glimpse at their powerful Frontend code).
So, first of all, listening for Keyboard events is possible by Stimulus itself, as can be read in the docs, however there is one drawback to this approach: it only works if some node in the DOM-tree at the stimulus control or below has the focus of the HTML page. This makes a lot of sense in the Stimulus mindset, that is all about encapsulating localized behavior and not have a gigantic Frontend context that know about everything. Also, I assume that from a Event bubbling perspective, the Stimulus component would not even have the event bubble up to it, since the focus is higher up, like at the HTML body level.
This is where the JS package @github/hotkey comes into play. It can be installed via node or importmap, the command for the later is
bin/importmap pin @github/hotkey
Next, how to tie this up the Rails Way? Add a Stimulus controller named app/javascript/controllers/keybind_controller.js
with the following content
import { Controller } from "@hotwired/stimulus"
import { install, uninstall } from "@github/hotkey"
// Connects to data-controller="keybind"
export default class extends Controller {
connect() {
install(this.element)
}
disconnect() {
uninstall(this.element)
}
}
This makes sure the Hotkey library can be easily configured without writing any Javascript at all.
Next, let's define a keybind to a Stimulus action. In the ERB-template of the page where you want the Hotkey to work, place an element like this
<button hidden="hidden" data-hotkey="Shift+ArrowDown" data-controller="keybind" data-action="workspace-keyboard-nav#shiftDown"></button>
The data attribute data-hotkey
defines your keybind. I explicitly gave an example with a modifier key, but refer to the Hotkey docs for more details, sequences or alternatives of keys are also possible. The keybind controller makes sure that the Hotkey library is initialized on the hidden button. And finally, the data-action poiints to the Stimulus Controller and Action that should be executed. Of course, this is dependent on what you want to achieve, in my case i want to reorder a list item one position down. Also please be aware that the Stimulus Controller being called needs to be set on a DOM-node that is higher up in the tree. What this means is the button code snippet above needs to move inside of the DOM-node of the Stimulus controller. Look at the more complete example below
<div data-controller="workspace-keyboard-nav">
<button hidden="hidden" data-hotkey="Shift+ArrowDown" data-controller="keybind" data-action="workspace-keyboard-nav#shiftDown"></button>
<!-- Whatever else you want to put here -->
</div>
The good thing about this approach is that the keybind definitions live close to the Stimulus Controller that needs to be called, but can still listen globally for keyboard presses. This makes it also very convenient to add keybinds to different pages while maintaining a clear understanding of what's happening.
Good luck with those keybinds and may your interactions be blazingly fast!