Skip to content

Contributing Keybindings

Keybindings add keyboard shortcuts beyond the built-in navigation keys. The vis domain uses them for jump-to shortcuts (x, y, l). You can add your own.

KeybindingContribution<P>

ts
interface KeybindingContribution<P> {
  key: string;
  label?: string;
  group?: string;
  handler: (runtime: NavigationRuntime<P>, event: KeyboardEvent) => boolean;
}
PropertyTypeRequiredDescription
keystringyesThe event.key value to match (e.g., 'x', 'j', '1').
labelstringnoDescription shown in the help dialog.
groupstringnoGroup heading in the help dialog.
handlerfunctionyesCalled when the key is pressed. Return true if handled, false to pass through.

Keybindings are skipped when modifier keys (Ctrl, Alt, Meta) are held. This prevents conflicts with browser and screen reader shortcuts.

Writing a keybinding

Here's a keybinding that jumps to the first group node:

ts
import type { KeybindingContribution } from 'olli-core';
import type { MyPayload } from './spec.js';

export function jumpToFirstGroup(): KeybindingContribution<MyPayload> {
  return {
    key: 'g',
    label: 'Jump to first group',
    group: 'Navigation',
    handler: (runtime) => {
      const graph = runtime.hypergraph();
      for (const [, edge] of graph.edges) {
        if (edge.role === 'group') {
          const navTree = runtime.navTree();
          for (const [navId, node] of navTree.byNavId) {
            if (node.hyperedgeId === edge.id) {
              runtime.focus(navId);
              return true;
            }
          }
        }
      }
      return false;
    },
  };
}

The handler has access to the full runtime — it can read the hypergraph, nav tree, and focused node, then call runtime.focus() to move focus.

Return true to indicate the key was handled (prevents default behavior). Return false to let other keybindings or the browser handle it.

Registering keybindings

Add to your domain object:

ts
export const myDomain: OlliDomain<MySpec, MyPayload> = {
  name: 'my-domain',
  toHypergraph: lowerMySpec,
  keybindings: [jumpToFirstGroup()],
};

Or register directly:

ts
runtime.registerKeybinding(jumpToFirstGroup());

KeybindingRegistry

ts
interface KeybindingRegistry<P> {
  register(binding: KeybindingContribution<P>): void;
  list(): readonly KeybindingContribution<P>[];
  dispatch(runtime: NavigationRuntime<P>, event: KeyboardEvent): boolean;
}

The dispatch method is called by the renderer on every keydown event. It iterates through registered bindings, skipping events with modifier keys, and calls each handler until one returns true.

Real-world example: vis jump shortcuts

The vis domain registers three jump shortcuts:

ts
function jumpToNodeType(nodeType: string): KeybindingContribution<VisPayload> {
  return {
    key: nodeType === 'xAxis' ? 'x' : nodeType === 'yAxis' ? 'y' : 'l',
    label: `Jump to ${nodeType === 'xAxis' ? 'X axis' : nodeType === 'yAxis' ? 'Y axis' : 'legend'}`,
    group: 'Jump to section',
    handler: (runtime) => {
      const graph = runtime.hypergraph();
      for (const [, edge] of graph.edges) {
        if (edge.payload?.nodeType === nodeType) {
          const navTree = runtime.navTree();
          for (const [navId, node] of navTree.byNavId) {
            if (node.hyperedgeId === edge.id) {
              runtime.focus(navId);
              return true;
            }
          }
        }
      }
      return false;
    },
  };
}

Next

Released under the BSD-3-Clause License.