How to Build a Datepicker with Angular and CSS Grid Layout

This article aims to provide a starting point for building a calendar that can be used as a form control element in a reactive form or with the NgModel directive in a template-driven form. The calendar's features include picking a single date, disabling past dates and configuring the locale. No new dependency outside the @angular scope will be introduced. Styles will be written in SCSS, with class names following the BEM naming convention. The focus will be on the layout rather than on aesthetics.

As I built the calendar with a TDD approach, unit tests are also included in the article. If you prefer reading only the component code, you can hide the unit tests using the checkbox at the top.

Find the final project on Github: https://github.com/tamas-nemeth/angular-datepicker or try the live demo.

Introduction

When reimagining our flight search mobile web application at Liligo, our UX team had very specific design and behaviour requirements towards all the form controls. They designed a multi-month, vertically scrollable date range picker calendar, with the days of the week in a fixed modal header. The HTML5 solution, <input type="date">, would not do, as it still has poor browser support and also because supporting browsers provide different, unstylable controls for it (some do not even use calendars). Customising similar third-party components would have been cumbersome if not impossible. Therefore we built our own custom form controls. Based on that experience I put together this tutorial to show how I would approach building a custom datepicker from the ground up.

Liligo's datepicker viewed on a mobile device

Terminology - datepicker vs. calendar

Several third-party libraries make a distinction between what the words calendar and datepicker indicate. A calendar usually refers to the component that displays a list of dates selectable by clicking, tapping or keyboard navigation, while a datepicker consists of an input for editing a date value and/or a button that triggers a calendar to drop down or pop up in a modal. This article is focussed on creating a calendar, and discussing modal dialogs is beyond its scope. In spite of that, you can find an example using @angular/cdk/overlay in the project repository.

Component Architecture

Creating a new library

The calendar needs to have its own library so that it can be published on its own and reused in multiple apps/libs.

yarn ng g lib calendar

Alternative command for npm users:

npx ng g lib calendar

As one might not have @angular/cli installed globally, we rely on the local binary (located in node_modules/.bin) - through npx or yarn. The generated library contains a module, a component and a service. After adding CalendarModule to a feature module's imports array, CalendarComponent can be used in any component declared on that module.

<lib-calendar></lib-calendar> <!-- a real app would use a proper custom selector prefix -->

At this point the consumer component would display CalendarComponent with the following message:

calendar works!

Displaying the days of the week

Our calendar is going to show the abbreviated names of the days of the week (Sunday to Saturday) in a new component.

yarn ng g c days-of-week --project=calendar

DaysOfWeekComponent is going to create 7 elements with the days-of-week__day class containing the first letter of each day in a text node.

it('should display the 7 days of the week', () => {
  const narrowDaysOfWeek = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
  expect(getDaysOfWeek()).toEqual(narrowDaysOfWeek);
});

function getDaysOfWeek() {
  return getDayOfWeekDebugElements()
    .map(dayOfWeekDebugElement => dayOfWeekDebugElement.nativeElement.textContent);
}

function getDayOfWeekDebugElements() {
  return fixture.debugElement.queryAll(By.css('.days-of-week__day'));
}
export class DaysOfWeekComponent {
  narrowDaysOfWeek!: readonly string[];
}
<div class="days-of-week">
  <abbr class="days-of-week__day"
        *ngFor="let narrowDayOfWeek of narrowDaysOfWeek"
  >{{narrowDayOfWeek}}</abbr>
</div>

Angular's DatePipe and other pipes get their localisation data from @angular/common/locales, from TypeScript files like this default (United States English) locale file. Those locale files are a generated subset of the locale data in the Common Locale Data Repository (CLDR) maintained by the Unicode Consortium. Each Angular locale file stores most of its data in an Array. Fortunately, @angular/common also exports utilities for retrieving specific parts of those Arrays, e.g. getLocaleDayNames. getLocaleDayNames takes the locale as a string (e.g. 'en-US') and further formatting options as enum members. The locale used by the consumer component, module or app can be injected with the LOCALE_ID token.

import { FormStyle, getLocaleDayNames, TranslationWidth } from '@angular/common';
import { LOCALE_ID } from '@angular/core';
...
export class DaysOfWeekComponent implements OnInit {
  narrowDaysOfWeek!: readonly string[];

  constructor(@Inject(LOCALE_ID) private localeId: string) {}

  ngOnInit() {
    this.narrowDaysOfWeek = getLocaleDayNames(this.localeId, FormStyle.Format, TranslationWidth.Narrow);
  }
}

To be semantic, we have wrapped the day abbreviations in <abbr> elements. An abbr element may also have a title attribute which browsers may display in a tooltip on hover. It would also be useful to add an aria-label attribute to the element for accessibility. Screen readers will read the text in that attribute instead of the element's visible text content.

it('should display the 7 days of the week with the proper attributes', () => {
  const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  expect(getDayOfWeekTitles()).toEqual(daysOfWeek);
  expect(getDayOfWeekAriaLabels()).toEqual(daysOfWeek);
});

function getDayOfWeekTitles() {
  return getDayOfWeekDebugElements()
    .map(dayOfWeekDebugElement => dayOfWeekDebugElement.properties.title);
}

function getDayOfWeekAriaLabels() {
  return getDayOfWeekDebugElements()
    .map(dayOfWeekDebugElement => dayOfWeekDebugElement.attributes['aria-label']);
}
<div class="days-of-week">
  <abbr class="days-of-week__day"
        *ngFor="let dayOfWeek of daysOfWeek; index as index"
        [title]="dayOfWeek"
        [attr.aria-label]="dayOfWeek"
  >{{narrowDaysOfWeek[index]}}</abbr>
</div>

Instead of the narrowDaysOfWeek, we are iterating through the daysOfWeek as that is used twice in the template and get the narrow day of week from the array with the index from the NgFor directive.

export class DaysOfWeekComponent implements OnInit {
  narrowDaysOfWeek!: readonly string[];
  daysOfWeek!: readonly string[];

  constructor(@Inject(LOCALE_ID) private localeId: string) {}

  ngOnInit() {
    this.narrowDaysOfWeek = getLocaleDayNames(this.localeId, FormStyle.Format, TranslationWidth.Narrow);
    this.daysOfWeek = getLocaleDayNames(this.localeId, FormStyle.Format, TranslationWidth.Wide);
  }
}

Alternative locales

By default, Angular uses the en-US locale. Any further locales need to be imported and registered.

import { registerLocaleData } from '@angular/common';
import HungarianLocale from '@angular/common/locales/hu';

registerLocaleData(HungarianLocale);

