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}/>
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}/>
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:
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.
Top comments (0)