Attach a keydown listener to target (document, or a region element to scope it).
Match event and, if it maps to an enabled command, preventDefault() +
stopPropagation() + dispatch it (the resolver's execute fires the ripple/observers).
Returns whether the event was consumed.
Responder chain: dispatchers attach to their region's element, so DOM bubbling makes
the innermost region fire first; consuming (stopPropagation) keeps a parent region's
dispatcher from re-handling it, while an unmatched key is left to bubble up to the next
region and ultimately the Studio-level backstop.
No target-type defer (the dispatch law). A key that reaches dispatch is one the
z-stack did not consume, so it is dispatched on its own merit — there is no "is the focus an
input?" second-guess. A widget that wants a key (a focused text input, the cell editor in edit
mode) opts out by stopPropagation-ing it, so dispatch is never called for it. This is what
keeps consumption to a single signal (design/COMMAND-DESIGN.md "Core law — one keystroke
bubble"); the editable-target heuristic that used to live here was a competing rule and is gone.
The one structural gate is origin containment (see the class doc): resolution itself omits
every contribution whose region doesn't contain the event's origin — about where the key was
born, never what kind of element it was born on.
The command key bound to event at the resolver's current anchor, or undefined.
Enumerates the keys reachable from the anchor, resolves each origin-filtered (see the class
doc — resolution omits every contribution whose region doesn't contain the event's origin), and
matches its shortcut(s) — so an overridden shortcut resolves through the cascade like any other
property, and a fully-omitted command simply doesn't resolve (the binding doesn't exist for this
origin; another command on the same shortcut may still match).
Introspection/testing: the current shortcutString → commandKey map at the anchor.
Maps a keyboard event → command key, then dispatches through a CommandResolver (so a shortcut resolves against the resolver's anchor exactly like a button — the same cascade/override). Ctrl/meta are treated alike, with an
e.code→e.keyfallback andKey*/Digit*prefix stripping. See design/COMMAND-DESIGN.md "key dispatching".The dispatch law (design/COMMAND-DESIGN.md "Core law — one keystroke bubble"): this is the terminal handler of one bubble. A keystroke goes down the z-stack first (every layer is offered it via the Stage responder chain; a layer that handles it calls
stopPropagation()), and only what no layer consumed bubbles up to this dispatcher (attached at the region root), which resolves it up the command scope hierarchy. SostopPropagationis the only consume signal — a stopped key never reaches us — and there is no competing target-type defer.Origin containment (the second half of the law): dispatch resolves origin-filtered — the event's origin (
event.target) is passed intoCommandScope.resolve, whose gather loop omits every contribution from a node whoseCommandScope.contains(origin)rejects it (the scope OWNS the decision: any of its ownattachElemented elements DOM-contains the origin, else its parent decides, none up the chain ⇒ unrestricted — and a region owner can attach a portaled container as an additional element). Per contribution, not per command: a key born in a task pane omits a sheet-layer's portion but keeps aworkbook-level portion of the same command — the cascade falls through to the permitted callback — and a command whose every contribution is omitted doesn't resolve at all (the binding doesn't exist for that origin; the key stays with the input). This is what keeps a pane-layer command (selection nav, clipboard) from firing off a keystroke born in an orthogonal subtree — a task-pane input's ArrowDown moves its caret, not the grid selection — while a keystroke a layer deliberately lets propagate (the cell editor's commit Enter) still dispatches, because it originates inside the pane's Stage. NOT a target-type rule: it never asks "is the target an input?", only "is the origin inside the contributor's region?" — so it cannot re-introduce the editable-defer that broke edit-mode commits.Containment only works for scopes that BOUND themselves. An element-less scope is unrestricted by design (consequence 3) — right for
app-tier commands (ctrl+Smust save while the caret is in a task-pane field), wrong for anything acting on the grid. Asheet-scope binding left unbounded is howDeletecame to runclearContentsfrom a comment box (clearing cells and dirtying the workbook mid-keystroke). The rule for authors: if a command needs a UI region, bound its scope (CommandScope.attachBounds); leave a scope element-less only when its commands are genuinely app-global.Anchor-dependent by construction: because it asks the resolver, the same dispatcher yields different commands as the anchor moves — the app root owns one over the main resolver; a region (e.g. the scripting editor) can own its own over its resolver, so its bindings win only while focused there.
Not built yet (seams left): multi-stroke chords (a pending-stroke state) and a user keybinding override map consulted before the contributed defaults.