DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Updated on

Electron Adventures: Episode 27: File Manager Keyboard Controls

This episode was created in collaboration with the amazing Amanda Cavallaro.

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:

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

<svelte:window on:keydown={handleKey}/>
Enter fullscreen mode Exit fullscreen mode

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:

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

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 Panels 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

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.

Discussion (0)