UtilAdded in v16.4Optional

wrTypeahead

Type-to-jump for a list: the letters typed in a row select the next row that starts with them.

Usage

It holds the letters typed so far. Pass the labels and the current row with each keydown, and move your own cursor to the index that comes back. wr-select, wr-dropdown, wr-tree, wr-context-menu and wr-cascader run on it.

import { wrTypeahead } from 'ngwr/utils';

@Component({
  host: { '(keydown)': 'onKeydown($event)' },
})
export class Menu {
  readonly labels = input.required<readonly string[]>();
  protected readonly cursor = signal(0);
  private readonly typeahead = wrTypeahead();

  protected onKeydown(event: KeyboardEvent): void {
    const index = this.typeahead.handleKey(event, this.labels(), this.cursor());
    if (index === null) return this.onOtherKey(event);
    event.preventDefault();
    if (index >= 0) this.cursor.set(index);
  }
}

Rules

TypedResult
One letterThe next row after the current one that starts with it, wrapping past the end.
The same letter againThe next row that starts with it, so the letter cycles.
More lettersThe row that starts with the whole run. The current row stays while it still matches.
A pause of resetMsThe next key starts a new run.
SpaceJoins a run that has started. As the first key it is left to you.
Ctrl, Meta or Alt, an IME key, a key in a text fieldLeft to you: handleKey returns null.

Disabled rows and language

A row passed as { label, disabled: true } is never a match. Case is folded in locale, which can be a function, so it follows a language switch.

const locale = inject(WR_LIVE_LOCALE, { optional: true });
const typeahead = wrTypeahead({ resetMs: 700, locale: () => locale?.() });

typeahead.handleKey(event, [
  { label: 'Copy' },
  { label: 'Copy link', disabled: true },
  { label: 'Cut' },
], current);

Key now, search later

type() takes the key and find() searches, for a list that is not there yet when the key arrives. wr-select opens on a letter this way.

if (typeahead.type(event)) {
  event.preventDefault();
  open();
  afterNextRender(() => {
    const index = typeahead.find(labels(), cursor());
    if (index >= 0) cursor.set(index);
  }, { injector });
}

API

NameDescriptionTypeDefault
wrTypeahead(options?)Returns a WrTypeahead. One per list: it holds the run.(options?: WrTypeaheadOptions) => WrTypeahead—
options.resetMsThe pause, in milliseconds, after which the next key starts a new run.number500
options.localeThe language the labels are in, for the case folding. The browser locale when omitted.string | (() => string | undefined)—
handleKey(event, items, current)Takes a keydown and answers the index to move to, -1 when the key joined the run and nothing matches, or null when the key is not a typeahead key.(event: KeyboardEvent, items: readonly (string | WrTypeaheadItem)[], current: number) => number | null—
type(event)Adds a keydown to the run. Returns whether the key was taken.(event: KeyboardEvent) => boolean—
find(items, current)The index the run points at, or -1.(items: readonly (string | WrTypeaheadItem)[], current: number) => number—
activeWhether a run is in progress.boolean—
reset()Drops the run.() => void—

See also