Skip to content

Focus Migration ​

3.1 ​

Focus 3.1 replaces raw RangeError throws with typed errors and matches navigation keys through @vielzeug/keymap.

Typed configuration errors ​

Invalid configuration now throws FocusConfigError (extending a new FocusError base) instead of RangeError:

ts
// Before
try {
  createListNavigation({ getItems, keys: { next: ['j'], prev: ['j'] } });
} catch (e) {
  if (e instanceof RangeError) {
    /* ... */
  }
}

// After
import { FocusConfigError } from '@vielzeug/focus';

try {
  createListNavigation({ getItems, keys: { next: ['j'], prev: ['j'] } });
} catch (e) {
  if (e instanceof FocusConfigError) {
    /* ... */
  }
}

Keys match through @vielzeug/keymap ​

Default and custom key tables are matched with matchKey, so entries accept shortcut patterns with aliases (esc, space, up) and modifiers (shift+Home), and modifier state must match exactly: a plain ArrowDown entry no longer fires on Ctrl+ArrowDown, so browser and app chords reach the page.

3.0 ​

Focus 3.0 makes list navigation a pure in-memory decision primitive. It returns structured changes while framework and component layers own focus effects, reactive state, event listeners, and disposal.

Replace onNavigate with returned changes ​

diff
  const nav = createListNavigation({
    getItems: () => items,
-   onNavigate: ({ item }) => item.focus(),
  });

- list.addEventListener('keydown', nav.handleKeydown);
+ const onKeydown = (event: KeyboardEvent) => {
+   const result = nav.handleKeydown(event);
+   result?.change?.item.focus();
+ };
+ list.addEventListener('keydown', onKeydown);

Programmatic navigation also returns the operation snapshot:

ts
nav.navigate('next')?.item.focus();

Inspect keyboard handling explicitly ​

handleKeydown() returns null for disabled, already-prevented, composing, or unrecognized events. Recognized keys return:

ts
{
  handled: true,
  change: ListNavigationChange<T> | null,
}

change is null when a recognized boundary key is consumed without moving. Successful typeahead returns a change; set typeahead.preventDefault: true when printable keys should be consumed. Leave it false for editable combobox inputs.

Remove artificial navigation lifecycle ​

createListNavigation() no longer owns resources, listeners, or timers. Remove:

  • signal
  • dispose()
  • disposed
  • disposalSignal
  • [Symbol.dispose]()

The layer that attaches keydown owns listener removal. Framework adapters that add reactive state or signals remain disposable at that owning layer.

Validate navigation configuration ​

  • set(index) accepts only integer indexes; invalid or disabled indexes reset to -1.
  • Conflicting custom key assignments throw RangeError (replaced by FocusConfigError in Focus 3.1).
  • typeahead.delayMs must be a positive finite number.
  • Typeahead timing uses a monotonic clock and ignores IME composition.

Focus restoration failures use fallbacks ​

restoreFocus() now treats throwing target getters or focus() implementations as failed attempts. It tries the configured fallback and returns false only when neither target can be restored.

NodeNext declarations ​

Published declarations use explicit .js relative specifiers and are checked through strict NodeNext and ESM/CJS artifact fixtures.