Adding Twitter/VIM-inspired navigation to my React website
Tags: webdev, javascript, programming
Published: 2024-02-21 16:46:32

I recently started learning VIM and became enchanted by its navigation. I am also a twitter addict, which has excellent shortcuts.

Inspired by these, I added similar navigation to my project. And I will show you how to do this in React in this article.

I created [phived.com](https://phived.com) a year ago with the purpose of being a minimal to-do list that would help me remember things I had to do.

Since then, I have used it everyday. I added some shortcuts:

- `enter` to go to the next task
- `shift + enter` to go to the previous task
- `cmd + enter` to complete a task

> [Here is the code](https://github.com/LukeberryPi/phived/blob/afce487a817e0c5cf305e47cf2b8ce8467f6b711/src/components/GeneralTasks.tsx#L81) for these behaviors. Since it isn't the focus of this article, I won't explain it further. 

This mostly kept my hands off the mouse. But another feature changed things...

After using the website routinely, I found myself adding the same tasks every day. I always pushed back the idea of adding *more* than five tasks at a time, since that would undermine the original purpose of the project.

I ended up adding a [/daily](https://phived.com/daily) page, similar to the original tasks, with a key difference: tasks that you complete can be restored tomorrow: so you don't have to type them everyday!

Now, I wanted to toggle between `phived.com` and `phived.com/daily` tasks using `g + g`, inspired by twitter's shortcuts such as `g + h` to go to Home, `g + n` to go to Notifications, etc...

I achieved this with the following code. It belongs to the Daily component and redirects us to "/" when the user presses the combination `g + g`.

```
  const pressedKeys = useRef("");

  useEffect(() => {
    const handleKeyPress = (event: KeyboardEvent) => {
      const inputIsFocused = document.activeElement instanceof HTMLInputElement;

      if (event.key !== "g" || inputIsFocused) {
        return;
      }

      pressedKeys.current += "g";

      if (pressedKeys.current === "gg") {
        window.location.href = "/";
        pressedKeys.current = "";
      }
    };

    window.addEventListener("keydown", handleKeyPress);

    return () => {
      window.removeEventListener("keydown", handleKeyPress);
    };
  }, []);

```

Let's dive in! First, a ref will store our key presses:

```
const keysPressed = useRef("")
```

Our useEffect has one goal: listen to the keydown event and store them in the ref. 

> An important caveat is that, if the input is focused (user is editing a task) the redirect shouldn't happen. Imagine you're typing "buy e **gg** s" as a task, and it suddenly jumps you to another page.

We avoid that by returning early, which we also do if the key pressed isn't `g`:

```
const inputIsFocused = document.activeElement instanceof HTMLInputElement;

if (event.key !== "g" || inputIsFocused) {
  return;
}
```

Now we can add the key press to the ref:

```
pressedKeys.current += "g";
```

Check if the combination is `g + g` and redirect towards our general tasks (that live on `phived.com`, remember). We also empty the ref since it has fulfilled its cycle:

```
if (pressedKeys.current === "gg") {
  window.location.href = "/";
  pressedKeys.current = "";
}
```

We then add (and remove) the eventListener with useEffect, so that it hooks into the component's lifecycle correctly:

```
window.addEventListener("keydown", handleKeyPress);

return () => {
  window.removeEventListener("keydown", handleKeyPress);
};
```

And voilà! It works as intended. But not quite...

Since we can't redirect while input is focused, when can we redirect? When the focus is somewhere else. Currently, the only way to do that is by clicking another element. And that's not what we're looking for!

We are missing a crucial step, that Twitter implements in its website: pressign `ESC` to blur from input.

<img width="100%" style="width:100%" src="https://media.giphy.com/media/RBbsJrhaZ0nK6GYW3G/giphy.gif">

The following code does this. If there's an active element and the key pressed is `ESC`, we blur (remove focus).

```
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
    if (!(document.activeElement instanceof HTMLElement)) {
      return;
    }

    if (e.key === "Escape") {
      document.activeElement.blur();
    }
  };
```

Now, we just assign the onKeyDown prop to handleKeyDown on the element that surrounds the components where we want this behavior. In my case, that is a `div`.

```
...
return (
    <GeneralTasksContextProvider>
      <div
        onKeyDown={handleKeyDown}
        className="flex h-full w-full flex-col items-center justify-center bg-softWhite dark:bg-trueBlack"
      >
        <HelmetProvider>
          <Head />
        </HelmetProvider>
        <ModeSelectorMobile />
        <Header />
        <GeneralTasks />
        <Message />
        <Footer />
      </div>
    </GeneralTasksContextProvider>
  );
...
```

Now it works: we can toggle between the general and daily tasks by pressing `ESC + g + g` - without touching the mouse!

<img width="100%" style="width:100%" src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExMXRhZXFqNjVkNWp1YWdiMXZ0YnkwZzlkNG5pMTV6aXRhMzdiNHJodyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/FPrcuySxmZXLjsjU0e/giphy.gif">

Thanks for reading! If you enjoyed this follow me on [twitter](https://twitter.com/lukeberrypi) and [star phived on github](https://github.com/LukeberryPi/phived).