Import it from @grafloria/renderer.
tsclass KeyboardNavigationController
Methods
constructor(config: Partial<KeyboardNavConfig> = {})getConfig(): KeyboardNavConfigupdateConfig(patch: Partial<KeyboardNavConfig>): voidgetFocusOrder(engine: DiagramEngine): FocusTarget[]— Tab order: nodes in reading order (top-to-bottom, then left-to-right), then links ordered by their source node's place in that same order — so tabbing walks the structure the way a sighted user reads it.getFocused(): FocusTarget | nullsetFocus(target: FocusTarget | null, engine?: DiagramEngine): FocusTarget | null— Set focus (null clears it) and announce the new target.focusNext(engine: DiagramEngine): FocusTarget | null— Tab.focusPrevious(engine: DiagramEngine): FocusTarget | null— Shift+Tab.focusDirection(engine: DiagramEngine, direction: NavDirection): FocusTarget | null— Spatial arrow-key focus movement across NODES: pick the nearest node whose centre lies in the requested half-plane, preferring small perpendicular offset (the standard "directional focus" heuristic).getFocusRing(engine: DiagramEngine): FocusRing | null— The visible focus ring for the current target (null when nothing is focused).selectFocused(engine: DiagramEngine, additive = false): boolean— Make the focused entity the selection (Space, or Enter on a link). Selection lives on the model, which is what the renderer draws and what the clipboard/delete commands read once the host syncs it to the store.incidentEdgesOfFocus(engine: DiagramEngine): Incidence[]— The edges incident on the focused node, in a stable traversal order.followEdge(engine: DiagramEngine, index = 0): FocusTarget | null— FOLLOW-EDGE NAVIGATION. From a focused node, step along its Nth incident edge to the node at the far end. From a focused EDGE, step to its endpoints.
This is the move that makes a diagram traversable without sight: you are on "Is order valid?", you are told it has 2 outgoing edges, and you walk one.
followOutgoing(engine: DiagramEngine, index = 0): FocusTarget | null— Walk to the Nth node this one points AT.followIncoming(engine: DiagramEngine, index = 0): FocusTarget | null— Walk back to the Nth node that points at this one.focusIncidentEdge(engine: DiagramEngine, index = 0): FocusTarget | null— Focus the EDGE itself (rather than jumping over it) — so it can be deleted.focusEntryPoint(engine: DiagramEngine, index = 0): FocusTarget | null— Jump to the first entry point — "take me back to the start of the flow".positionContextOfFocus(engine: DiagramEngine): string— The POSITION CONTEXT for the focused node — "node 3 of 12, 2 incoming, 1 outgoing". The orientation a sighted user gets for free from the picture.announcePosition(engine: DiagramEngine): Announcement | null— Announce where we are in the graph. Bound to a key in the host.announceSummary(engine: DiagramEngine): Announcement | null— The whole-diagram natural-language summary, announced on entry.deleteCommand(engine: DiagramEngine): Command | null— Delete the selection (or, with nothing selected, the focused entity), as one undoable macro. Deleting a node takes its incident edges with it — leaving dangling links is how a keyboard user silently corrupts a diagram.duplicateCommand(engine: DiagramEngine, offset: Point = { x: 24, y: 24 }): Command | null— Duplicate the focused/selected nodes at an offset, carrying any edge that runs BETWEEN two duplicated nodes (an edge to a node you did not copy has no meaningful counterpart, so it is dropped — the same rule the pointer duplicate uses).reparentCommand(engine: DiagramEngine, parentId: string | null): Command | null— REPARENT the focused node into (or out of) a container — the last thing that was pointer-only.nullunparents.
SetParentCommand already rejects a cycle; we catch it EARLY so the user
gets an assertive "cannot" instead of an exception thrown into the void.
reparentCandidates(engine: DiagramEngine): NodeModel[]— The containers the focused node could legally be reparented into — what a host puts in a "move to…" list. Excludes itself and its own descendants.nudgeDelta(key: string, coarse = false): Point | null— World delta for an arrow key (null when the key is not an arrow).nudgeCommand( engine: DiagramEngine, dx: number, dy: number, options: { mergeable?: boolean } = {} ): Command | null— Move the selection (or, with nothing selected, the focused node) by a world delta, as ONE undoable command per key press. Locked / undraggable nodes are skipped; returns null when there is nothing to move.
mergeable defaults FALSE — the a11y contract is "⌘Z undoes each nudge to
the exact pixel". An editor that wants held-key auto-repeat to collapse
into one undo entry (Visio's feel) passes true and the CommandManager's
merge window does the rest.
getConnectState(): KeyboardConnectState | nullisConnecting(): booleanbeginConnect(engine: DiagramEngine): boolean— Returns false when the focused node has no connectable port.cyclePort(engine: DiagramEngine, delta: number): boolean— Cycle the port being picked in the current phase.cycleTargetNode(engine: DiagramEngine, delta: number): boolean— Cycle the TARGET node.confirmConnect(engine: DiagramEngine): Command | nullcancelConnect(): void— Escape.onAnnounce(listener: AnnouncementListener): AnnouncementUnsubscribegetLastAnnouncement(): Announcement | nullannounce(message: string, politeness: 'polite' | 'assertive' = 'polite'): AnnouncementannounceSelection(engine: DiagramEngine): Announcement | null— "2 nodes and 1 link selected" — announced whenever the selection changes.announceStructure(engine: DiagramEngine, change: string): Announcement | null— "Node Start added. 4 nodes, 3 links." — for structure changes.describe(engine: DiagramEngine, target: FocusTarget): string— The accessible name of a target: what a screen reader reads on focus, and what the host puts inaria-labelon the rendered element.nodeName(node: NodeModel): string— Human name of a node: its label, else its type + short id.dispose(): void
Was this page helpful?