useKey
useKey(target?: Document | Element | Window): Service<KeyProps>The keyboard as a service, one instance per target, defaulting to the document.
Props
interface KeyProps {
readonly event: KeyboardEvent | null;
readonly triggered: number;
readonly isDown: boolean;
readonly isUp: boolean;
// plus one boolean per named key
readonly ENTER: boolean;
readonly SPACE: boolean;
readonly TAB: boolean;
readonly ESC: boolean;
readonly LEFT: boolean;
readonly UP: boolean;
readonly RIGHT: boolean;
readonly DOWN: boolean;
}Usage
import { Base, useKey } from '@studiometa/js-toolkit';
class Dialog extends Base {
static config = { name: 'Dialog' };
mounted() {
return useKey().subscribe(({ ESC, isDown }) => {
if (ESC && isDown) this.$el.removeAttribute('data-option-open');
});
}
}A region rather than the document:
import { Base, useKey } from '@studiometa/js-toolkit';
class Menu extends Base {
static config = { name: 'Menu' };
mounted() {
return useKey(this.$el).subscribe(({ DOWN, isDown }) => {
if (DOWN && isDown) this.focusNext();
});
}
focusNext() {}
}An element target is what removes the hasFocus bookkeeping a document-only service forces on a consumer.
The eight names
The eight names of v3 are kept, resolved from KeyboardEvent.key rather than from the deprecated keyCode:
| Name | event.key |
|---|---|
ENTER | Enter |
SPACE | ' ' |
TAB | Tab |
ESC | Escape |
LEFT | ArrowLeft |
UP | ArrowUp |
RIGHT | ArrowRight |
DOWN | ArrowDown |
The constant that maps them is module-internal: the names reach a consumer as props, so nothing is left to compare against. The flags are a mapped type over that constant, so the props cannot drift from it.
Why they are an exception
Props are flat and nothing derivable is a field — and each of these compares event.key against a named value, so nothing about them is unavailable from event. They are kept for call-site parity with v3, where keyed({ ESC }) is how components read the keyboard.
triggered counts repeats of one key
A keydown whose key matches the previous event's while that key is still down increments it. A different key, or any keyup, sets it back to 1.
v3 incremented on any two consecutive keydowns, so holding A and then pressing B reported 2.
The listeners are neither passive nor capturing
- Not passive, because
trapFocus()and every keyboard shortcut callpreventDefault()on the event the subscriber is handed, and a passive listener cannot. - No capture, unlike the pointer service, because a descendant that handles its own keys and stops the propagation is respected rather than overheard.
Mixin
class Dialog extends withKey(Base) {
keyed({ ESC, isDown }) {}
}withKey takes the document default too, as withScroll and withResize do for their own page-wide source. A region is withKey(Base, { target: (instance) => instance.$refs.wrapper }).