Vim and Gmail-style key sequences
g then g to jump to the top,
g then i to go to the inbox — a sequence is a
different thing from a chord, and needs its own syntax and its own rules
for what counts as "pressed in order."
Sequence vs. chord
A chord ("ctrl+a") is keys held down
together, resolved from a single KeyboardEvent. A
sequence ("g,o") is keys pressed
one after another, within a timeout of each other — two
completely different things bind-keyboard represents with two different
syntaxes rather than overloading one.
Registering a sequence
A comma-separated key combination string is a sequence — its callback
only fires once every step is pressed in order, within
sequenceTimeout ms of each other (a constructor option,
1000ms by default):
import { BindKeyboard } from "bind-keyboard";
const bindKeyboard = new BindKeyboard({ sequenceTimeout: 1000 });
bindKeyboard.add("g,g", () => window.scrollTo({ top: 0 }));
bindKeyboard.add("g,i", goToInbox);
A literal comma is unambiguous
A comma right after a + is a literal comma key, not a
separator — so "ctrl+," (a real Ctrl+Comma binding, e.g.
"open settings") and "ctrl+,,g" (a two-step sequence:
Ctrl+Comma, then g) are never ambiguous with each other.
Holding a key doesn't complete a same-key sequence
OS key auto-repeat (holding g down, which fires repeated
keydowns for the same physical press) is ignored for
sequence matching — so simply holding g doesn't
accidentally complete "g,g" on its own. It has to be
released and pressed again for real.
Two sequences that could both complete on the same press
Registering both "g,o" and "g,o,x" means a
press of g then o completes the first while
the second is still legitimately pending on the same prefix — an
ambiguous case most libraries resolve by picking whichever one
arbitrarily. bind-keyboard fires neither in that
instant, with a debug
-only warning naming both, rather than guessing which one you meant:
bindKeyboard.add("g,o", goToOverview);
bindKeyboard.add("g,o,x", goToOverviewExtended);
// Pressing g, then o: neither fires yet (ambiguous).
// Pressing x next: "g,o,x" is no longer ambiguous — it fires.
Letting a plain binding wait to see if it's part of a sequence
By default, a plain binding and a sequence sharing a starting key ("g"
and "g,o") coexist — the plain one fires immediately, same
as always. The per-binding option
{ deferForSequence: true } opts a plain binding out of
that: it waits up to sequenceTimeout to see whether the
press was the start of a sequence, firing only if it wasn't (as soon as
that's known, not necessarily the full timeout):
bindKeyboard.add("g", showGoToMenuHint, true, "keydown", {
deferForSequence: true,
});
bindKeyboard.add("g,i", goToInbox);
Scoped and input-safe like any other binding
Sequences support everything a plain binding does —
scope, allowInInputElements,
description, override: false — so a sequence
can be scoped to a modal exactly the way described in
keyboard shortcuts inside a modal.
Related
Migrating from Mousetrap or hotkeys-js · Keyboard shortcuts inside a modal