From 366fd72fd12312b0202558fe7ff8b6026b100577 Mon Sep 17 00:00:00 2001 From: Yuriy Yevstihnyeyev Date: Mon, 15 Jan 2024 13:13:55 +0200 Subject: [PATCH] feat: Add isDateDisabled functionality (#7075) * Add isDateAvailable property * Further changes to allow disabling arbitrary dates Co-authored-by: A.J. Roberts * Starting to add tests for isDateAvailable changes * Add validation and disabled tests * refactor: Refactoring per review comments * feat(date-picker): Add disabled function to demo * feat(date-picker): update month-calendar tests * feat(date-picker): Update datepicker playground to ensure disabled function returns a true boolean * feat(date-picker): month-calendar should not use isDateDisabled function for checking if entire month is disabled * fix(date-picker): change per review. * feat(date-picker): Update isDateDisabled contract to use DatePickerDate type instead of Date * feat(date-picker): Update date-picker html demo to provide sample isDateDisabled use * feat(date-picker): Update keyboard logic for disabled dates * feat(date-picker): Update date-picker demo * fix(date-picker): Fix bug in overlay mixin _selectDate function * fix(date-picker): update disabled dates test to use DatePickerDate type * feat(date-picker): Add tests for keyboard navigation * fix(date-picker): Update getDayAriaDisabled logic to account for custom function * fix(date-picker): update month-calendar tests for disabled date function * fix(date-picker): fix lint issues * chore(date-picker): move keyboard-related disabled date tests to keyboard nav suite * fix(date-picker): update keyboard validation tests * handle async property update in a Lit test * fix(date-picker): per review - use dateAllowed helper function instead of dupe logic * test: align tests with existing tests * feat(date-picker): Update keyboard test for disabled dates to test ENTER * test: split cases into separate tests * test: fix test errors in the console * chore: update helper import * Update packages/date-picker/src/vaadin-date-picker-overlay-content-mixin.js --------- Co-authored-by: David Ethell Co-authored-by: A.J. Roberts Co-authored-by: Tomi Virkki Co-authored-by: Serhii Kulykov --- dev/date-picker.html | 11 + .../src/vaadin-date-picker-helper.d.ts | 7 +- .../src/vaadin-date-picker-helper.js | 39 +- .../src/vaadin-date-picker-mixin.d.ts | 7 + .../src/vaadin-date-picker-mixin.js | 37 +- ...aadin-date-picker-overlay-content-mixin.js | 51 +- .../src/vaadin-lit-month-calendar.js | 2 +- .../src/vaadin-month-calendar-mixin.js | 11 + .../date-picker/src/vaadin-month-calendar.js | 22 +- .../__snapshots__/month-calendar.test.snap.js | 440 ++++++++++++++++++ .../test/dom/month-calendar.test.js | 10 + .../test/keyboard-input-polymer.test.js | 2 +- .../test/keyboard-navigation-polymer.test.js | 2 +- .../test/keyboard-navigation.common.js | 91 ++++ .../date-picker/test/month-calendar.common.js | 17 +- .../test/typings/date-picker.types.ts | 3 +- .../date-picker/test/validation.common.js | 30 +- 17 files changed, 717 insertions(+), 65 deletions(-) diff --git a/dev/date-picker.html b/dev/date-picker.html index ea00a9b171..90df3f0078 100644 --- a/dev/date-picker.html +++ b/dev/date-picker.html @@ -9,6 +9,17 @@ diff --git a/packages/date-picker/src/vaadin-date-picker-helper.d.ts b/packages/date-picker/src/vaadin-date-picker-helper.d.ts index c42d79cdf4..8aabc1797a 100644 --- a/packages/date-picker/src/vaadin-date-picker-helper.d.ts +++ b/packages/date-picker/src/vaadin-date-picker-helper.d.ts @@ -17,7 +17,12 @@ declare function dateEquals(date1: Date | null, date2: Date | null): boolean; * * @returns True if the date is in the range */ -declare function dateAllowed(date: Date, min: Date | null, max: Date | null): boolean; +declare function dateAllowed( + date: Date, + min: Date | null, + max: Date | null, + isDateDisabled: (DatePickerDate) => boolean | null, +): boolean; /** * Get closest date from array of dates. diff --git a/packages/date-picker/src/vaadin-date-picker-helper.js b/packages/date-picker/src/vaadin-date-picker-helper.js index ba13525296..b6b8d39c2a 100644 --- a/packages/date-picker/src/vaadin-date-picker-helper.js +++ b/packages/date-picker/src/vaadin-date-picker-helper.js @@ -53,16 +53,37 @@ export function dateEquals(date1, date2) { ); } +/** + * Extracts the basic component parts of a date (day, month and year) + * to the expected format. + * @param {!Date} date + * @return {{day: number, month: number, year: number}} + */ +export function extractDateParts(date) { + return { + day: date.getDate(), + month: date.getMonth(), + year: date.getFullYear(), + }; +} + /** * Check if the given date is in the range of allowed dates. * * @param {!Date} date The date to check * @param {Date} min Range start * @param {Date} max Range end + * @param {function(!DatePickerDate): boolean} isDateDisabled Callback to check if the date is disabled * @return {boolean} True if the date is in the range */ -export function dateAllowed(date, min, max) { - return (!min || date >= min) && (!max || date <= max); +export function dateAllowed(date, min, max, isDateDisabled) { + let dateIsDisabled = false; + if (typeof isDateDisabled === 'function' && !!date) { + const dateToCheck = extractDateParts(date); + dateIsDisabled = isDateDisabled(dateToCheck); + } + + return (!min || date >= min) && (!max || date <= max) && !dateIsDisabled; } /** @@ -90,20 +111,6 @@ export function getClosestDate(date, dates) { }); } -/** - * Extracts the basic component parts of a date (day, month and year) - * to the expected format. - * @param {!Date} date - * @return {{day: number, month: number, year: number}} - */ -export function extractDateParts(date) { - return { - day: date.getDate(), - month: date.getMonth(), - year: date.getFullYear(), - }; -} - /** * Get difference in months between today and given months value. * diff --git a/packages/date-picker/src/vaadin-date-picker-mixin.d.ts b/packages/date-picker/src/vaadin-date-picker-mixin.d.ts index 06f11afc66..29a44c7002 100644 --- a/packages/date-picker/src/vaadin-date-picker-mixin.d.ts +++ b/packages/date-picker/src/vaadin-date-picker-mixin.d.ts @@ -237,6 +237,13 @@ export declare class DatePickerMixinClass { */ max: string | undefined; + /** + * A function to be used to determine whether the user can select a given date. + * Receives a `DatePickerDate` object of the date to be selected and should return a + * boolean. + */ + isDateDisabled: (date: DatePickerDate) => boolean; + /** * Opens the dropdown. */ diff --git a/packages/date-picker/src/vaadin-date-picker-mixin.js b/packages/date-picker/src/vaadin-date-picker-mixin.js index 21dc183fd7..5a4a196b9b 100644 --- a/packages/date-picker/src/vaadin-date-picker-mixin.js +++ b/packages/date-picker/src/vaadin-date-picker-mixin.js @@ -302,6 +302,17 @@ export const DatePickerMixin = (subclass) => sync: true, }, + /** + * A function to be used to determine whether the user can select a given date. + * Receives a `DatePickerDate` object of the date to be selected and should return a + * boolean. + * + * @type {function(DatePickerDate): boolean | undefined} + */ + isDateDisabled: { + type: Function, + }, + /** * The earliest date that can be selected. All earlier dates will be disabled. * @type {Date | undefined} @@ -365,7 +376,7 @@ export const DatePickerMixin = (subclass) => return [ '_selectedDateChanged(_selectedDate, i18n)', '_focusedDateChanged(_focusedDate, i18n)', - '__updateOverlayContent(_overlayContent, i18n, label, _minDate, _maxDate, _focusedDate, _selectedDate, showWeekNumbers)', + '__updateOverlayContent(_overlayContent, i18n, label, _minDate, _maxDate, _focusedDate, _selectedDate, showWeekNumbers, isDateDisabled)', '__updateOverlayContentTheme(_overlayContent, _theme)', '__updateOverlayContentFullScreen(_overlayContent, _fullscreen)', ]; @@ -601,7 +612,8 @@ export const DatePickerMixin = (subclass) => checkValidity() { const inputValue = this._inputElementValue; const inputValid = !inputValue || (!!this._selectedDate && inputValue === this.__formatDate(this._selectedDate)); - const minMaxValid = !this._selectedDate || dateAllowed(this._selectedDate, this._minDate, this._maxDate); + const isDateValid = + !this._selectedDate || dateAllowed(this._selectedDate, this._minDate, this._maxDate, this.isDateDisabled); let inputValidity = true; if (this.inputElement) { @@ -613,7 +625,7 @@ export const DatePickerMixin = (subclass) => } } - return inputValid && minMaxValid && inputValidity; + return inputValid && isDateValid && inputValidity; } /** @@ -852,7 +864,17 @@ export const DatePickerMixin = (subclass) => /** @private */ // eslint-disable-next-line max-params - __updateOverlayContent(overlayContent, i18n, label, minDate, maxDate, focusedDate, selectedDate, showWeekNumbers) { + __updateOverlayContent( + overlayContent, + i18n, + label, + minDate, + maxDate, + focusedDate, + selectedDate, + showWeekNumbers, + isDateDisabled, + ) { if (overlayContent) { overlayContent.i18n = i18n; overlayContent.label = label; @@ -861,6 +883,7 @@ export const DatePickerMixin = (subclass) => overlayContent.focusedDate = focusedDate; overlayContent.selectedDate = selectedDate; overlayContent.showWeekNumbers = showWeekNumbers; + overlayContent.isDateDisabled = isDateDisabled; } } @@ -932,9 +955,11 @@ export const DatePickerMixin = (subclass) => const initialPosition = this._selectedDate || this._overlayContent.initialPosition || parsedInitialPosition || new Date(); - return parsedInitialPosition || dateAllowed(initialPosition, this._minDate, this._maxDate) + return parsedInitialPosition || dateAllowed(initialPosition, this._minDate, this._maxDate, this.isDateDisabled) ? initialPosition - : getClosestDate(initialPosition, [this._minDate, this._maxDate]); + : this._minDate || this._maxDate + ? getClosestDate(initialPosition, [this._minDate, this._maxDate]) + : new Date(); } /** diff --git a/packages/date-picker/src/vaadin-date-picker-overlay-content-mixin.js b/packages/date-picker/src/vaadin-date-picker-overlay-content-mixin.js index 504269ad2f..3786327d40 100644 --- a/packages/date-picker/src/vaadin-date-picker-overlay-content-mixin.js +++ b/packages/date-picker/src/vaadin-date-picker-overlay-content-mixin.js @@ -10,7 +10,13 @@ import { Debouncer } from '@vaadin/component-base/src/debounce.js'; import { addListener, setTouchAction } from '@vaadin/component-base/src/gestures.js'; import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js'; import { SlotController } from '@vaadin/component-base/src/slot-controller.js'; -import { dateAfterXMonths, dateEquals, extractDateParts, getClosestDate } from './vaadin-date-picker-helper.js'; +import { + dateAfterXMonths, + dateAllowed, + dateEquals, + extractDateParts, + getClosestDate, +} from './vaadin-date-picker-helper.js'; /** * @polymerMixin @@ -107,6 +113,17 @@ export const DatePickerOverlayContentMixin = (superClass) => sync: true, }, + /** + * A function to be used to determine whether the user can select a given date. + * Receives a `DatePickerDate` object of the date to be selected and should return a + * boolean. + * + * @type {function(DatePickerDate): boolean | undefined} + */ + isDateDisabled: { + type: Function, + }, + /** * Input label */ @@ -134,9 +151,9 @@ export const DatePickerOverlayContentMixin = (superClass) => static get observers() { return [ - '__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme)', + '__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme, isDateDisabled)', '__updateCancelButton(_cancelButton, i18n)', - '__updateTodayButton(_todayButton, i18n, minDate, maxDate)', + '__updateTodayButton(_todayButton, i18n, minDate, maxDate, isDateDisabled)', '__updateYears(years, selectedDate, _theme)', ]; } @@ -303,10 +320,10 @@ export const DatePickerOverlayContentMixin = (superClass) => } /** @private */ - __updateTodayButton(todayButton, i18n, minDate, maxDate) { + __updateTodayButton(todayButton, i18n, minDate, maxDate, isDateDisabled) { if (todayButton) { todayButton.textContent = i18n && i18n.today; - todayButton.disabled = !this._isTodayAllowed(minDate, maxDate); + todayButton.disabled = !this._isTodayAllowed(minDate, maxDate, isDateDisabled); } } @@ -321,12 +338,14 @@ export const DatePickerOverlayContentMixin = (superClass) => showWeekNumbers, ignoreTaps, theme, + isDateDisabled, ) { if (calendars && calendars.length) { calendars.forEach((calendar) => { calendar.i18n = i18n; calendar.minDate = minDate; calendar.maxDate = maxDate; + calendar.isDateDisabled = isDateDisabled; calendar.focusedDate = focusedDate; calendar.selectedDate = selectedDate; calendar.showWeekNumbers = showWeekNumbers; @@ -361,10 +380,14 @@ export const DatePickerOverlayContentMixin = (superClass) => * @protected */ _selectDate(dateToSelect) { + if (!this._dateAllowed(dateToSelect)) { + return false; + } this.selectedDate = dateToSelect; this.dispatchEvent( new CustomEvent('date-selected', { detail: { date: dateToSelect }, bubbles: true, composed: true }), ); + return true; } /** @private */ @@ -775,9 +798,10 @@ export const DatePickerOverlayContentMixin = (superClass) => handled = true; break; case 'Enter': - this._selectDate(this.focusedDate); - this._close(); - handled = true; + if (this._selectDate(this.focusedDate)) { + this._close(); + handled = true; + } break; case ' ': this.__toggleDate(this.focusedDate); @@ -931,7 +955,8 @@ export const DatePickerOverlayContentMixin = (superClass) => /** @private */ _focusAllowedDate(dateToFocus, diff, keepMonth) { - if (this._dateAllowed(dateToFocus)) { + // For this check we do consider the isDateDisabled function because disabled dates are allowed to be focused, just not outside min/max + if (this._dateAllowed(dateToFocus, undefined, undefined, () => false)) { this.focusDate(dateToFocus, keepMonth); } else if (this._dateAllowed(this.focusedDate)) { // Move to min or max date @@ -1009,18 +1034,18 @@ export const DatePickerOverlayContentMixin = (superClass) => } /** @private */ - _dateAllowed(date, min = this.minDate, max = this.maxDate) { - return (!min || date >= min) && (!max || date <= max); + _dateAllowed(date, min = this.minDate, max = this.maxDate, isDateDisabled = this.isDateDisabled) { + return dateAllowed(date, min, max, isDateDisabled); } /** @private */ - _isTodayAllowed(min, max) { + _isTodayAllowed(min, max, isDateDisabled) { const today = new Date(); const todayMidnight = new Date(0, 0); todayMidnight.setFullYear(today.getFullYear()); todayMidnight.setMonth(today.getMonth()); todayMidnight.setDate(today.getDate()); - return this._dateAllowed(todayMidnight, min, max); + return this._dateAllowed(todayMidnight, min, max, isDateDisabled); } /** diff --git a/packages/date-picker/src/vaadin-lit-month-calendar.js b/packages/date-picker/src/vaadin-lit-month-calendar.js index 7cd3bb1dc7..2a72fbeb7e 100644 --- a/packages/date-picker/src/vaadin-lit-month-calendar.js +++ b/packages/date-picker/src/vaadin-lit-month-calendar.js @@ -61,7 +61,7 @@ class MonthCalendar extends MonthCalendarMixin(ThemableMixin(PolylitMixin(LitEle ${week.map((date) => { const isFocused = dateEquals(date, this.focusedDate); const isSelected = dateEquals(date, this.selectedDate); - const isDisabled = !dateAllowed(date, this.minDate, this.maxDate); + const isDisabled = !dateAllowed(date, this.minDate, this.maxDate, this.isDateDisabled); const parts = [ 'date', diff --git a/packages/date-picker/src/vaadin-month-calendar-mixin.js b/packages/date-picker/src/vaadin-month-calendar-mixin.js index 856f7a0f44..1ccbf07ee0 100644 --- a/packages/date-picker/src/vaadin-month-calendar-mixin.js +++ b/packages/date-picker/src/vaadin-month-calendar-mixin.js @@ -80,6 +80,17 @@ export const MonthCalendarMixin = (superClass) => sync: true, }, + /** + * A function to be used to determine whether the user can select a given date. + * Receives a `DatePickerDate` object of the date to be selected and should return a + * boolean. + * @type {Function | undefined} + */ + isDateDisabled: { + type: Function, + value: () => false, + }, + disabled: { type: Boolean, reflectToAttribute: true, diff --git a/packages/date-picker/src/vaadin-month-calendar.js b/packages/date-picker/src/vaadin-month-calendar.js index 8598d4724a..eb2604af2e 100644 --- a/packages/date-picker/src/vaadin-month-calendar.js +++ b/packages/date-picker/src/vaadin-month-calendar.js @@ -50,12 +50,12 @@ class MonthCalendar extends MonthCalendarMixin(ThemableMixin(PolymerElement)) {