AppModule, a feature module or a component might provide a different locale under the LOCALE_ID token from @angular/core.

providers: [
  {
    provide: LOCALE_ID,
    useValue: 'hu'
  }
]
days of week in Hungarian (some of the day names start with two-character letters)

Styling weekdays

.days-of-week without styles

The days of the week are currently in a single line with no space between them. Let's lay them out in a grid with 7 columns, 1 row, and 48x48px cells. 48x48px is the minimum recommended size for accessible tap targets according to the Material Design Guidelines. The document also advises using 8 pixels of gap between the tap targets, however, unclickable gaps would be undesirable in this scenario (the same grid is going to be applied to the rest of the calendar).

$calendar-cell-size: 3rem; // 48px / 16px
$number-of-days-in-a-week: 7;

.days-of-week {
  display: grid;
  grid-template-columns: repeat($number-of-days-in-a-week, $calendar-cell-size);
  grid-template-rows: $calendar-cell-size;
}

The grid items (children of the element with display: grid;) are stretched to full grid cell width and height (48x48px) by default. Let's center the text within the grid items horizontally and vertically.

.days-of-week__day {
  display: flex;
  justify-content: center;
  align-items: center;
}
.days-of-week with styles

Month caption

The year and the name of the month are going to be shown by a new component.

yarn ng g c month-header --project=calendar

MonthHeaderComponent is going to receive the month (a Date object) via input binding from CalendarComponent and display a caption for the month.

export class MonthHeaderComponent {
  @Input() month!: Date;
}
it('should display the year and the name of the month received via input', () => {
  component.month = new Date(2019, Month.February);

  fixture.detectChanges();

  expect(getCaption()).toEqual('February 2019');
});

function getCaption() {
  return fixture.debugElement.query(By.css('.month-header__caption')).nativeElement.textContent;
}
// date.utils.ts
export enum Month {
  January, // 0
  February,
  March,
  April,
  May,
  June,
  July,
  August,
  September,
  October,
  November,
  December
}

Our first approach might be to format the month with Angular's DatePipe.

<div class="month-header">
  <time class="month-header__caption"
        [dateTime]="month | date:'yyyy-MM'"
  >{{month | date:'MMMM yyyy'}}</time>
</div>
days of the week and month caption with styles (omitted)

However, our hard-coded date format would not work with locales in which the year and the month should be in a different order and/or should have separator characters between them. For instance, Spanish people would expect to see 'junio de 2019' and Hungarians would expect '2019. június'. Unfortunately, Angular's locales do not include a date format for displaying only the month and the year without the day of month. If we would like to stick with the DatePipe, we could create our own locale extension files (based on CLDR datetime formats, for instance) or retrieve the date format for the given locale using Intl.DateTimeFormat.prototype.formatToParts (see my formatToParts example). However, we might just create our own pipe for this purpose, relying on the EcmaScript Internationalization API.

<div class="month-header">
  <time class="month-header__caption"
        [dateTime]="month | date:'yyyy-MM'"
  >{{month | monthAndYear}}</time>
</div>
describe('MonthAndYearPipe', () => {
  const defaultLocaleId = 'en-US';
  let pipe: MonthAndYearPipe;

  beforeAll(() => {
    registerLocaleData(HungarianLocale);
    registerLocaleData(BritishLocale);
  });

  beforeEach(() => {
    pipe = new MonthAndYearPipe(defaultLocaleId);
  });

  it('should format month with default locale format', () => {
    const month = new Date(2019, Month.June);

    expect(pipe.transform(month)).toBe('June 2019');
  });

  it('should format month with provided locale format', () => {
    const month = new Date(2019, Month.June);
    const locale = 'hu';

    expect(pipe.transform(month, locale)).toBe('2019. június');
  });

  it('should return null if value is not a Date', () => {
    const month = {};

    expect(pipe.transform(month)).toBe(null);
  });
});
@Pipe({
  name: 'monthAndYear'
})
export class MonthAndYearPipe implements PipeTransform {
  private readonly monthAndYearFormatOptions = {
    year: 'numeric',
    month: 'long'
  };

  constructor(@Inject(LOCALE_ID) private localeId: string) {}

  transform(value: any, locale = this.localeId) {
    return isValidDate(value) ? value.toLocaleString(locale, monthAndYearFormatOptions) : null;
  }
}
// date.utils.ts
export function isValidDate(value?: any): value is Date {
  return value instanceof Date && typeof value.getTime === 'function' && !isNaN(value.getTime());
}

Displaying the days of the month

Below the caption, the days of the month (numbers) are going to appear in a new component called MonthComponent.

yarn ng g c month --project=calendar
export class MonthComponent {
  @Input() month!: Date;
}

Similarly to MonthHeaderComponent, MonthComponent is also going to receive the month from CalendarComponent. Each date in that month will be placed in an element with the class .month__date.

it('should display the days of the month received via input', () => {
  component.month = new Date(2019, Month.February);
  const daysInFebruary = 28;
  const rangeFrom1To28 = Array.from({length: daysInFebruary}, (_, index) => `${index + 1}`);

  fixture.detectChanges();

  expect(getDates()).toEqual(rangeFrom1To28);
});

function getDates() {
  return getDayDebugElements()
    .map(dayOfWeekDebugElement => dayOfWeekDebugElement.nativeElement.textContent);
}

function getDayDebugElements() {
  return fixture.debugElement.queryAll(By.css('.month__date'));
}

MonthComponent could produce an array of numbers and display them, however, as we will need Dates for further bindings and calculations, it is better to produce an array of dates and format them with a DatePipe to show only the day of month.

export class MonthComponent {
  @Input() month!: Date;
  daysOfMonth!: readonly Date[];
}
<div class="month">
  <time class="month__date"
        *ngFor="let dayOfMonth of daysOfMonth"
  >{{dayOfMonth | date:'d'}}</time>
</div>

The array of dates should be generated in the month input property's setter so that the array is only regenerated if the input changes. Note that we cannot put the date generation logic in a get accessor for daysOfMonth, as even with the OnPush change detection strategy, the dates would be re-rendered when any of the component's inputs changes (not only if month does) or any event binding fires or any async-piped Observable emits.

export class MonthComponent {
  daysOfMonth!: readonly Date[];

  private _month!: Date;

  @Input()
  set month(month: Date) {
    this._month = month;
    this.daysOfMonth = getDaysOfMonth(this._month);
  }
  get month() {
    return this._month;
  }
}

Array.from() (introduced in EcmaScript 2015) is an ideal candidate for creating an array with a specific number of items. It takes an array-like object (an object with a length property and/or indexed elements) or an iterable (e.g. Array, string, Set, Map) as its first parameter, and an optional function to map each value.

