Electron Adventures: Episode 27: File Manager Keyboard Controls
Tags: javascript, electron, svelte
Published: 2021-08-21 00:38:14

This episode was created in collaboration with the amazing [Amanda Cavallaro](https://www.amandacavallaro.com/).

The very point of Orthodox File Managers is that they're designed for 100% keyboard use. You can still use a mouse occasionally, but it's keyboard first world. And since that's not how browsers work, we'll need to implement all those things ourselves.

I'll start with code we had in the previous episode, and just add some keyboard shortcuts.

### Keyboard Shortcuts

Right now I'll add the most basic keyboard shortcuts:

* down arrow goes to the next item in the panel, unless we're at the end already
* up arrow goes to the previous item in the panel, unless we're at the top already
* tab switches between the panels
* space flips the selection state of the current item, and goes to the next item if possible - this way it's very easy to mark a bunch of items at once

### How browser keyboard events work

Short answer - unfortunately not the way we want. When some `<input>` element is focused, it gets keyboard events. But as don't do any of the "focusing" here, all events just go to the top level - the `window`.

To route them to the right place, we'd need to do some complicated event routing. Fortunately Svelte has our back here, and any component can attach event handlers to `<svelte:window>` element, and Svelte will do the right thing.

### `App.svelte` Tab handling

The main component needs to handle one event. Pressing `Tab` should switch which panel is active.

This needs tho following additions:

```html
<script>
    let handleKey = (e) => {
    if (e.key === "Tab") {
      if (activePanel === "left") {
        activePanel = "right"
      } else {
        activePanel = "left"
      }
      e.preventDefault()
    }
  }
</script>

<svelte:window on:keydown={handleKey}/>
```

Notice that `e.preventDefault()` is conditional - it will only happen if Tab key was pressed.

### `Panel.svelte`

We need to handle a total of five events:

* arrow up key - go to previous item
* arrow down key - go to next item
* space key - flip selection, go to next item
* left click - activate panel, go to clicked item
* right click - activate panel, go to clicked item, flip selection

There's some overlap between them, so I extracted some logic into helpers functions. Here's the changes:

```html
<script>
  export let position
  export let files
  export let active
  export let onActivate

  let focused = files[0]
  let selected = []
  let onclick = (file) => {
    onActivate(position)
    focused = file
  }
  let onrightclick = (file) => {
    onActivate(position)
    focused = file
    flipSelected(file)
  }
  let flipSelected = (file) => {
    if (selected.includes(file)) {
      selected = selected.filter(f => f !== file)
    } else {
      selected = [...selected, file]
    }
  }
  let goUp = () => {
    let i = files.indexOf(focused)
    if (i > 0) {
      focused = files[i - 1]
    }
  }
  let goDown = () => {
    let i = files.indexOf(focused)
    if (i < files.length - 1) {
      focused = files[i + 1]
    }
  }
  let handleKey = (e) => {
    if (!active) {
      return
    }
    if (e.key === "ArrowDown") {
      e.preventDefault()
      goDown()
    }
    if (e.key === "ArrowUp") {
      e.preventDefault()
      goUp()
    }
    if (e.key === " ") {
      e.preventDefault()
      flipSelected(focused)
      goDown()
    }
  }
</script>

<svelte:window on:keydown={handleKey}/>
```

There's one more bit of logic, and Svelte is helping us here. `window` receives all key presses, and every component which registered `on:keydown` on it wil get called. This means both `Panel`s get sent every key, so it's each `Panel`'s responsibility to check if it's active, and only handle events if it is.

For mouse events, we don't do any such checks, as we know which panel is clicked, and mouse events activate a `Panel` if non-active one got clicked.

Logic as implemented assumes each item on the list will be unique, which is always true for files, as you cannot have two files with same name in same folder. But if you try to adapt this code for other kinds of items, you might need to save focused / selected positions, not names.

Also for special keys like arrows and Tab, we must handle `keydown` not `keypress` events. `keypress` will only trigger for normal keys like letters, numbers, and space.

### Result

Here's the results, still looking just as our static mockup:

![Episode 27 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9e3iltmu66wsz56nqfdl.png)

We'll continue working on our app in future episodes, but first I'll take a small detour to do something completely different.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-27-file-manager-keyboard-controls).