If they pressed Up, we want to select the previous task. We can find the previous task with previousElementSibling.
If they pressed Down, we want to select the next task. We can find the next task with nextElementSibling.
document.addEventListener('keydown', event => {
// ...
if (key === 'ArrowUp' && key === 'ArrowDown') {
// ...
if (event.target.closest('.task')) {
// ...
if (key === 'ArrowUp') return currentTaskElement.previousElementSibling.focus()
if (key === 'ArrowDown') return currentTaskElement.nextElementSibling.focus()
}
}
})
If the user presses Up on the first task, we want to select the last task. Here, we can check if the currentTaskElement is the first task.
You might add the condition like this:
document.addEventListener('keydown', event => {
// ...
if (key === 'ArrowUp' && key === 'ArrowDown') {
// ...
if (event.target.closest('.task')) {
// ...
if (key === 'ArrowUp') return currentTaskElement.previousElementSibling.focus()
if (key === 'ArrowDown') return currentTaskElement.nextElementSibling.focus()
if (currentTaskElement === firstTask && key === 'ArrowUp') return lastTask.focus()
}
}
})
Our latest condition wouldn’t work. Why? Because the key === 'ArrowUp' would be fulfilled first. For the latest condition to work, we need to put it before key === 'ArrowUp'.
document.addEventListener('keydown', event => {
// ...
if (key === 'ArrowUp' && key === 'ArrowDown') {
// ...
if (event.target.closest('.task')) {
// ...
if (currentTaskElement === firstTask && key === 'ArrowUp') return lastTask.focus()
if (key === 'ArrowUp') return currentTaskElement.previousElementSibling.focus()
if (key === 'ArrowDown') return currentTaskElement.nextElementSibling.focus()
}
}
})
If the user presses Down on the last task, we want to select the first task. Here’s how you do it:
There’s a white background when we hover on a task. We want to add this white background on focus as well. We can also accentuate the focus by styling it differently from the default.
We want to keep the white background if the user focuses on an element inside the task. We can do this with focus-within.
.task:focus-within {
background-color: #fff;
}
Completing a task
Most (real) Todolist applications let users complete a task with a keyboard shortcut. This shortcut is usually Command + Enter on Mac (or Control + Enter on Windows).
We can allow users to complete tasks with the same keyboard shortcut. To do this, we need to check for the Super key.
const isSuperKey = event => {
const os = navigator.userAgent.includes('Mac OS X') !== -1
? 'mac'
: 'windows'
if (os === 'mac' && event.metaKey) return true
if (os === 'windows' && event.ctrlKey) return true
}
Since we’re listening to Super + Enter on tasks, we’ll use the event delegation pattern.
Once we have the task, we can find the checkbox. And once we have the checkbox, we can “click” the checkbox. The rest will be done by the click event listener.
Users might press the Backspace key by accident (especially since Backspace is used to delete a character inside an input). It can be frustrating for users if they delete things accidentally and they can’t undo it.
The simplest way to prevent accidental deletions is to make the keyboard shortcut slightly harder to use. This is why I choose to use Super + Backspace to delete a task.
Users who use an extended keyboard (with the number pad and stuff) have the Delete key on their keyboard. This key is rarely used. If a user presses the Delete key, we can be sure they want to delete something.
We can also allow users to delete a task with the Delete key.
One of the most important things for Todolist is speed (because speed leads to productivity). This is why we create keyboard shortcuts for Todolist applications.
The most important thing a user can do in a Todolist is create new tasks. We want to make a keyboard shortcut for users to create tasks easily.
In this, I chose to use the n key to create focus on the new task field, so they can begin writing their task.
It doesn’t matter where the user presses n. We always want n to focus on the new task field, which is why we listen to keydown on the document.
document.addEventListener('keydown', event => {
if (event.key !== 'n') return
})
There’s a possibility that users have their Capslock turned on. If they press n with their Capslock turned on, we still want to focus on the new task field. (Our keyboard shortcut should be case -insensitive).
To focus, on the new task field, we need to find the new task field. Then, we focus on it.
// Press n to focus on task
document.addEventListener('keydown', event => {
// ...
const newTaskField = todolist.querySelector('input')
newTaskField.focus()
})
The focus works. But unfortunately, it enters n into the new task field as well.
This happens because the keydown event is followed by the input event.
// Press n to focus on task
document.addEventListener('keydown', event => {
// ...
newTaskField.focus()
console.log(event.type)
})
document.addEventListener('input', event => {
console.log(event.type)
})
To prevent n from showing up, we can prevent the keydown event from creating an input event
// Press n to focus on task
document.addEventListener('keydown', event => {
// ...
event.prevenDefault()
const newTaskField = todolist.querySelector('input')
newTaskField.focus()
})
However, since we used preventDefault, we can’t type n in the new task field.
To allow users to type normally in the new task field, we need to bail if the event originates there.
document.addEventListener('keydown', event => {
if (event.key !== 'n') return
if (event.target.matches('#new-task')) return
// ...
})
But we also want to let users type normally if they’re editing a task. So, instead of checking if they’re focusing on #new-task, we check if they focus on a text input field.
document.addEventListener('keydown', event => {
if (event.key !== 'n') return
if (event.target.matches('input[type="text"]')) return
// ...
})