Driving the Calendar with the Keyboard
We added shortcuts to the desktop calendar — and found a tiny bug only after we did.
Driving the Calendar with the Keyboard
Everything in the desktop app works with a mouse. Pick a date, create an event, jump back to today — all one click each. Nothing was broken.
But repeat the same actions a few times in a single sitting, and the click overhead becomes noticeable. Find the “today” button, move the mouse there, click, move it back somewhere else. Short, but choppy.
On desktop, the keyboard is sometimes faster.
What Deserves a Shortcut
Mapping everything to a shortcut is a bad idea. The more you have to memorize, the less you’ll use any of them. We picked only the actions that come up constantly.
- N — new event
- G — generate from template
- T — go to today
- ←→↑↓ — move the selected date (in month view: left/right is one day, up/down is one week)
- Enter — open the selected day’s detail
That’s a one-handful of shortcuts, all corresponding to the most common calendar actions.
Stopping Leaks
The first thing we did after adding the shortcuts was build guards.
The failure mode is obvious. The user opens a modal and types into a text field — pressing “n” should add an “n” to the text, not pop another modal. Or in a filter modal’s keyword input, pressing ←→ should move the cursor between letters, not jump the calendar to a different day.
So at the very top of the handler, we check two things. If the focused element is an input, textarea, or contentEditable, do nothing. If any modal is open, do nothing. The shortcuts only fire when the calendar itself is the active surface.
We’ve been burned before by forgetting this guard on another page. This time we built it in from the start.
A Subtle Jump
It seemed done. Then we noticed something off. Pressing ←→ to move within May would scroll the calendar down to June. Even when staying inside the same month.
We assumed at first that the “scroll to that month when crossing a boundary” logic was just too aggressive. Pressing → on May 31 should land on June 1 and scroll to June — that part is intentional. But pressing → on May 15 was triggering the same scroll.
Back to the code. There’s a comparison between “the month currently in view” and “the month of the next selected date.” Turns out one side was 0-indexed (the way JavaScript’s getMonth() returns it) and the other side was 1-indexed. So inside May (index 4), the comparison always read as “different month,” and the scroll triggered every time.
Inconsistency in how months are represented across the codebase was the real culprit. Sometimes 0-based, sometimes 1-based. Mix the two in a single line and you get a quiet off-by-one that doesn’t go away.
Removing the -1 on one side and the +1 on the other side made it behave. Movement inside a month is silent now; only the actual transition between months scrolls.
Small Change, Big Difference
Adding keyboard navigation changed how time is spent in the calendar.
Reviewing today’s tasks went from a click-and-drag flow to a few keystrokes. T returns you to today. Two presses of ← shows day-before-yesterday. The mouse stays still, the eyes stay on the calendar.
A handful of shortcuts on a frequently-used page. Memorizable in a sitting, and once they’re in your hand, they replace hundreds of mouse motions. The flow gets noticeably smoother without adding any new feature at all.