Optionalopts: { name?: string; registry?: CommandRegistry }ReadonlykeyReadonlynameThe scope's name — its key into the flat CommandRegistry (design/COMMAND-DESIGN.md
§"Named scopes"). Defaults to key; a distinct name lets two instances with different keys
(a double-buffer's two workbooks, a sheet's per-pane scopes) share one registry entry. At every
node of resolve/collectKeys, the registry entry for this name is folded in as the
least-specific (prototype) layer beneath the node's own defs.
Observe executions of a specific command key. Fires for ALL dispatch paths (button,
keystroke, programmatic) because the resolved command's execute() fires it — the
ripple/close + middleware hook. Root-anchored, so it survives command re-derivation.
Subscribe to coarse changes (re-resolve on fire). Root-anchored.
Bound this scope's region to element for origin containment — WITHOUT
naming a focus region. The containment half of attachElement, alone.
Use this when a scope's commands need a UI region but the scope is not a focus leaf: the
sheet scope owns the grid's commands, yet focus inside the grid must keep anchoring at the
pane's z-stack (a deeper region), not here. Recording it in the root index would make this scope
a candidate anchor and change what a focus lands on; bounding only does not.
Leaving a scope unbounded is a decision, not a default: an element-less scope is unrestricted
(see contains), so its shortcuts fire from any origin under the dispatch seam — correct
for app-tier commands (ctrl+S saves while the caret is in a task-pane field), wrong for
anything that acts on a region's own UI.
A region is a set of containers, not one subtree. Call this more than once when the region's UI
is not contiguous in the DOM — a host that renders workbook chrome (a formula bar, a status bar, a
toolbar with a search box) outside the workbook element adds each piece, and a keystroke born there
still reaches the region's commands. A host declares those from where the chrome renders, via
useCommandElement; the scope's own owner does not have to know about them. Same seam for a
portaled float — DOM-outside the region, logically inside it.
Returns a remover the recording site calls on unmount.
Record element → source in the tree's _regions region index — a focus region naming its
own focusable element so a consumer's FocusActivation can anchor there on focus. source
defaults to this scope (a fixed leaf — the formula-bar editor, the script pane); pass a live
AnchorSource (a pane's LayerActivation, or a getter) for a dynamic leaf whose current
value tracks a moving stack top. The entry lands on the root (shared by every consumer), so this can
be called on any node. Returns a remover the recording site calls on unmount — identity-guarded, so a
re-record of the same element by another owner is never clobbered by a stale cleanup.
Optionalsource: AnchorSourceAll command keys contributed along the chain from anchor → root (deduped, order
unspecified). The keystroke dispatcher uses this to enumerate the commands that could
match an event at the current anchor (then resolves each to read its shortcut).
Like collectKeys, but only keys whose contribution somewhere along the chain carries
tag (tags accumulate across scopes, so a match at any scope on the chain counts). Enables
cross-cutting, tag-driven rules — e.g. "disable every mutator command when the sheet is
protected" — without a hand-maintained parallel key list. Order unspecified; deduped.
(Re)establish this scope's registry subscription. Idempotent. The owning provider's mount
effect calls this so a scope disposed on an effect cleanup (React StrictMode's
mount→unmount→remount) comes back to life instead of staying permanently deaf to
registry changes — the same resurrect contract as CommandResolver.connect. Without it,
anything that reaches consumers only through the registry listener (a plugin registering
statics after mount, a language-pack switch re-deriving translated chrome) silently stops
propagating after the StrictMode cycle. Bumps on re-subscribe: the registry may have
changed while deaf.
Origin containment — the scope decides (design/COMMAND-DESIGN.md "Core law", consequence 3):
whether node counts as within this scope's region, i.e. whether a keystroke born at node
may dispatch this scope's contributions. The default policy:
true iff any of them
DOM-contains node. (Plural is the portal seam: a float/popup portaled to body is
DOM-outside the region but logically inside it — its owner attaches the portal container as
an additional element and keys born there qualify.)Stage via the chain base, so the whole
z-stack shares one region, and a key a layer deliberately lets propagate — the editor's commit
Enter — still qualifies, because it was born inside that Stage).true (an element-less chain — a sheet/workbook/app-level scope
— is unrestricted: its commands may fire from any origin under the dispatch seam).It is a method on the scope — not an inline DOM check — precisely so a region owner can make a richer decision than raw containment when the DOM and the logical UI disagree (portals today; anything else tomorrow).
Open a tracked CommandContribution on THIS scope: updateCommand through the handle
records each key, and remove() removes exactly those — so a contributor (esp. a large one)
cleans up with a single call rather than a per-key removeCommand list that's easy to fall out
of sync. The handle owns its keys only; it doesn't touch other contributions to this scope.
Create (and register) a child scope. The bare form deliberately does not bump the version: a
fresh child has no definitions, so it changes no resolved command (resolution only sees it once it
gets updateCommand — which bumps — or becomes the anchor). This keeps the endorsed pattern
of creating a region's scope during render (useMemo(() => parent.createChild(key))) free of a
notify-during-render — which would re-enter subscribed consumers (the "setState while rendering"
warning). Teardown (dispose) does bump, since it drops contributions.
contributor — bind a contribution to the child's lifetime (so a scope and its commands are
created and torn down as one, instead of a separate useEffect). It runs immediately with a fresh
CommandContribution for the new child (plus any forwarded args), and returns only its own
external-subscription disposer (e.g. a model listener) — it does not manage command removal.
dispose() does the rest automatically: invoke that disposer and clear the child's _defs
(so the contributed commands go without a per-key list). Declarative call site:
parent.createChild('undo', contributeUndoCommands, { manager, notifier }).
NOTE: the contributor contributes synchronously, and contributing bumps (notifies). So — like
any updateCommand — call this where a contribution-time notify is fine: at mount (in useMemo,
no subscribers yet), NOT mid-render with live subscribers.
Optionalcontributor: (contribution: CommandContribution, ...args: A) => void | RemoveListenerTear this scope down when the feature/region that owns it unmounts: recursively dispose
children, drop this scope's definitions, detach it from its parent (so it stops
contributing to resolution), and bump the root version so resolvers re-resolve. The
_parent pointer is left intact so a stale anchor still pointing here resolves up the
(now-detached) chain rather than throwing — turning a torn-down scope into its parent is
the eventual destruction-fallback (design/COMMAND-DESIGN.md, open Q5). Idempotent.
The nearest region scope at or above element: walk parentElement to the first
attached element and stop (leaf-finding, unbounded — a live source is resolved
to its current scope). This is leaf-finding, NOT the resolution walk (design/COMMAND-DESIGN.md
§Guardrail): it names the anchor; resolution is the separate anchor → root command-parent walk.
undefined if no attached element is on the ancestor chain.
This scope's own (merged) contribution for a key, or undefined.
This scope's own middleware (authoring order).
The region source recorded exactly at element (single-element lookup on the root index), or
undefined. The bounded DOM walk-up is the caller's (FocusActivation.findSource, region-bounded;
or findScope, unbounded) — this is the per-element primitive both build on.
The flat named registry bound to this tree (from the root), or undefined if none.
Trigger a re-resolution without changing any contribution — bump version + notify.
A contributor calls this when the model/context changed (so dynamic values should
re-evaluate) instead of enumerating updateCommands; containers re-resolve on their own
cadence (debounce). NOT a substitute for updateCommand (which sets values).
Remove this scope's contribution for commandKey.
Resolve a command by gathering its contributions along the chain from anchor → root,
reducing them least→most specific, and deriving a ResolvedCommand. anchor is the
resolution leaf; it defaults to the registry root. A consumer (useCommands({ anchor })) sets it — normally to the focus-driven active scope (an ActiveScope
the consumer reads), or to a context menu's snapshot. Returns undefined when no dynamic
contribution for the key exists on the chain — a registry (static) catalog entry alone does not
resolve (see the "static only if dynamic first" note below). Memoized by version, per anchor.
origin (keystroke dispatch only — buttons/display omit it) applies origin containment per
contribution: a node whose contains rejects the event's origin contributes nothing to
this resolve, so the cascade falls through to the permitted portion (or to undefined when
everything is omitted). See the gather-loop comment below.
Optionalinjected: readonly ICommandDefinition<any, any, void>[]Optionalroot: CommandScopeOptionalorigin: Node | nullResolve a command purely from the static registry — the not-in-context primitive a consumer's
fallback composes, deliberately bypassing the "static only if a dynamic exists" rule that governs the
context resolve. Use it to show a command whose scope isn't currently contributing (a toolbar's
disabled button; a table command with no table) — the result is disabled if the static has no
callback, or executable if it carries a global one. prefer is the consumer's priority list of
names (CommandRegistry.findByKey searches it first, then all names).
Never call this from dispatch. The keyboard dispatcher resolves only via resolve, so an
inert static never binds a shortcut (an idle editor's F4 stays free for redo). The policy — whether
to fall back at all, and the prefer list — is entirely the consumer's; the core never decides it.
Returns undefined if the key is registered under no name. Memoized by version.
Optionalprefer: readonly string[]Re-attach this scope under a new parent (or detach with null). The primitive the z-ordered
layer chain is built from: a layer registry re-parents each layer's scope into a stack ordered
by z (higher z → deeper → wins the cascade), so the unchanged resolve (which walks
_parent from the anchor) resolves top-of-stack first. This is what lets a stacked layer
override a command by stacking, not by focus — and lets a client add a command-bearing layer
with no core special-casing.
Reorders within the same root (the layer stack shares one registry root); _root is fixed at
construction, so cross-root moves are rejected. Rejects a cycle. Bumps the root so resolvers
re-resolve. No-op if parent is already the parent.
Contribute/merge a partial definition for commandKey into THIS scope (within-scope
merge). undefined fields are ignored (don't clobber); null is kept (an explicit
clear). The cross-scope cascade happens later, in resolve.
Register dispatch CommandMiddleware on THIS scope: it auto-wraps every callback
this scope authors (so contributor callbacks stay pure — transactional wrapping
becomes one use(tx) instead of wrapping each command by hand). Not inherited — a
command's middleware is that of the scope whose callback wins resolution, nothing up the
chain. First-registered is outermost. Returns a remover.
A node in the command scope tree — the pure registry (the new-model counterpart of the existing
CommandGroup, but holding definitions instead of instances). A scope holds the within-scope merged ICommandDefinition for each command key; it knows nothing about "active"/focus — which leaf to resolve from is the consumer'sanchor(seeCommandResolver).resolve walks the chain from a given
anchorleaf → root, reducing each scope's contribution least→most specific into a derived ResolvedCommand. Two merge levels:Also provides change notification (addListener / invalidate) and per-key execute observation (addExecuteListener). It does NOT dispatch — the resolved command's
execute()does (firing the per-key observers).