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);
}
}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.
Rules
| Typed | Result |
|---|---|
| One letter | The next row after the current one that starts with it, wrapping past the end. |
| The same letter again | The next row that starts with it, so the letter cycles. |
| More letters | The row that starts with the whole run. The current row stays while it still matches. |
A pause of resetMs | The next key starts a new run. |
| Space | Joins 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 field | Left 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
| Name | Description | Type | Default |
|---|---|---|---|
wrTypeahead(options?) | Returns a WrTypeahead. One per list: it holds the run. | (options?: WrTypeaheadOptions) => WrTypeahead | — |
options.resetMs | The pause, in milliseconds, after which the next key starts a new run. | number | 500 |
options.locale | The 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 | — |
active | Whether a run is in progress. | boolean | — |
reset() | Drops the run. | () => void | — |
See also
Guide KeyboardBind shortcuts, show them as keycaps, and see which keys each component handles.Util wrIsPrintableKeyTrue for a single printable character typed without Ctrl, Cmd or Alt (Shift is allowed).Util wrIsComposingTrue while an input method (IME) is composing, so the key belongs to it and not to your handler.Util WR_KEYSKeyboardEvent.key constants, the WrKey union of their values, and WR_KEY_LABELS, what each key is called on a keycap.Component SelectPick one or several values from a list, with search and free-text tags.Component TreeA nested list to browse and select from, inline or as a dropdown picker.Component DropdownA menu that opens from any trigger.