🛠️ Datepicker: Keyboard shortcuts

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Datepicker: Keyboard shortcuts

We’re going to provide users with these keyboard shortcuts:

  1. Arrow left: Select previous day
  2. Arrow right: Select next day
  3. Arrow up: Select 7 days before
  4. Arrow down: Select 7 days later

Better focus styles

While I worked on this lesson, I realized the focus on each button wasn’t clear enough. I added some CSS to emphasise them.

Here’s the CSS I added: (You’ll find them in the starter file).

.datepicker_date-grid button:focus {
  box-shadow: inset 0 0 0 2px var(--blue-grey-100);
}

.datepicker_date-grid button.is-selected:focus {
  box-shadow: inset 0 0 0 2px var(--teal-200);
}

Up/Down/Left/Right shortcuts

We only want these shortcuts to work inside the Datepicker. So let’s start by adding an event listener to the Datepicker.

datepicker.addEventListener('keydown', event => {
  // ...
})

This time, we only want to do something if the user presses an arrow key.

datepicker.addEventListener('keydown', event => {
  const { key } = event
  if (
    key !== 'ArrowUp' &&
    key !== 'ArrowDown' &&
    key !== 'ArrowLeft' &&
    key !== 'ArrowRight' &&
  ) return
})

We also want the prevent the default behaviour of any arrow keys (so the browser doesn’t scroll while the datepicker is active).

datepicker.addEventListener('keydown', event => {
  const { key } = event
  if (
    key !== 'ArrowUp' &&
    key !== 'ArrowDown' &&
    key !== 'ArrowLeft' &&
    key !== 'ArrowRight' &&
  ) return
  event.preventDefault()
})

Focusing on the first date

If the user selected a date, we want to focus on that date. Once they have their attention on a date, the next up/down/left/right movement would make sense.

If the user has selected a date, there would be a <button> with an is-selected class.

We will also use a return statement here to prevent further execution of the function.

datepicker.addEventListener('keydown', event => {
  // ...
  const selectedDate = dategrid.querySelector('is-selected')
  const dates = [...dategrid.children]
  if (!selectedDate) {
    dates[0].focus()
    return
  }
})

Focusing on the next date

Let’s say the user selects 13 February. If they press the right arrow now, we want to highlight 14 February.

To do this, need to find 14 February. To find 14 February, we need to get the index of the selected date.

datepicker.addEventListener('keydown', event => {
  // ...
  const index = dates.findIndex(d => d === selectedDate)
  if (key === 'ArrowRight') {
    const nextDate = dates[index + 1]
    nextDate.focus()
  }
})

We’ve done all we need by now. So we’ll use a return statement here to prevent the function from executing further. This makes it easier to write other conditionals.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowRight') {
    const nextDate = dates[index + 1]
    nextDate.focus()
    return
  }
})

We can collapse the three statements inside this if condition into one. This makes it easier to read and understand.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowRight') return dates[index + 1].focus()
})

Pressing right again

Let’s say the user presses the right arrow key when their focus is on 14 February. In this case, we want to select 15 February. To do this, we need to use the index of the currently focused date (instead of the selected date).

datepicker.addEventListener('keydown', event => {
  // ...
  let index = dates.findIndex(d => d === selectedDate)
  const focusedDate = document.activeElement
  if (focusedDate.matches('.datepicker__date')) {
    index = dates.findIndex(d => d === focusedDate))
  }
  if (key === 'ArrowRight') return dates[index + 1].focus()
})

We can clean up the code a little by using a ternary operator.

datepicker.addEventListener('keydown', event => {
  // ...
  const index = document.activeElement.matches('.datepicker__date')
    ? dates.findIndex(d => d === document.activeElement)
    : dates.findIndex(d => d === selectedDate)
  // ...
})

We also need to allow the code to reach this point. So, if the user has focus on a date (but did not select a date yet), we don’t force focus onto the first date.

datepicker.addEventListener('keydown', event => {
  // ...
  if (!selectedDate && !document.activeElement.matches('.datepicker__date')) {
    return dates[0].focus()
  }
  // ...
})

Pressing right on the last day

Let’s say the user press the Right arrow key when their focus is on the last day (28 February). In this case, we want to highlight the first day of the next month (1 March).

Here, we first need to check if they are on the last day. We’ll use another if condition for the checking. We will also put the new if condition before the previous if condition because it’s more specific.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowRight' && index === dates.length - 1) {
    // ... Highlight first day of the next month
  }
  if (key === 'ArrowRight') /*...*/
})

To focus on the next month, we need to change the calendar such that it shows the next month. We can do it by clicking the next month’s button.

datepicker.addEventListener('keydown', event => {
  // ...
  const nextMonthButton = datepicker.querySelector('.datepicker__next')
  if (key === 'ArrowRight' && index === dates.length - 1) {
    nextMonthButton.click()
  }
  // ...
})

We need to find a new set of dates since we changed the calendar’s HTML. We can find the dates with querySelectorAll.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowRight' && index === dates.length - 1) {
    // ...
    const dates = datepicker.querySelectorAll('.datepicker__dates')
  }
  // ...
})

After we find the new set of dates, we can focus on the first day.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowRight' && index === dates.length - 1) {
    // ...
    const dates = datepicker.querySelectorAll('.datepicker__dates')
    dates[0].focus()
  }
  // ...
})

We’ve done what we need to. Let’s use an early return statement so the next if condition doesn’t trigger.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowRight' && index === dates.length - 1) {
    // ...
    const dates = datepicker.querySelectorAll('.datepicker__dates')
    dates[0].focus()
    return
  }
  // ...
})

