Create Keyboard Shortcuts with RxJS

The cleanest way to create and orchestrate Keyboard Shortcuts with RxJS.

Authors
Gary Großgarten Gary Großgarten
Published at

I recently tried to add some keyboard shortcuts to my Angular app. 🤯 But don't worry, the solution is quite simple. At least Brent Rambo approves.

Brent Rambo - Shortcut master

In this Quick Tip, I'll show you what I came up with, using RxJS. This demonstration is done in an Angular Workspace scaffolded with the Angular CLI.

Implementation

The shortcut function below can be used to effortlessly create Observables for any keyboard shortcut. A keyboard shortcut is an array of keycodes (event.code), each representing a key of your keyboard. Grab the KeyCode Enum here.

See the comments in the code for explanation:

export const shortcut = (shortcut: KeyCode[]): Observable<KeyboardEvent[]> => {
  // Observables for all keydown and keyup events
  const keyDown$ = fromEvent<KeyboardEvent>(document, 'keydown');
  const keyUp$ = fromEvent<KeyboardEvent>(document, 'keyup');

  // All KeyboardEvents - emitted only when KeyboardEvent changes (key or type)
  const keyEvents$ = merge(keyDown$, keyUp$).pipe(
    distinctUntilChanged((a, b) => a.code === b.code && a.type === b.type),
    share()
  );

  // Create KeyboardEvent Observable for specified KeyCode
  const createKeyPressStream = (charCode: KeyCode) =>
    keyEvents$.pipe(filter((event) => event.code === charCode.valueOf()));

  // Create Event Stream for every KeyCode in shortcut
  const keyCodeEvents$ = shortcut.map((s) => createKeyPressStream(s));

  // Emit when specified keys are pressed (keydown).
  // Emit only when all specified keys are pressed at the same time.
  // More on combineLatest below
  return combineLatest(keyCodeEvents$).pipe(
    filter<KeyboardEvent[]>((arr) => arr.every((a) => a.type === 'keydown'))
  );
};
**[combineLatest](https://rxjs-dev.firebaseapp.com/api/index/function/combineLatest)**: Whenever any input `Observable` emits, `combineLatest` will compute and emit all the latest values of all inputs. However, `combineLatest` will wait for all input `Observables` to emit at least once, before it starts emitting results.

Usage

Usage is simple. Just call the shortcut function with your desired KeyCode combination. Then subscribe to the result and handle the keyboard shortcut. More examples can be found in the repo.

const commaDot$ = shortcut([KeyCode.Comma, KeyCode.Period]);

const ctrlO$ = merge(
  shortcut([KeyCode.ControlLeft, KeyCode.KeyO]),
  shortcut([KeyCode.ControlRight, KeyCode.KeyO])
);

commaDot$.pipe(tap(() => doSomething())).subscribe();

Bonus

The shortcut function emits whenever the specified keys are pressed simultaneously, no matter in which order they were pressed. If the sequence of the key presses is also important to you, you can use the pipeable operator below.

export function sequence() {
  return (source: Observable<KeyboardEvent[]>) => {
    return source.pipe(
      filter((arr) => {
        const sorted = [...arr]
          .sort((a, b) => (a.timeStamp < b.timeStamp ? -1 : 1))
          .map((a) => a.code)
          .join();
        const seq = arr.map((a) => a.code).join();
        return sorted === seq;
      })
    );
  };
}

Then use it like this

const abc$ = shortcut([KeyCode.KeyA, KeyCode.KeyB, KeyCode.KeyC]).pipe(
  sequence()
);

Now the abc$ Observable will only emit when the keys are pressed sequentially (a->b->c).

Limitations

Beware that keyboard shortcuts could collide with global shortcuts specified by your OS or Browser (e.g. Spotlight on Mac for Cmd+Space). Also there are cases where keyup events will be skipped when certain keys are pressed down. This is particularly the case with the cmd key (KeyCode.MetaRight and KeyCode.MetaLeft) on mac.


If you have further questions on the article or just want to say hi, feel free to slide into my twitter dms.🐦

Table of Contents

Top of Page Sorry, could not load static page content Comments Related Articles

Related Posts

Find more posts like this one.

Sign up for our newsletter

Sign up for our newsletter to stay up to date. Sent every other week.

We care about the protection of your data. Read our Privacy Policy.