// date.utils.ts

export function getDaysOfMonth(month: Date) {
  return Array.from({length: numberOfDaysInMonth(month)}, (_, index) => setDate(month, index + 1));
}

export function numberOfDaysInMonth(month: Date) {
  return new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
}

export function setDate(date: Date, dayOfMonth: number) {
  const dateCopy = new Date(date);
  dateCopy.setDate(dayOfMonth);
  return dateCopy;
}

Date.prototype.setDate mutates the object it is called on, hence the introduction of a utility function with the same name. numberOfDaysInMonth relies on JavaScript's auto-correction of Dates when it retrieves the last day of the given month by getting the zeroeth day of the following month.

Placing each date in a time element with the full date in its datetime attribute provides good semantics and will aid end-to-end testing later. Users accessing the calendar with assistive technologies would also benefit from hearing/sensing the full date instead of a number when focusing the element.

it('should display the days with the proper datetime attributes', () => {
  component.month = new Date(2019, Month.February);
  const daysInFebruary = 28;
  const isoDatesInFebruary = Array.from(
    {length: daysInFebruary},
    (_, index) => `2019-02-${(index + 1).toString().padStart(2, '0')}`
  );

  fixture.detectChanges();

  expect(getDayDatetimeAttributes()).toEqual(isoDatesInFebruary);
});

it('should display the days with the proper ARIA labels', () => {
  component.month = new Date(2019, Month.February);
  const ariaLabelsInFebruary = getDaysOfMonth(component.month).map((date) => formatDate(date, 'fullDate', 'en-US'));
  fixture.detectChanges();

  expect(getDateAriaLabels()).toEqual(ariaLabelsInFebruary);
});

function getDayDateTimeProperties() {
  return getDayDebugElements().map(dayDebugElement => dayDebugElement.properties.dateTime);
}

function getDateAriaLabels() {
  return getDayDebugElements().map(dayDebugElement => dayDebugElement.attributes['aria-label']);
}
<div class="month">
  <time class="month__date"
        *ngFor="let dayOfMonth of daysOfMonth"
        [dateTime]="dayOfMonth | date:'yyyy-MM-dd'"
        [attr.aria-label]="dayOfMonth | date:'fullDate'"
  >{{dayOfMonth | date:'d'}}</time>
</div>
calendar with month unstyled

Traditional calendar layout: <table>

Tables have been widely applied for datepicker layout (the Angular Material Datepicker is also using one, and so is PrimeNg's calendar). The days of the week are usually placed in <th> elements, while days of the month in <td>s. This technique comes with great browser support and accessibility but also with extra calculations. For instance, the days of the months need to be split up to groups by weeks so that they can be placed in table rows (<tr>). Also, unless the first day of the month is a Sunday, a dummy <td> with a colspan attribute needs to be placed before the first <td> to push the first week's cells to the appropriate column.

CSS Grid Layout for calendars

With CSS Grid Layout, there is no need for nested arrays of dates or dummy spacer elements. The same styles that have been applied to .days-of-week can be applied to the container of day elements to place the days in 7 columns.

.month {
  display: grid;
  grid-template-columns: repeat($number-of-days-in-a-week, $calendar-cell-size);
  grid-template-rows: $calendar-cell-size;
}

Although there is only one row specified, the grid container adds implicit row tracks for further items that do not fit in the first 7 cells. The implicit rows can be sized with the grid-auto-rows property. Once that is set, we no longer need the grid-template-rows property.

.month {
  display: grid;
  grid-template-columns: repeat($number-of-days-in-a-week, $calendar-cell-size);
  grid-auto-rows: $calendar-cell-size;
}
calendar with dates in the wrong column

Placing the dates in the right column

Currently MonthComponent displays all the dates in a specified month. However, the dates have yet to be placed in the right column. February 1, 2019, for instance, is a Friday in reality, whereas in here it is in the Sunday column. It will be enough to change the column of the first grid item with CSS and the rest will follow it. In order to do that, we will place a modifier class on the grid container (e.g. .month--first-day-friday) which conveys what day the first day of the month is.

it('should have a class with the first day of month', () => {
  component.month = new Date(2019, Month.February, 17);

  fixture.detectChanges();

  expect(getMonthDebugElement().classes['month--first-day-friday']).toBe(true);
});

function getMonthDebugElement() {
  return fixture.debugElement.query(By.css('.month'));
}
import { DayOfWeek } from '@angular/common';
...
export class MonthComponent {
  daysOfMonth!: readonly Date[];
  firstDayOfMonth!: string;

  private _month!: Date;

  @Input()
  set month(month: Date) {
    this._month = month;
    this.daysOfMonth = getDaysOfMonth(this._month);
    this.firstDayOfMonth = DayOfWeek[this.daysOfMonth[0].getDay()].toLowerCase();
  }
  get month() {
    return this._month;
  }
}

Date.prototype.getDay() returns the day of the week the date is on as a number (Sunday = 0, Saturday = 6). We are calling that method on the first item in daysOfMonth and using the returned number as an index to retrieve the English name of the day from WeekDay, a numeric enum imported from @angular/common. TypeScript's numeric enums two-way-map strings to numbers.

<div class="month"
     [ngClass]="'month--first-day-' + firstDayOfMonth"
>
  <time class="month__date"
        *ngFor="let dayOfMonth of daysOfMonth"
        ...
  >{{dayOfMonth | date:'d'}}</time>
</div>

The starting lines which grid items are placed on can be modified with the grid-column-start (vertical line) and grid-row-start (horizontal line) properties. As grid lines are indexed from 1, for a Sunday date to go in the first column, the time element that is the first child of .month should get a grid-column-start of 1, every Tuesday date a grid-column-start of 2, and so on. Lists in Sass are also indexed from 1, thus it is convenient to store the names of the days in one and iterate over its items to create the modifier classes.

$days-of-week: (
  sunday,
  monday,
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday
);

.month {
  @each $day-of-week in $days-of-week {
    &--first-day-#{$day-of-week} {
      time:first-child {
        grid-column-start: index($days-of-week, $day-of-week);
      }
    }
  }
}

Let's examine why grid-column-start offsets all consecutive grid items in this example. By default, the grid auto-placement algorithm uses a row flow with sparse packing, i.e., it keeps the order of the elements and does not fill earlier empty cells. It is important to note that the grid item only specifies a grid-column-start and no grid-row-start, therefore the latter will default to auto. If grid-row-start were specified (e.g. 1), consecutive grid items would fill in the empty cells before the first element and thus the order of the elements would also change. The same would happen if grid-auto-flow, the property that controls the auto-placement algorithm, were set to dense (same as row dense) on the grid container.

