'use strict'; import DSComponent from '../../base/component/component'; import elementIdModifier from '../../base/tools/id-modifier/id-modifier'; type CalendarDayArgs = { button: HTMLButtonElement click: (event: MouseEvent) => void date: Date init: () => void isDisabled?: boolean isHidden?: boolean keyPress: (event: KeyboardEvent) => void update: (day: Date, isHidden?: boolean, isDisabled?: boolean) => void } type DatePickerOptionsArgs = { dateSelectCallback?: (date: Date) => void disabledDates?: Date[] maxDate?: Date minDate?: Date } /** * Date picker component * * @class DSDatePicker * @extends DSComponent * @property {HTMLElement} datePickerParent - the date picker parent element * @property {HTMLButtonElement} calendarButtonElement - the calendar button element * @property {HTMLInputElement} dateInput - the date input element * @property {HTMLElement} dialogElement - the date picker dialog element * @property {HTMLElement} dialogTitleElement - the date picker dialog title element * @property {HTMLButtonElement} firstButtonInDialog - the first button in the date picker dialog * @property {HTMLInputElement} inputElement - the main input element * @property {HTMLButtonElement} lastButtonInDialog - the last button in the date picker dialog * @property {HTMLInputElement} monthInput - the month input element * @property {HTMLInputElement} yearInput - the year input element * @property {boolean} isMultipleInput - whether the date picker uses multiple input fields * @property {function} dateSelectCallback - callback function to be called when a date is selected * @property {Date} currentDate - the currently selected date * @property {Date[]} disabledDates - array of disabled dates * @property {Date} inputDate - the date currently in the input field * @property {Date} maxDate - the maximum selectable date * @property {Date} minDate - the minimum selectable date * @property {CalendarDayArgs[]} calendarDays - array of calendar day objects * @property {string[]} dayLabels - array of day labels * @property {string[]} monthLabels - array of month labels * @property {object} icons - object containing SVG icon templates */ class DSDatePicker extends DSComponent { private options!: DatePickerOptionsArgs; private calendarButtonElement!: HTMLButtonElement; private dateInput!: HTMLInputElement; private datePickerParent!: HTMLElement; private dialogElement!: HTMLElement; private dialogTitleElement!: HTMLElement; private firstButtonInDialog!: HTMLButtonElement; private inputElement!: HTMLInputElement; private lastButtonInDialog!: HTMLButtonElement; private monthInput!: HTMLInputElement; private yearInput!: HTMLInputElement; private isMultipleInput!: boolean; private currentDate!: Date; private inputDate?: Date; private calendarDays!: CalendarDayArgs[]; private dayLabels = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; private monthLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; private icons = { calendar_today: '', chevron_left: '', chevron_right: '', double_chevron_left: '', double_chevron_right: '', } /** * Creates a date picker component * * @param {HTMLElement} el - the date picker element * @param {object} options - configuration options for the date picker */ constructor(el: HTMLElement, options: DatePickerOptionsArgs = {}) { super(el); if (!el) return; this.datePickerParent = el; this.options = Object.assign({ disabledDates: [] }, options); this.inputElement = this.datePickerParent.querySelector('input') as HTMLInputElement; this.isMultipleInput = el.classList.contains('ds_datepicker--multiple'); this.dateInput = el.querySelector('.js-datepicker-date') as HTMLInputElement; this.monthInput = el.querySelector('.js-datepicker-month') as HTMLInputElement; this.yearInput = el.querySelector('.js-datepicker-year') as HTMLInputElement; this.currentDate = new Date(); this.currentDate.setHours(0, 0, 0, 0); this.calendarDays = []; } /** * Initialise the date picker * - inserts button and dialog into the DOM * - sets up event listeners * - populates the calendar with initial dates * * @returns {void} */ init(): void { if (!this.inputElement || this.isInitialised) { return; } this.setOptions(); this.setMinAndMaxDatesOnCalendar(); // insert calendar button const calendarButtonTempContainer = document.createElement('div'); calendarButtonTempContainer.innerHTML = this.buttonTemplate(); this.calendarButtonElement = calendarButtonTempContainer.firstChild as HTMLButtonElement; this.calendarButtonElement.setAttribute('data-button', `datepicker-${this.inputElement.id}-toggle`); if (this.isMultipleInput) { this.inputElement.parentElement?.parentElement?.appendChild(this.calendarButtonElement); } else { this.inputElement.parentElement?.appendChild(this.calendarButtonElement); this.inputElement.parentElement?.classList.add('ds_input__wrapper--has-icon'); } // insert dialog template this.dialogElement = document.createElement('div'); this.dialogElement.id = 'datepicker-' + elementIdModifier(); this.dialogElement.setAttribute('class', 'ds_datepicker__dialog datepickerDialog'); this.dialogElement.setAttribute('role', 'dialog'); this.dialogElement.setAttribute('aria-modal', 'true'); this.dialogElement.innerHTML = this.dialogTemplate(this.dialogElement.id); this.calendarButtonElement.setAttribute('aria-controls', this.dialogElement.id); this.calendarButtonElement.setAttribute('aria-expanded', false.toString()); this.datePickerParent.appendChild(this.dialogElement); this.dialogTitleElement = this.dialogElement.querySelector('.js-datepicker-month-year') as HTMLElement; // create calendar const tbody = this.datePickerParent.querySelector('tbody') as HTMLTableSectionElement; for (let i = 0; i < 6; i++) { // create row const row = tbody.insertRow(i); for (let j = 0; j < 7; j++) { // create cell (day) const cell = document.createElement('td'); const dateButton = document.createElement('button'); dateButton.type = 'button'; dateButton.dataset.form = 'date-select'; cell.appendChild(dateButton); row.appendChild(cell); const calendarDay = new DSCalendarDay(dateButton, this) as CalendarDayArgs; calendarDay.init(); this.calendarDays.push(calendarDay); } } // add event listeners const prevMonthButton = this.dialogElement.querySelector('.js-datepicker-prev-month') as HTMLButtonElement; const prevYearButton = this.dialogElement.querySelector('.js-datepicker-prev-year') as HTMLButtonElement; const nextMonthButton = this.dialogElement.querySelector('.js-datepicker-next-month') as HTMLButtonElement; const nextYearButton = this.dialogElement.querySelector('.js-datepicker-next-year') as HTMLButtonElement; prevMonthButton.addEventListener('click', (event) => this.focusPreviousMonth(event, false)); prevYearButton.addEventListener('click', (event) => this.focusPreviousYear(event, false)); nextMonthButton.addEventListener('click', (event) => this.focusNextMonth(event, false)); nextYearButton.addEventListener('click', (event) => this.focusNextYear(event, false)); const dateInputFields = [this.inputElement, this.dateInput, this.monthInput, this.yearInput]; dateInputFields.forEach(input => { if (input) { input.addEventListener('blur', () => { (this.calendarButtonElement.querySelector('span') as HTMLSpanElement).textContent = 'Choose date'; }); } }); const cancelButton = this.dialogElement.querySelector('.js-datepicker-cancel') as HTMLButtonElement; const okButton = this.dialogElement.querySelector('.js-datepicker-ok') as HTMLButtonElement; cancelButton.addEventListener('click', (event) => { event.preventDefault(); this.closeDialog(); }); okButton.addEventListener('click', () => this.selectDate(this.currentDate)); const dialogButtons = this.dialogElement.querySelectorAll('button:not([disabled="true"])'); this.firstButtonInDialog = dialogButtons[0] as HTMLButtonElement; this.lastButtonInDialog = dialogButtons[dialogButtons.length - 1] as HTMLButtonElement; this.firstButtonInDialog.addEventListener('keydown', (event) => this.firstButtonKeyup(event)); this.lastButtonInDialog.addEventListener('keydown', (event) => this.lastButtonKeyup(event)); this.calendarButtonElement.addEventListener('click', (event) => this.toggleDialog(event)); document.body.addEventListener('mouseup', (event) => this.backgroundClick(event)); // populates calendar with inital dates, avoids Wave errors about null buttons this.updateCalendar(); this.isInitialised = true; } /** * Adds months to a date * * @param {Date} date - the date to add months to * @param {number} months - number of months to add (negative to subtract) * @returns {Date} - the new date after adding months */ private addMonths(date: Date, months: number): Date { const tempDate = date.getDate(); date.setMonth(date.getMonth() + +months); if (date.getDate() !== tempDate) { date.setDate(0); } return date; } /** * Date picker button template * * @returns {string} - HTML template for the date picker button */ private buttonTemplate(): string { return ` `; } /** * Date picker dialog template * * @param {string} id * @returns {string} - HTML template for the date picker dialog */ private dialogTemplate(id: string): string { return `

June 2020

You can use the cursor keys to select a date
Sunday Monday Tuesday Wednesday Thursday Friday Saturday
`; } /** * Formats a number with leading zeroes * * @param {number} value - value to format * @param {number} length - desired length of output string * @returns {string} - formatted string */ private leadingZeroes(value: number, length: number = 2): string { let ret = value.toString(); while (ret.length < length) { ret = '0' + ret.toString(); } return ret; } /** * Handle clicks outside the date picker dialog * - closes the dialog if open and the click is outside the dialog * * @param {MouseEvent} event * @returns {void} */ private backgroundClick(event: MouseEvent): void { const target = event.target as Node; if (this.isOpen() && !this.dialogElement.contains(target) && !this.inputElement.contains(target) && !this.calendarButtonElement.contains(target)) { event.preventDefault(); this.closeDialog(); } } /** * Close the date picker dialog * - sets aria-expanded to false on the calendar button * - focuses the calendar button * * @returns {void} */ private closeDialog(): void { this.dialogElement.classList.remove('ds_datepicker__dialog--open'); this.calendarButtonElement.setAttribute('aria-expanded', false.toString()); this.calendarButtonElement.focus(); } /** * Handles the keyup event on the first button in the dialog * - focuses the first button in the dialog if the Tab and Shift keys are pressed * * @param {KeyboardEvent} event * @returns {void} */ private firstButtonKeyup(event: KeyboardEvent): void { if (event.key === 'Tab' && event.shiftKey) { this.lastButtonInDialog.focus(); event.preventDefault(); } } /** * Focuses the next day in the calendar * * @param {Date} date * @returns {void} */ focusNextDay(date: Date = new Date(this.currentDate)): void { date.setDate(date.getDate() + 1); this.goToDate(date); } /** * Focuses the previous day in the calendar * * @param {Date} date * @returns {void} */ focusPreviousDay(date: Date = new Date(this.currentDate)): void { date.setDate(date.getDate() - 1); this.goToDate(date); } /** * Focuses the next week in the calendar * * @param {Date} date * @returns {void} */ focusNextWeek(date: Date = new Date(this.currentDate)): void { date.setDate(date.getDate() + 7); this.goToDate(date); } /** * Focuses the previous week in the calendar * * @param {Date} date * @returns {void} */ focusPreviousWeek(date: Date = new Date(this.currentDate)): void { date.setDate(date.getDate() - 7); this.goToDate(date); } /** * Focuses the first day of the week in the calendar * * @returns {void} */ focusFirstDayOfWeek(): void { const date = new Date(this.currentDate); date.setDate(date.getDate() - date.getDay()); this.goToDate(date); } /** * Focuses the last day of the week in the calendar * * @returns {void} */ focusLastDayOfWeek(): void { const date = new Date(this.currentDate); date.setDate(date.getDate() - date.getDay() + 6); this.goToDate(date); } /** * Focuses the next month in the calendar * * @param {Event} event * @param {boolean} focus * @returns {void} */ focusNextMonth(event: Event, focus: boolean = true): void { event.preventDefault(); const date = new Date(this.currentDate); this.addMonths(date, 1); this.goToDate(date, focus); } /** * Focuses the previous month in the calendar * * @param {Event} event * @param {boolean} focus * @returns {void} */ focusPreviousMonth (event: Event, focus: boolean = true): void { event.preventDefault(); const date = new Date(this.currentDate); this.addMonths(date, -1); this.goToDate(date, focus); } /** * Focuses the next year in the calendar * * @param {Event} event * @param {boolean} focus * @returns {void} */ focusNextYear (event: Event, focus: boolean = true): void { event.preventDefault(); const date = new Date(this.currentDate); date.setFullYear(date.getFullYear() + 1); this.goToDate(date, focus); } /** * Focuses the previous year in the calendar * * @param {Event} event * @param {boolean} focus * @returns {void} */ focusPreviousYear (event: Event, focus: boolean = true): void { event.preventDefault(); const date = new Date(this.currentDate); date.setFullYear(date.getFullYear() - 1); this.goToDate(date, focus); } /** * Formats a date string into a Date object * - according to the date format set on the date picker parent element * - falls back to the provided fallback date if formatting fails * * @param {string} dateString - The date string to format * @param {Date | null} fallback - The fallback date if formatting fails * @returns {Date} - The formatted date */ private formattedDateFromString(dateString: string, fallback: Date | null = new Date()): Date | null { let formattedDate = null; const parts = dateString.split('/'); if (dateString.match(/\d{1,4}\/\d{1,2}\/\d{1,4}/)) { switch (this.datePickerParent.dataset.dateformat) { case 'YMD': formattedDate = new Date(`${parts[1]}/${parts[2]}/${parts[0]}`); break; case 'MDY': formattedDate = new Date(`${parts[0]}/${parts[1]}/${parts[2]}`); break; case 'DMY': default: formattedDate = new Date(`${parts[1]}/${parts[0]}/${parts[2]}`); break; } } if (formattedDate instanceof Date && !isNaN(formattedDate.getTime())) { return formattedDate; } else { return fallback; } } /** * Formats a date in a human-readable format * * @param {Date} date - The date to format * @returns {string} - The formatted date */ formattedDateHuman(date: Date): string { return `${this.dayLabels[date.getDay()]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`; } /** * Go to a specific date in the calendar * * @param {Date} date - The date to go to * @param {boolean} focus - Whether to focus the date in the calendar * @returns {void} */ goToDate(date: Date, focus?: boolean): void { const current = this.currentDate; this.currentDate = date; if (current.getMonth() !== this.currentDate.getMonth() || current.getFullYear() !== this.currentDate.getFullYear()) { this.updateCalendar(); } this.setCurrentDate(focus); } /** * Check whether a date is disabled * - Checks if the date is before minDate or after maxDate * - Checks if the date is in the disabledDates array * * @param {Date} date - The date to check * @returns {boolean} - whether the date is disabled */ private isDisabledDate(date: Date): boolean { let disabled = false; if (this.options.minDate && this.options.minDate > date) { disabled = true; } if (this.options.maxDate && this.options.maxDate < date) { disabled = true; } for (const disabledDate of this.options.disabledDates as Date[]) { if (date.toDateString() === disabledDate.toDateString()) { disabled = true; } } return disabled; } /** * Checks whether the date picker dialog is open * * @returns {boolean} - whether the dialog is open */ private isOpen(): boolean { return this.dialogElement.classList.contains('ds_datepicker__dialog--open'); } /** * Handles the keyup event on the last button in the dialog * - focuses the first button in the dialog if the Tab key is pressed * * @param {KeyboardEvent} event * @returns {void} */ private lastButtonKeyup(event: KeyboardEvent): void { if (event.key === 'Tab' && !event.shiftKey) { this.firstButtonInDialog.focus(); event.preventDefault(); } } /** * Opens the date picker dialog * - displays the dialog * - positions the dialog * - gets the date from the input element(s) * - updates the calendar * - sets the current date * * @returns {void} */ private openDialog(): void { // display the dialog this.dialogElement.classList.add('ds_datepicker__dialog--open'); this.calendarButtonElement.setAttribute('aria-expanded', true.toString()); // position the dialog let leftOffset: number; // get the date from the input element(s) let dateAsString: string; if (this.isMultipleInput) { // leftOffset in multiples of the 8px spacing leftOffset = this.calendarButtonElement.offsetLeft + this.calendarButtonElement.offsetWidth + 16; dateAsString = `${this.dateInput.value}/${this.monthInput.value}/${this.yearInput.value}`; } else { // leftOffset in multiples of the 8px spacing leftOffset = this.inputElement.offsetWidth + 16; dateAsString = this.inputElement.value; } const dialogElementSpacingUnits = Math.ceil(leftOffset / 8); this.dialogElement.classList.forEach(className => { if (className.match(/ds_!_off-l-/)) { this.dialogElement.classList.remove(className); } }); this.dialogElement.classList.add(`ds_!_off-l-${dialogElementSpacingUnits}`); if (dateAsString.match(/^\d{1,2}\/\d{1,2}\/\d{4}$/)) { this.inputDate = this.formattedDateFromString(dateAsString) as Date; this.currentDate = this.inputDate; } this.updateCalendar(); this.setCurrentDate(); } /** * Selects a date from the calendar * - Updates the calendar button text * - Sets the date in the input field(s) * - Dispatches a change event on the input element * - Calls the dateSelectCallback if provided * - Closes the dialog * * @param {Date} date The date to select * @returns {void | false} */ selectDate(date: Date): void | false { if (this.isDisabledDate(date)) { return false; } (this.calendarButtonElement.querySelector('span') as HTMLSpanElement).textContent = `Choose date. Selected date is ${this.formattedDateHuman(date)}`; this.setDate(date); const changeEvent = new Event('change'); // todo: true true (bubbles, etc?) this.inputElement.dispatchEvent(changeEvent); if (this.options.dateSelectCallback) { this.options.dateSelectCallback(date); } this.closeDialog(); } /** * Sets the current date in the calendar * - Sets the current date in the calendar * - Focuses the current date button if focus is true * - Marks today and selected date with appropriate classes and attributes * * @param {boolean} focus Whether to focus the current date button * @returns {void} */ private setCurrentDate(focus: boolean = true): void { const currentDate = this.currentDate; const filteredDays = this.calendarDays.filter(calendarDay => calendarDay.button.classList.contains('fully-hidden') === false); filteredDays.forEach((calendarDay) => { calendarDay.button.setAttribute('tabindex', (-1).toString()); calendarDay.button.classList.remove('ds_selected'); const calendarDayDate = calendarDay.date; calendarDayDate.setHours(0, 0, 0, 0); const today = new Date(); today.setHours(0, 0, 0, 0); if (calendarDayDate.getTime() === currentDate.getTime() && !calendarDay.isDisabled) { if (focus) { calendarDay.button.setAttribute('tabindex', (0).toString()); calendarDay.button.focus(); calendarDay.button.classList.add('ds_selected'); } } if (this.inputDate && !this.isDisabledDate(this.inputDate) && calendarDayDate.getTime() === this.inputDate.getTime() ) { calendarDay.button.classList.add('ds_datepicker__current'); calendarDay.button.setAttribute('aria-description', 'selected date'); } else { calendarDay.button.classList.remove('ds_datepicker__current'); calendarDay.button.removeAttribute('aria-description'); } if (calendarDayDate.getTime() === today.getTime()) { calendarDay.button.classList.add('ds_datepicker__today'); calendarDay.button.setAttribute('aria-current', 'date'); } else { calendarDay.button.classList.remove('ds_datepicker__today'); calendarDay.button.removeAttribute('aria-current'); } }); // if no date is tab-able, make the first non-disabled date tab-able if (!focus) { filteredDays[0].button.setAttribute('tabindex', (0).toString()); this.currentDate = filteredDays[0].date; } } /** * Sets the date in the input field(s) * * @param {Date} date - The date to set * @returns {void} */ private setDate(date: Date): void { if (this.isMultipleInput) { this.dateInput.value = date.getDate().toString(); this.monthInput.value = (date.getMonth() + 1).toString(); this.yearInput.value = date.getFullYear().toString(); } else { this.inputElement.value = `${this.leadingZeroes(date.getDate())}/${this.leadingZeroes(date.getMonth() + 1)}/${date.getFullYear()}`; switch (this.datePickerParent.dataset.dateformat) { case 'YMD': this.inputElement.value = `${date.getFullYear()}/${this.leadingZeroes(date.getMonth() + 1)}/${this.leadingZeroes(date.getDate())}`; break; case 'MDY': this.inputElement.value = `${this.leadingZeroes(date.getMonth() + 1)}/${this.leadingZeroes(date.getDate())}/${date.getFullYear()}`; break; case 'DMY': default: this.inputElement.value = `${this.leadingZeroes(date.getDate())}/${this.leadingZeroes(date.getMonth() + 1)}/${date.getFullYear()}`; break; } } } /** * Sets the current date to be within the min and max date range * * @returns {void} */ private setMinAndMaxDatesOnCalendar(): void { if (this.options.minDate && this.currentDate < this.options.minDate) { this.currentDate = this.options.minDate; } if (this.options.maxDate && this.currentDate > this.options.maxDate) { this.currentDate = this.options.maxDate; } } /** * Sets options for the date picker from both passed options and data attributes * * @returns {void} */ private setOptions(): void { this.transformLegacyDataAttributes(); if (!this.options.minDate && this.datePickerParent.dataset.mindate) { this.options.minDate = this.formattedDateFromString(this.datePickerParent.dataset.mindate, null) as Date; } if (!this.options.maxDate && this.datePickerParent.dataset.maxdate) { this.options.maxDate = this.formattedDateFromString(this.datePickerParent.dataset.maxdate, null) as Date; } if (!this.options.disabledDates?.length && this.datePickerParent.dataset.disableddates) { this.options.disabledDates = this.datePickerParent.dataset.disableddates .replace(/\s+/, ' ') .split(' ') .map(item => this.formattedDateFromString(item) as Date) .filter(item => item); } } /** * Toggles the date picker dialog open or closed * * @param {Event} event - The event that triggered the toggle * @returns {void} */ private toggleDialog(event: Event): void { event.preventDefault(); if (this.isOpen()) { this.closeDialog(); } else { this.setMinAndMaxDatesOnCalendar(); this.openDialog(); } } /** * Transforms legacy data attributes from the input element to the date picker parent element * * @returns {void} */ private transformLegacyDataAttributes(): void { if (this.inputElement.dataset.mindate) { this.datePickerParent.dataset.mindate = this.inputElement.dataset.mindate; } if (this.inputElement.dataset.maxdate) { this.datePickerParent.dataset.maxdate = this.inputElement.dataset.maxdate; } if (this.inputElement.dataset.dateformat) { this.datePickerParent.dataset.dateformat = this.inputElement.dataset.dateformat; } } /** * Updates the calendar display by redrawing it * - Sets the dialog title to the current month and year * - Updates each day button in the calendar grid * - Hides days from previous/next month * - Disables days outside min/max date range or in disabled dates list * * @returns {void} */ private updateCalendar(): void { this.dialogTitleElement.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`; this.dialogElement.setAttribute('aria-label', this.dialogTitleElement.innerHTML); const day = this.currentDate; const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1); const dayOfWeek = firstOfMonth.getDay(); firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek); const thisDay = new Date(firstOfMonth); // loop through our days for (const element of this.calendarDays) { const isHidden = thisDay.getMonth() !== day.getMonth(); let isDisabled: boolean = false; if (this.options.minDate && thisDay < this.options.minDate) { isDisabled = true; } if (this.options.maxDate && thisDay > this.options.maxDate) { isDisabled = true; } if (this.isDisabledDate(thisDay)) { isDisabled = true; } element.update(thisDay, isHidden, isDisabled); thisDay.setDate(thisDay.getDate() + 1); } } } /** * Class representing a day button in the date picker calendar * * @class DSCalendarDay * @property {HTMLButtonElement} button - The button element representing the day * @property {number} column - Column index of the day button * @property {Date} date - The date represented by the day button * @property {number} index - Index of the day button in the calendar grid * @property {number} row - Row index of the day button * @property {DSDatePicker} picker - Parent date picker instance */ class DSCalendarDay { button: HTMLButtonElement; date: Date; private picker: DSDatePicker; /** * Constructor for a day button in the date picker calendar * * @param {HTMLElement} button - The button element representing the day * @param {number} index - Index of the day button in the calendar grid * @param {number} row - Row index of the day button * @param {number} column - Column index of the day button * @param {DSDatePicker} picker - Parent date picker instance */ constructor(button: HTMLButtonElement, picker: DSDatePicker) { this.button = button; this.picker = picker; this.date = new Date(); } /** * Initializes the day button, attaching event listeners for click and keydown events * * @returns {void} */ init(): void { this.button.addEventListener('keydown', this.keyPress.bind(this)); this.button.addEventListener('click', this.click.bind(this)); } /** * Updates the day button * - Sets the button text to the day of the month * - Sets the aria-label to the formatted date * - Adds/removes fully-hidden class based on isHidden * - Adds/removes aria-disabled attribute based on isDisabled * - Sets the date property to the provided date * * @param {Date} day The date to update the button with * @param {boolean} isHidden Whether the day is hidden (from previous/next month) * @param {boolean} isDisabled Whether the day is disabled * @returns {void} */ update(day: Date, isHidden: boolean, isDisabled: boolean): void { this.date = new Date(day); this.button.innerHTML = day.getDate().toString(); this.button.setAttribute('aria-label', this.picker.formattedDateHuman(this.date)); if (isDisabled) { this.button.setAttribute('aria-disabled', true.toString()); } else { this.button.removeAttribute('aria-disabled'); } if (isHidden) { this.button.classList.add('fully-hidden'); } else { this.button.classList.remove('fully-hidden'); } } /** * Handler for mouse click on day buttons * - Selects the clicked date * * @param {MouseEvent} event * @returns {void} */ click(event: MouseEvent): void { this.picker.goToDate(this.date); this.picker.selectDate(this.date); event.stopPropagation(); event.preventDefault(); } /** * Handler for keyboard events on day buttons * - Arrow keys to navigate days/weeks * - Home/End to go to first/last day of week * - Page Up/Down to go to previous/next month (with Shift for year) * - Escape to close the dialog * - Enter/Space to select the focused date * - Tab to move focus to next/previous focusable element in the dialog * - Shift+Tab to move focus to previous focusable element in the dialog * * @param {KeyboardEvent} event * @returns {void} */ keyPress(event: KeyboardEvent): void { let calendarNavKey = true; switch (event.key) { case 'ArrowLeft': this.picker.focusPreviousDay(); break; case 'ArrowRight': this.picker.focusNextDay(); break; case 'ArrowUp': this.picker.focusPreviousWeek(); break; case 'ArrowDown': this.picker.focusNextWeek(); break; case 'Home': this.picker.focusFirstDayOfWeek(); break; case 'End': this.picker.focusLastDayOfWeek(); break; case 'PageUp': if (event.shiftKey) { this.picker.focusPreviousYear(event); } else { this.picker.focusPreviousMonth(event); } break; case 'PageDown': if (event.shiftKey) { this.picker.focusNextYear(event); } else { this.picker.focusNextMonth(event); } break; default: calendarNavKey = false; break; } if (calendarNavKey) { event.preventDefault(); event.stopPropagation(); } } } export default DSDatePicker;