DEV Community

Cover image for Adding Twitter/VIM-inspired navigation to my React website
Luke Berry
Luke Berry

Posted on

Adding Twitter/VIM-inspired navigation to my React website

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 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 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 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);
    };
  }, []);

Enter fullscreen mode Exit fullscreen mode

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

const keysPressed = useRef("")
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Now we can add the key press to the ref:

pressedKeys.current += "g";
Enter fullscreen mode Exit fullscreen mode

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 = "";
}
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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.

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();
    }
  };
Enter fullscreen mode Exit fullscreen mode

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>
  );
...
Enter fullscreen mode Exit fullscreen mode

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

Thanks for reading! If you enjoyed this follow me on twitter and star phived on github.

Top comments (3)

Collapse
 
lemoscaio profile image
Caio Lemos

Great article and great feature, man!

I would suggest also adding a debounce in case you decide to add more commands. This way, if the user presses "g" but then decides to send another command, it wouldn't trigger an unwanted command that may fit the sequence.

Adding a debounce would discard the pressed key if the user doesn't type anything else (or an invalid command sequence) in, like, 1s.

Collapse
 
lukeberrypi profile image
Luke Berry

Thank you for the feedback! That is a super important point, not only for avoiding unwanted keyboard combinations, but also long pauses between the g keystrokes.

Currently, I can press g, wait for 10 seconds, and press g again and the shortcut will work, which feels clunky at best. I will add the debounce, thanks!

Collapse
 
lixeletto profile image
Camilo Micheletto

Nice! Maybe using aria-keyshortcuts to enable it for accessibility would improve further the experience