calendar with days in the appropriate column

Monday start

In European calendars, the first day of the week is Monday. The calendar should therefore reorder its items based on the locale. As both DaysOfWeekComponent and MonthComponent will need to change their grid (and they may both have multiple instances in the same calendar), we will communicate the first day of week to their styles through a CSS class (e.g. .calendar--first-day-of-week-monday). This way we only need one class binding.

describe('CalendarComponent with British locale', () => {
  ...
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        CalendarComponent,
        MockComponent(DaysOfWeekComponent),
        MockComponent(MonthHeaderComponent),
        MockComponent(MonthComponent)
      ],
      providers: [
        {provide: LOCALE_ID, useValue: 'en-GB'}
      ]
    })
      .compileComponents();
  }));

  ...

  it('should have a class with the first day of week', () => {
    fixture.detectChanges();

    expect(getCalendarDebugElement().classes['days-of-week--first-day-monday']).toBe(true);
  });

  function getCalendarDebugElement() {
    return fixture.debugElement.query(By.css('.calendar'));
  }
});
<div class="calendar"
     [ngClass]="'calendar--first-day-of-week-' + (firstDayOfWeek | lowercase)"
>
  <lib-days-of-week></lib-days-of-week>
  <lib-month-header [month]="activeMonth"></lib-month-header>
  <lib-month [month]="activeMonth"></lib-month>
</div>

For retrieving the first day of week from Angular's locales, we can use getLocaleFirstDayOfWeek, which returns a WeekDay enum member (0 = 'Sunday', 1 = 'Monday').

import { getLocaleFirstDayOfWeek, WeekDay } from '@angular/common';

...

export class CalendarComponent implements OnInit {
  activeMonth = startOfMonth(new Date());
  firstDayOfWeek!: keyof typeof WeekDay; // 'Sunday' | 'Monday' | ...

  constructor(@Inject(LOCALE_ID) private localeId: string) {}

  ngOnInit() {
    this.firstDayOfWeek = WeekDay[getLocaleFirstDayOfWeek(this.locale)] as keyof typeof WeekDay;
  }
}

If .days-of-week is inside .calendar--first-day-of-week-monday, the grid items (abbr.days-of-week__day) should be reordered. Due to Angular's view encapsulation, the child selector .calendar--first-day-of-week-monday .days-of-week would not work. It can be subsituted with Angular's host-context selector: :host-context(.calendar--first-day-of-week-monday) .days-of-week. The modifier class will increase the placement order of the Sunday item to 1. This way, the first item is going to be placed in the grid after all other grid items with a default order of 0 have been placed. Thus, Sunday will be moved from the first column to the last one, while remaining the first element in its container. The order property will also work with a flex fallback for old browsers (see the repository for an example).

.days-of-week {
  :host-context(.calendar--first-day-of-week-monday) & {
    abbr:first-child {
      order: 1;
    }
  }
}

Changing the order is usually not optimal for users accessing the app with assistive technologies, as those rely on source order. However as our aria-label attributes for dates contain the full date with the day of week ({{dayOfWeek | date:'fullDate'}}), and as .days-of-week is not in the tab order, reordering the days may not cause huge issues.

For placing the first day of month in the right column we generated 7 classes (e.g. .month--first-day-tuesday) and set grid-column-start on their first child to indexes from a Sass list. Now we'll need twice as many selectors with host-context. For that, we will change our list to a string-to-list map.

$first-day-of-week-to-days-of-week: (
  sunday: (
    sunday,
    monday,
    tuesday,
    wednesday,
    thursday,
    friday,
    saturday
  ),
  monday: (
    monday,
    tuesday,
    wednesday,
    thursday,
    friday,
    saturday,
    sunday
  )
);

The following nested iteration is going to create 14 selectors, such as:

:host-context(.calendar-first-day-of-week-monday) .month--first-day-monday time:first-child {
  grid-column: 1;
}
.month {
  @each $first-day-of-week, $days-of-week in $first-day-of-week-to-days-of-week {
    @each $day-of-week in $days-of-week {
      :host-context(.calendar--first-day-of-week-#{$first-day-of-week}) &--first-day-#{$day-of-week} {
        time:first-child {
          grid-column: index($days-of-week, $day-of-week);
        }
      }
    }
  }
}
Calendar with a British locale

Picking a date

When .month__date is clicked inside MonthComponent, the component should signal to its parent CalendarComponent that a date has been selected using a custom event binding.

export class MonthComponent {
  ...
  @Output() selectedDateChange = new EventEmitter<Date>();
  ...
}
const valentinesDay = new Date(2019, Month.February, 14);

it('should emit the picked date', () => {
  component.month = new Date(2019, Month.February);
  fixture.detectChanges();
  spyOn(component.selectedDateChange, 'emit');

  clickDay(14);

  expect(component.selectedDateChange.emit).toHaveBeenCalledWith(valentinesDay);
});

function clickDay(dayOfMonth: number) {
  const dayDebugElement = getDayDebugElement(dayOfMonth);

  if (dayDebugElement) {
    dayDebugElement.nativeElement.click();
  } else {
    throw new Error(`day ${dayOfMonth} element not found`);
  }
}

function getDayDebugElement(dayOfMonth: number) {
  return fixture.debugElement.queryAll(By.css('.month__date'))
    .find(dayDebugElement => dayDebugElement.nativeElement.textContent === `${dayOfMonth}`);
}

Having a click binding on each .month__date would add too many event listeners, especially as there may be more than 12 MonthComponents in a scrollable calendar. To avoid that, we are going to use event delegation by attaching the click binding on the parent element of days (.month).

<div class="month"
     [ngClass]="'month--first-day-' + firstDayOfMonth"
     (click)="onMonthClick($event)"
>
  <time class="month__date"
        *ngFor="let dayOfMonth of daysOfMonth"
        [dateTime]="dayOfMonth | date:'yyyy-MM-dd'"
        [attr.aria-label]="dayOfMonth | date:'fullDate'"
  >{{dayOfMonth | date:'d'}}</time>
</div>

The downside of event delegation is the need to rely on data stored in the DOM, which can be modified easily in the inspector. The time elements store the dates in two locations: their textContent and dateTime properties. The date string in the datetime attribute is in a simplified ISO 8601 format (yyyy-MM-dd), which may be directly passed to the Date constructor.

export class MonthComponent {
  ...
  onMonthClick(event: MouseEvent) {
    this.selectedDateChange.emit(
      new Date((event.target as HTMLTimeElement).dateTime + 'T00:00')
    );
  }
  ...
}

