Adding Keyboard Shortcuts To Incident Commander Using Alpine.js
In the old Angular version of my Incident Commander tool, all of the interactivity took place in a Single-Page Application (SPA) context. In that model, the primary input never lost focus. In my new ColdFusion version, I'm using a Multi-Page Application (MPA) architecture which naturally resets the focus after each form submission. As such, I needed a way to re-focus the primary form control; but, I didn't want to hurt the accessibility (A11Y) of the page. To this end, I've implemented a keyboard shortcut for focusing the input using Alpine.js.
Run this app at www.incident-commander.com
.
View this code in my Incident Commander project on GitHub.
When thinking about bringing focus to the primary input for status updates in Incident Commander, my first thought was to add the autofocus
attribute to the <textarea>
element. This way, every time the status page loads, the browser will automatically move focus to the desired form control.
But after reading What Every Engineer Should Know About Digital Accessibility, I've become cautious about overriding the natural focus behaviors of the web. I don't want to disorient the users that use assistive technology (AT). This is especially true after the status update form has been submitted and I render a success message as a "live region" at the top of the page:
<div
x-data="izm317.FlashMessage"
tabindex="-1"
role="alert"
aria-live="polite"
class="flash-message">
Your status update has been posted.
</div>
This <div>
represents an Alpine.js component, FlashMessage
, which calls .focus()
behind the scenes in order to bring the focus to the aria-live
region. To be honest, I don't know if this is the correct practice; but, it's what ChatGPT recommended for accessibility and I don't have a strong enough foundation in A11Y best practices to dispute it.
With the flash message component receiving the focus, I decided to use a global keyboard shortcut to allow the user to explicitly move focus to the <textarea>
as needed. Taking inspiration from Gmail, I'm using the letter (C
), short for "Compose", to move focus to the status update form control.
Alpine.js makes this simple because the @keyboard
directive can be modified with .window
or .document
. This allows me to define my global keyboard shortcut directly from the <textarea>
element itself:
<textarea
name="contentMarkdown"
x-data="tcr65f.ContentMarkdown"
@keydown.window.c="handleFocusRequest( event )"
@keydown.meta.enter="$el.form.submit()"
@keydown.ctrl.enter="$el.form.submit()"
class="ui-textarea"
>#encodeForHtml( form.contentMarkdown )#</textarea>
Here, the attribute, @keydown.window.c
, is binding an event handler that listens for the c
key to be pressed. And, because I'm using the .window
modifier, this event handler is being bound at the window level, not the textarea level. Which means that my event handler will still be invoked even if the textarea doesn't have focus.
My <textarea>
element represents an Alpine.js component, ContentMarkdown
, which is what's providing the handleFocusRequest()
event handler. This method calls .focus()
behind the scenes; but, only if the conditions are relevant: that the event is not modified by another key and that the event hasn't been default-prevented by a competing event handler.
function ContentMarkdown() {
return {
handleFocusRequest: handleFocusRequest
};
// ---
// PUBLIC METHODS.
// ---
/**
* I handle the global focus request for the status update.
*/
function handleFocusRequest( event ) {
if (
// If the event has already been intercepted by another keyboard shortcut,
// ignore it.
event.defaultPrevented ||
// If the event is modified in any way, ignore it.
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
// If the event is coming from a form control, ignore it.
event.target.matches( "input, select, textarea, button" )
) {
return;
}
event.preventDefault();
this.$el.focus();
}
}
With this Alpine.js component in place, I can now use the (C
) keyboard shortcut to bring focus back to the status update form control after each submission:
As you can see in the screen recording, I'm using (CMD+Enter
) to submit the form while I'm still focused in the <textarea>
. Then, once the page loads, I'm using (C
) to move focus back to the <textarea>
for a subsequent status update.
My Alpine.js component doesn't contain any code relating to the CMD+Enter
shortcut because I'm not adding any additional logic to the event handling. As such, the form submission can be triggered directly from the inline directive:
@keydown.meta.enter="$el.form.submit()"
I don't love that Alpine.js is, essentially, using eval()
under the hood to execute JavaScript expressions. But, it sure does make it easy to bind little bits of JavaScript control flow to the rendered page.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →