Skip to content

useKey

ts
useKey(target?: Document | Element | Window): Service<KeyProps>

The keyboard as a service, one instance per target, defaulting to the document.

Props

ts
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

js
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:

js
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:

Nameevent.key
ENTEREnter
SPACE' '
TABTab
ESCEscape
LEFTArrowLeft
UPArrowUp
RIGHTArrowRight
DOWNArrowDown

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 call preventDefault() 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

js
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 }).

MIT Licensed