A date-only ISO 8601 string passed to the Date constructor would produce a UTC date, not a local one. With the hours and minutes also provided, a local date will be constructed.

As there are empty cells in .month, it is possible to click on an area which is not a time.month__date. This case should be handled.

it('should NOT emit a picked date when what has been clicked is not a date', () => {
  component.month = new Date(2019, Month.February);
  fixture.detectChanges();
  spyOn(component.selectedDateChange, 'emit');

  clickOnMonthElement();

  expect(component.selectedDateChange.emit).not.toHaveBeenCalled();
});

function clickOnMonthElement() {
  getMonthDebugElement().triggerEventHandler('click', {target: getMonthDebugElement().nativeElement});
}

Checking if the click target is a .month__date eliminates the unwanted emissions on the output.

export class MonthComponent {
  ...
  private dateSelector = 'time.month__date';

  onMonthClick(event: MouseEvent) {
    const target = event.target as HTMLElement;

    if (this.isTimeElement(target)) {
      this.onDateClick(target);
    }
  }

  private onDateClick(timeElement: HTMLTimeElement) {
    const selectedDate = new Date(timeElement.dateTime + 'T00:00');

    if (isValidDate(selectedDate)) {
      this.selectedDateChange.emit(selectedDate);
    }
  }

  private isTimeElement(element: HTMLElement): element is HTMLTimeElement {
    return !!element && element.matches(this.dateSelector);
  }
  ...
}

Note that MonthComponent does not set any of its properties to the selected date. Instead, it is going to receive the selected date from its parent CalendarComponent, as there might be more than one MonthComponents, each emitting selected dates, and also because CalendarComponent might perform further validation on emitted dates. Thus, selectedDate needs to be passed back from CalendarComponent to its children MonthComponents.

export class MonthComponent {
  ...
  @Input() selectedDate?: Date;
  ...
}

The property holding the selected date in CalendarComponent will be called value, so that it resembles the interface of HTML input elements.

// calendar.component.spec.ts

it('should set value on MonthComponent selectedDateChange event', () => {
  component.activeMonth = new Date(2019, Month.February);
  selectDate(valentinesDay);
  fixture.detectChanges();

  expect(component.value).toBe(valentinesDay);
});

function selectDate(date: Date) {
  getMonthComponentDebugElement().triggerEventHandler('selectedDateChange', date);
}

function getMonthComponentDebugElement() {
  return fixture.debugElement.query(By.css('lib-month'));
}
export class CalendarComponent {
  ...
  value?: Date;

  onSelect(date: Date) {
    this.value = date;
  }
  ...
}
it('should bind selectedDate input of monthcomponent to value', () => {
  component.activeMonth = new Date(2019, Month.February);
  selectDate(valentinesDay);

  fixture.detectChanges();

  expect(getMonthComponentDebugElement().componentInstance.selectedDate).toBe(component.value);
});
<div class="calendar"
     [ngClass]="'calendar--first-day-of-week-' + (firstDayOfWeek | lowercase)"
>
  <lib-days-of-week></lib-days-of-week>
  <lib-month-header [month]="activeMonth"></lib-month-header>
  <lib-month [month]="activeMonth"
             [selectedDate]="value"
             (selectedDateChange)="onSelect($event)"
  ></lib-month>
</div>

CalendarComponent should also notify its consumer component when its value changes and also make its value settable.

it('should emit a valueChange on MonthComponent selectedDateChange event', () => {
  component.activeMonth = new Date(2019, Month.February);
  spyOn(component.change, 'emit');
  fixture.detectChanges();

  selectDate(valentinesDay);
  fixture.detectChanges();

  expect(component.change.emit).toHaveBeenCalledWith(valentinesDay);
});
export class CalendarComponent {
  ...
  @Input() value?: Date;
  @Output() valueChange = new EventEmitter<Date>();

  onSelect(date: Date) {
    this.value = date;
    this.valueChange.emit(this.value);
  }
  ...
}

Giving the output the name of the input with the Change suffix allows the consumer to two-way bind to the input using the "banana in the box" syntax.

<lib-calendar ([value])="departureDate"></lib-calendar>
<!-- equivalent to: -->
<lib-calendar [value]="departureDate" (valueChange)="departureDate = $event"></lib-calendar>

Highlighting the selected date

Once the user clicks on a date, the selected date should be highlighted in the calendar. To achieve that, the selected day element should receive a new modifier class .month__date--selected as well as an aria-selected="true" attribute.

it('should add --selected class to selected day element', () => {
  component.month = new Date(2019, Month.February);
  component.selectedDate = valentinesDay;
  fixture.detectChanges();

  expect(getDayDebugElement(14)!.classes['month__date--selected']).toBe(true);
});
<div class="month"
     [ngClass]="'month--first-day-' + firstDayOfMonth"
     (click)="onMonthClick($event)"
>
  <time class="month__date"
        *ngFor="let dayOfMonth of daysOfMonth"
        [dateTime]="dayOfMonth | date:'yyyy-MM-dd'"
        [attr.aria-label]="dayOfMonth | date:'fullDate'"
        [class.month__date--selected]="isSelected(dayOfMonth)"
        [attr.aria-selected]="isSelected(dayOfMonth)"
  >{{dayOfMonth | date:'d'}}</time>
</div>
export class MonthComponent {
  @Input() selectedDate?: Date;

  ...

  isSelected(dayOfMonth: Date) {
    return this.selectedDate && isSameDate(dayOfMonth, this.selectedDate);
  }
}

The most performant way to compare Dates in JavaScript is using Date.prototype.getTime to convert the Dates to numbers (number of milliseconds since 1970). Significantly slower alternatives are Date.prototype.valueOf and the + operator which relies on valueOf() (for a comparison run this jsperf test)

// date.utils.ts
export function isSameDate(date1: Date, date2: Date) {
  return date1.getTime() === date2.getTime();
}

With the modifier class we can highlight the selected date element in my favourite HTML colour.

.month__date {
  display: flex;
  justify-content: center;
  align-items: center;

  &--selected {
    background-color: chocolate;
    color: white;
  }
}
Calendar with a selected date

Disabling dates

Buying a ticket for yesterday or being born tomorrow are examples of date selection our calendar may not allow. Configuring which dates to disable could work through setting the minimum and maximum dates as inputs on CalendarComponent. We could borrow the names for the optional inputs from HTML input type="date" validation attributes.

export class MonthComponent {
  ...
  @Input() min?: Date;
  @Input() max?: Date;
  ...
}

This article will suffice implementing the minimum date configuration feature. MonthComponent should only emit a selectedDateChange event if a date equal to or later than the minimum date has been clicked.