Focusing on the previous day

We can repeat the same process to focus on the previous day.

First, we focus on the previous day using the same index value. This time, we subtract 1 from index instead of adding 1 to it.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'LeftArrow') return dates[index + 1].focus()
})

Pressing left on the first day

If the user presses Left from the first day of the month (1 February), we want to highlight the last day of the previous month (31 January).

To do this, we need a conditional to check if they’re on the first day.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'LeftArrow' && index === 0) {
    // Highlights last day of previous month
  }
  if (key === 'LeftArrow') /*...*/
  // ...
})

We need to show the previous month’s dates to highlight the last day. So the first thing to do is to click the previous month’s button.

datepicker.addEventListener('keydown', event => {
  // ...
  const previousMonthButton = datepicker.querySelector('.datepicker__previous')
  if (key === 'LeftArrow' && index === 0) {
    previousMonthButton.focus()
  }
  // ...
})

Then, we need to find the new set of dates. Once we find the new set of dates, we can highlight the last day.

datepicker.addEventListener('keydown', event => {
  // ...
  const previousMonthButton = datepicker.querySelector('.datepicker__previous')
  if (key === 'LeftArrow' && index === 0) {
    // ...
    const dates = datepicker.querySelectorAll('.datepicker__dates')
    dates[dates.length - 1].focus()
    return
  }
  // ...
})

Focusing on the previous week

If the user presses Up, we want to focus on the previous week. Since one week is 7 days, we can subtract 7 from the index to find the previous week’s date.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowUp') return dates[index - 7].focus()
})

Pressing Up on the first week

If the index is small than 7, we know the user is on the first week of the month. In this case, we need to select the date that’s on the last week of the previous month.

Let’s say the user has focus on 6 February 2019. If they press up, we want to focus on 30 January 2019.

First, we need to show the previous month by clicking on the previous month’s button.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowUp' && index < 7) {
    previousMonthButton.click()
  }
  if (key === 'ArrowUp') /*...*/
})

Then, we need to find the new set of dates.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowUp' && index < 7) {
    previousMonthButton.click()
    const dates = [...datepicker.querySelectorAll('.datepicker__date')]
  }
  if (key === 'ArrowUp') /*...*/
})

Then, we need to do some math to get the correct date to focus on.

  • If index is 6, we want the last day of the previous month
  • If index is 5, we want the 2nd last day of the previous month
  • If index is 4, we want the 3rd last day of the previous month
  • And so on.

We can do it with this calculation: dates.length - (7 - index).

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowUp' && index < 7) {
    previousMonthButton.click()
    const dates = [...datepicker.querySelectorAll('.datepicker__date')]
    const date = dates[dates.length - (7 - index)]
    date.focus()
    return
  }
  // ...
})

Focusing on the next week

If the user presses Down, we want to focus on the next week. We can use the same 7-day calculation to get the next week’s date.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowDown') return dates[index + 7].focus()
})

Pressing down from the last week

If index + 7 is more than the number of days in the month, we know the user is pressing Down from the last week. In this case, we need to focus on the next week in the next month.

Let’s say the user presses Down from 27 February 2019. In this case, we want to focus on 6 March 2019.

First, we need to show the next month by clicking on the next month’s button.

datepicker.addEventListener('keydown', event => {
  // ...
  const daysInMonth = dates.length
  if (key === 'ArrowDown' && index + 7 > daysInMonth) {
    nextMonthButton.click()
  }
  // ...
})

Then, we find the new set of dates.

datepicker.addEventListener('keydown', event => {
  // ...
  const daysInMonth = dates.length
  if (key === 'ArrowDown' && index + 7 > daysInMonth) {
    // ...
    const dates = [...datepicker.querySelectorAll('.datepicker__date')]
  }
  // ...
})

We need to find the correct day of the week to focus on. The calculation is: index + 7 - daysInMonth.

datepicker.addEventListener('keydown', event => {
  // ...
  if (key === 'ArrowDown' && index + 7 > daysInMonth) {
    nextMonthButton.click()

    const dates = [...datepicker.querySelectorAll('.datepicker__date')]
    const date = dates[index + 7 - daysInMonth]
    date.focus()
    return
  }
  // ...
})

Arranging the if conditions

You can arrange the if conditions however you want. In this case, I think this is a good flow:

if (!event.shiftKey) {
  // Previous day
  if (index === 0 && key === 'ArrowLeft') {/*...*/}
  if (key === 'ArrowLeft') {/*...*/}

  // Next day
  if (index === dates.length - 1 && key === 'ArrowRight') {/*...*/}
  if (key === 'ArrowRight') {/*...*/}

  // Previous Week
  if (key === 'ArrowUp' && index < 7) {/*...*/}
  if (key === 'ArrowUp') {/*...*/}

  // Next Week
  if (key === 'ArrowDown' && index + 7 > dates.length) {/*...*/}
  if (key === 'ArrowDown') {/*...*/}
}

That’s it!

Extra challenge

I have an extra challenge for you for this datepicker. Try and see if you provide these additional keyboard shortcuts:

  1. Shift + left: Select same day in previous month
  2. Shift + right: Select same day next month
  3. Shift + up: Select same day previous year
  4. Shift + down: Select same day next year

Additional notes:

  1. If focus is on 31 May, and user presses Shift + left, you need to select 30 April (because there is no 31 April).

You can find the solution to the extra challenge here.