it('should NOT pick disabled date', () => {
  component.month = new Date(2019, Month.February);
  component.min = new Date(2019, Month.February, 3);
  fixture.detectChanges();
  spyOn(component.selectedDateChange, 'emit');

  clickDay(2);

  expect(component.selectedDateChange.emit).not.toHaveBeenCalled();
});

it('should pick minimum date', () => {
  component.month = new Date(2019, Month.February);
  component.min = new Date(2019, Month.February, 3);
  fixture.detectChanges();
  spyOn(component.selectedDateChange, 'emit');

  clickDay(3);

  expect(component.selectedDateChange.emit).toHaveBeenCalledWith(component.min);
});

it('should pick day later than minimum date', () => {
  component.month = new Date(2019, Month.February);
  component.min = new Date(2019, Month.February, 3);
  fixture.detectChanges();
  spyOn(component.selectedDateChange, 'emit');

  clickDay(4);

  expect(component.selectedDateChange.emit).toHaveBeenCalledWith(new Date(2019, Month.February, 4));
});
export class MonthComponent {
  ...

  private onDateClick(dateElement: HTMLElement) {

    const selectedDate = new Date(timeElement.dateTime + 'T00:00');

    if (isValidDate(selectedDate)) {
      this.selectDate(selectedDate);
    }
  }

  private selectDate(date: Date) {
    if (!this.isDisabled(date)) {
      this.selectedDateChange.emit(date);
    }
  }

  isDisabled(dayOfMonth: Date) {
    return !!this.min && isDateAfter(this.min, dayOfMonth);
  }
}
// date.utils.ts
export function isDateAfter(date1: Date, date2: Date) {
  return date1.getTime() > date2.getTime();
}

Although the comparison operators <, >, <=, >= might also be used to compare dates in JavaScript, comparing the numbers returned by their getTime method is significantly faster (see this jsperf test revision I created).

Graying disabled dates

Not only should disabled dates be unselectable, but they should also indicate their disabled state visually. This message should also be conveyed to assistive technologies with an aria-disabled attribute.

it('should add --disabled class to a day earlier than minimum date', () => {
  component.month = new Date(2019, Month.February);
  component.min = new Date(2019, Month.February, 3);
  fixture.detectChanges();

  expect(getDayDebugElement(1)!.classes['month__date--disabled']).toBe(true);
  expect(getDayDebugElement(2)!.classes['month__date--disabled']).toBe(true);
  expect(getDayDebugElement(3)!.classes['month__date--disabled']).toBe(false);
});
<div class="month"
     [ngClass]="'month--first-day-' + firstDayOfMonth"
     (click)="onMonthClick($event)"
>
  <time class="month__date"
        *ngFor="let dayOfMonth of daysOfMonth"
        [dateTime]="dayOfMonth | date:'yyyy-MM-dd'"
        [attr.aria-label]="dayOfMonth | date:'fullDate'"
        [class.month__date--selected]="isSelected(dayOfMonth)"
        [attr.aria-selected]="isSelected(dayOfMonth)"
        [class.month__date--disabled]="isDisabled(dayOfMonth)"
        [attr.aria-disabled]="isDisabled(dayOfMonth)"
  >{{dayOfMonth | date:'d'}}</time>
</div>
.month__date--disabled {
  opacity: .38;
}

According to the material design guidelines, disabled text should have 38% opacity.

Transforming the calendar into a custom form control

Currently, the selected date can be retrieved from CalendarComponent in two ways: using the value property on a template reference variable on the component tag or by binding to the component's custom valueChange event.

<form>
  <lib-calendar #myCalendar (valueChange)="onDateChange($event)"></lib-calendar>
  <button type="button" (click)="onSubmit(myCalendar.value)"></button>
</form>

This approach may be sufficient in small forms. Forms with complex initialisation or validation logic and asynchronous data sources had better rely on ReactiveFormsModule.

import { ReactiveFormsModule } from '@angular/forms';

The calendar should therefore support being used in a reactive form, that is, it should be controllable by the FormControl it is connected to with the formControl or formControlName directives. The following example form will also serve as the wrapper component used in unit tests.

const defaultDate = new Date(2019, Month.February, 10);

@Component({
  template: `
    <lib-calendar [min]="min"
                  [formControl]="dateControl"
    ></lib-calendar>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
class CalendarWrapperComponent {
  min?: Date;
  dateControl = new FormControl(defaultDate);

  // for testing purposes
  @ViewChild(CalendarComponent) calendarComponent!: CalendarComponent;
}

At this point, using the formControl directive on the calendar as above would result in an error.


Error: No value accessor for form control with unspecified name attribute

In order for that error to disappear, CalendarComponent must do 2 things: register itself with the NG_VALUE_ACCESSOR token and implement the ControlValueAccessor interface.

import { forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'lib-calendar',
  templateUrl: './calendar.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CalendarComponent),
      multi: true
    }
  ]
})
export class CalendarComponent implements ControlValueAccessor {

  ...

  writeValue() {}

  registerOnChange() {}

  registerOnTouched() {}

  ...
}

The writeValue method will be called when the FormControl's value is changed programatically (e.g with setValue or reset). When the date control's value is set, the calendar should update its view.

const valentinesDay = new Date(2019, Month.February, 14);

it('should set selectedDate when form control value is set', () => {
  wrapperComponent.dateControl.setValue(valentinesDay);

  expect(wrapperComponent.calendarComponent.value).toBe(valentinesDay);
});
export class CalendarComponent implements ControlValueAccessor {
  ...
  writeValue(value: Date) {
    this.value = value;
    this.changeDetectorRef.markForCheck();
  }
  ...
}

As the component is using the OnPush change detection strategy, the component property change is not detected - only changes in component inputs, event bindings or async pipes would be. For that reason, the component needs to be marked for check.

registerOnChange will be called with a callback function when the FormControl is initialised. CalendarComponent should save that callback to a property and call it when the user picks a date.

it('should set form control value on selectedDateChange', () => {
  getMonthComponentDebugElement().triggerEventHandler('selectedDateChange', valentinesDay);

  expect(wrapperComponent.dateControl.value).toBe(valentinesDay);
});
export class CalendarComponent implements ControlValueAccessor {
  ...
  private onChange!: (updatedValue: Date) => void;

  onSelect(date: Date) {
    this.writeValue(date);
	this.onChange(date);
  }

  registerOnChange(onChangeCallback: (updatedValue: Date) => void) {
    this.onChange = onChangeCallback;
  }
  ...
}

registerOnTouched will be called with a similar callback that can be used to mark the control as touched.

it('should set form control to touched on selectedDateChange', () => {
  expect(wrapperComponent.dateControl.touched).toBe(false);

  selectDate(valentinesDay);

  expect(wrapperComponent.dateControl.touched).toBe(true);
});
export class CalendarComponent implements ControlValueAccessor {
  ...
  private onTouched?: () => void;

  onSelect(date: Date) {
    this.writeValue(date);

    if (this.onChange) {
      this.onChange(date);
    }
    if (this.onTouched) {
      this.onTouched();
    }
  }

  registerOnTouched(onTouchedCallback: () => void) {
    this.onTouched = onTouchedCallback;
  }
  ...
}

The touched state might also be saved to a property of the component in case it is needed later.

it('should set touched property to true on selectedDateChange', () => {
  expect(wrapperComponent.calendarComponent.touched).toBe(false);

  selectDate(valentinesDay);

  expect(wrapperComponent.calendarComponent.touched).toBe(true);
});
export class CalendarComponent implements ControlValueAccessor {
  ...
  touched = false;

  registerOnTouched(onTouchedCallback: () => void): void {
    this.onTouched = () => {
      this.touched = true;
      onTouchedCallback();
    };
  }
  ...
}

Lastly, the optional setDisabledState method is called when the control is disabled / enabled programatically.

it('should set disabled when form control is set to disabled', () => {
  expect(wrapperComponent.calendarComponent.disabled).toBe(false);

  wrapperComponent.dateControl.disable();

  expect(wrapperComponent.calendarComponent.disabled).toBe(true);
});
export class CalendarComponent implements ControlValueAccessor {
  ...
  disabled = false;

  setDisabledState(isDisabled: boolean) {
    this.disabled = isDisabled;
    this.changeDetectorRef.markForCheck();
  }
  ...
}

The calendar should not allow picking a date when the whole form control is disabled.

it('should not pick date when disabled', () => {
  wrapperComponent.dateControl.disable();

  selectDate(valentinesDay);

  expect(wrapperComponent.calendarComponent.selectedDate).not.toBe(valentinesDay);
  expect(wrapperComponent.dateControl.value).not.toBe(valentinesDay);
  expect(wrapperComponent.dateControl.touched).toBe(false);
});
export class CalendarComponent implements ControlValueAccessor {
  ...
  onSelect(date: Date) {
    if (!this.disabled) {
      this.writeValue(date);

      if (this.onChange) {
        this.onChange(date);
      }
      if (this.onTouched) {
        this.onTouched();
      }
    }
  }
  ...
}

Although the above examples are for reactive forms, having implemented the ControlValueAccessor also means CalendarComponent can be used with the NgModel directive in a template-driven form. NgModel provides a higher level of abstraction by creating the FormControl instance based on the template.

<form #flightSearchForm="ngForm"
      (ngSubmit)="onSubmit(flightSearchForm.value)"
      novalidate
>
  <lib-calendar ngModel
                name="departureDate"
                required
                min="today"
  ></lib-calendar>
  <button type="submit">Submit</button>
</form>

Note that the additional abstraction of NgModel leads to less testable forms that do not play that well with OnPush change detection unless you create two-way bindings.

Stepping months

At this point, one can only pick a date from the current month. It should be possible to switch to the next / previous month with the click of a button. The month stepper buttons are usually located in the calendar's header, somewhere close to a caption showing the name of the month. In our case they will be placed on the two sides of the month caption in MonthHeaderComponent.

<div class="month-header">
  <button class="month-header__stepper month-header__stepper--previous"
          type="button"
          aria-label="Previous month"
  ></button>
  <time class="month-header__caption"
        [dateTime]="month | date:'yyyy-MM'"
  >{{month | monthAndYear}}</time>
  <button class="month-header__stepper month-header__stepper--next"
          type="button"
          aria-label="Next month"
  ></button>
</div>

We have just used two arrow emojis as button text which might be replaced with some inline SVG in a real application. If we didn't also give aria-labels to the buttons, screen readers would read out the names of the emojis inside them, e.g. arrow right.

TODO: check emoji name

MonthHeaderComponent will receive the active month as an input from CalendarComponent, and emit updates on an output when it changes. The active month could differ from the displayed month in multi-month view, hence the introduction of a new input. It is, for instance, common practice to show two months next to each other in desktop view so that it requires one less click to pick a date from next month, which is especially likely towards the end of the month.

export class MonthHeaderComponent {
  ...
  @Input() activeMonth!: Date;
  @Output() activeMonthChange = new EventEmitter<Date>();
  ...
}
it('should emit activeMonthChange on next month click', () => {
  component.activeMonth = new Date(2019, Month.July);
  spyOn(component.activeMonthChange, 'emit');
  fixture.detectChanges();

  stepMonth('next');

  expect(component.activeMonthChange.emit).toHaveBeenCalledWith(new Date(2019, Month.August));
});

it('should emit activeMonthChange on previous month click', () => {
  component.activeMonth = new Date(2019, Month.July);
  spyOn(component.activeMonthChange, 'emit');
  fixture.detectChanges();

  stepMonth('previous');

  expect(component.activeMonthChange.emit).toHaveBeenCalledWith(new Date(2019, Month.June));
});

function clickMonthStepperButton(button: 'previous' | 'next') {
  return getMonthStepperButton(button).triggerEventHandler('click', null);
}

function getMonthStepperButton(button: 'previous' | 'next') {
  return fixture.debugElement.query(By.css(`.month-header__stepper--${monthStep}`));
}

MonthHeaderComponent will add a month or subtract one from activeMonth and emit the new Date.

export type MonthStepDelta = -1 | 1;

export class MonthHeaderComponent {
  ...
  stepMonth(delta: MonthStepDelta) {
    const activeMonth = addMonths(this.activeMonth || new Date(), delta);
    this.activeMonthChange.emit(activeMonth);
  }
  ...
}
// date.utils.ts
export function addMonths(date: Date, months: number) {
  return setMonth(date, date.getMonth() + months);
}

export function setMonth(date: Date, month: number) {
  const dateCopy = new Date(date);
  dateCopy.setMonth(month);
  return dateCopy;
}
<div class="month-header">
  <button class="month-header__stepper month-header__stepper--previous"
          (click)="stepMonth(-1)"
          type="button"
          aria-label="Previous month"
  ></button>
  <time class="month-header__caption"
        [dateTime]="month | date:'yyyy-MM'"
  >{{month | monthAndYear}}</time>
  <button class="month-header__stepper month-header__stepper--next"
          (click)="stepMonth(1)"
          type="button"
          aria-label="Next month"
  ></button>
</div>

In reaction to the activeMonthChange event, CalendarComponent will update its activeMonth property, which is also bound to the month input of both MonthComponent and MonthHeaderComponent.

it('should display the emitted month in one-month view', () => {
  component.activeMonth = new Date(2019, Month.July);
  fixture.detectChanges();

  triggerActiveMonthChange(new Date(2019, Month.August));
  fixture.detectChanges();

  expect(getVisibleMonth()).toEqual(new Date(2019, Month.August));
  expect(getVisibleMonthInHeader()).toEqual(new Date(2019, Month.August));
});

function getVisibleMonth() {
  return getMonthComponentDebugElement().componentInstance.month;
}

function getVisibleMonthInHeader() {
  return getMonthHeaderComponentDebugElement().componentInstance.month;
}

function triggerActiveMonthChange(activeMonth: Date) {
  getMonthHeaderComponentDebugElement().triggerEventHandler('activeMonthChange', activeMonth);
}
<div class="calendar"
     [ngClass]="'calendar--first-day-of-week-' + (firstDayOfWeek | lowercase)"
>
  <lib-days-of-week></lib-days-of-week>
  <lib-month-header [month]="activeMonth"
                    [(activeMonth)]="activeMonth"
  ></lib-month-header>
  <lib-month [month]="activeMonth"
             [selectedDate]="value"
             (selectedDateChange)="onSelect($event)"
  ></lib-month>
</div>

When the month caption is updated due to a reference change in MonthHeaderComponent's month input, the new content of the month caption should be announced to screen readers. We may use the aria-live attribute or the cdkAriaLive directive from @angular/cdk to achieve that. The latter supports more browsers and screen readers.

import { A11yModule } from '@angular/cdk/a11y';

...

@NgModule({
  declarations: [
    CalendarComponent,
    MonthComponent,
    DaysOfWeekComponent,
    MonthHeaderComponent,
    MonthAndYearPipe,
  ],
  imports: [
    CommonModule,
    A11yModule,
  ],
  exports: [CalendarComponent]
})
export class CalendarModule {}
it('should announce month change politely', () => {
  fixture.detectChanges();

  expect(getCaptionDebugElement().attributes.cdkAriaLive).toBe('polite');
});
<div class="month-header">
  <button class="month-header__stepper month-header__stepper--previous"
          (click)="stepMonth(-1)"
          type="button"
          aria-label="Previous month"
  ></button>
  <time class="month-header__caption"
        [dateTime]="month | date:'yyyy-MM'"
        cdkAriaLive="polite"
  >{{month | monthAndYear}}</time>
  <button class="month-header__stepper month-header__stepper--next"
          (click)="stepMonth(1)"
          type="button"
          aria-label="Next month"
  ></button>
</div>

Keyboard navigation

Making the calendar navigable by keyboard is key to accessibility. Users with impaired vision rely on the TAB key to move focus to the next form control, button, link or other focusable element in the DOM. SHIFT+TAB moves focus to the previous focusable element. Once the focus has landed on a widget with multiple options, it becomes the target of keydown events and can move the focus within itself on arrow key presses and select an option on ENTER or SPACE. There are two major techniques for moving focus within a widget, one handles a virtual focus with the aria-activedescendant attribute, the other handles real focus with the tabindex attribute. We are going to use the latter technique, which is referred to as the roving tabindex.

Roving tabindex

There is a roving tabindex implementation for angular called FocusKeyManager in @angular/cdk/a11y. FocusKeyManager stores the active element's index and handles arrow key presses forwarded to it by incrementing that index and focusing the corresponding element. As a calendar may display multiple months, it is more effective to handle focus based on the active date rather than its index. This will require a custom roving tabindex implementation.

export class MonthComponent {
  ...
  @Input() activeDate!: Date;
  ...
}

The roving tabindex technique means the element containing the active option will receive a tabindex of 0, while the tabindex of all other option elements will be set to -1. tabindex="0" means the element is focusable by keyboard (with the TAB key). An element with a negative tabindex is only focusable programatically, e.g. using the element's focus() method. Having the active item in the tab order ensures that the focus goes back to the same item after tabbing out of the list and tabbing back to it.

it('should make all dates focusable programatically', () => {
  component.month = new Date(2019, Month.August);
  const daysInAugust = 31;
  fixture.detectChanges();

  expect(getDayTabIndexes()).toEqual(new Array(daysInAugust).fill(-1));
});

it('should put the active date in the tab order', () => {
  component.month = new Date(2019, Month.August);
  component.activeDate = new Date(2019, Month.August, 1);
  const daysInAugust = 31;
  fixture.detectChanges();

  expect(getDayTabIndexes()).toEqual([0].concat(new Array(daysInAugust - 1).fill(-1)));
});

function getDayTabIndexes() {
  return getDayDebugElements().map(dayDebugElement => dayDebugElement.properties.tabIndex);
}
<div class="month"
     [ngClass]="'month--first-day-' + firstDayOfMonth"
     (click)="onMonthClick($event)"
>
  <time class="month__date"
        *ngFor="let dayOfMonth of daysOfMonth"
        [dateTime]="dayOfMonth | date:'yyyy-MM-dd'"
        [attr.aria-label]="dayOfMonth | date:'fullDate'"
        [class.month__date--selected]="isSelected(dayOfMonth)"
        [attr.aria-selected]="isSelected(dayOfMonth)"
        [class.month__date--disabled]="isDisabled(dayOfMonth)"
        [attr.aria-disabled]="isDisabled(dayOfMonth)"
		[class.month__date--active]="isActive(dayOfMonth)"
        [tabIndex]="isActive(dayOfMonth) ? 0 : -1"
  >{{dayOfMonth | date:'d'}}</time>
</div>
export class MonthComponent {
  ...
  @Input() activeDate!: Date;

  isActive(dayOfMonth: Date) {
    return !!this.activeDate && isSameDate(dayOfMonth, this.activeDate);
  }
  ...
}

...

Submitting the form

toISODate...

Conclusion

We have built a localised, accessible calendar with minimal styling and an input for specifying the minimum date. This is meant as a starting point that covers the basic concepts. The calendar we have build might be sufficient for certain types of applications while others would require further features. For instance, a travel app would benefit from a range picking feature, while the registration form of a social app would require a year picker / dropdown / stepper for picking the date of birth.

We may have learnt some new things about working with Dates in JavaScript. We formatted the dates with Angular's DatePipe and where we reached its limits we turned to the EcmaScript Internationalization API.

For the layout, we relied on the implicit grid and the grid auto-placement. You can find a flexbox fallback in the repository for old browsers with no support for CSS Grid.

We implemented the ControlValueAccessor and made the calendar into a custom form control for use in reactive forms.

Accessibility