Skip to content

Commit

Permalink
feat: Add isDateDisabled functionality (#7075)
Browse files Browse the repository at this point in the history
* Add isDateAvailable property

* Further changes to allow disabling arbitrary dates

Co-authored-by: A.J. Roberts <arobertsbnr@users.noreply.github.com>

* 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 <dethell@jhacorp.com>
Co-authored-by: A.J. Roberts <arobertsbnr@users.noreply.github.com>
Co-authored-by: Tomi Virkki <virkki@vaadin.com>
Co-authored-by: Serhii Kulykov <iamkulykov@gmail.com>
  • Loading branch information
5 people committed Jan 15, 2024
1 parent 3f23b33 commit 366fd72
Show file tree
Hide file tree
Showing 17 changed files with 717 additions and 65 deletions.
11 changes: 11 additions & 0 deletions dev/date-picker.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
<script type="module">
import '@vaadin/date-picker';
import '@vaadin/tooltip';
const isDateDisabled = (date) => {
// Exclude weekends and the 16th day of each month:
const checkDate = new Date(0, 0);
checkDate.setFullYear(date.year);
checkDate.setMonth(date.month);
checkDate.setDate(date.day);
return checkDate.getDay() === 0 || checkDate.getDay() === 6 || checkDate.getDate() === 16;
}
const picker = document.querySelector('vaadin-date-picker');
picker.isDateDisabled = isDateDisabled;
picker.min = '2023-11-01';
</script>
</head>
<body>
Expand Down
7 changes: 6 additions & 1 deletion packages/date-picker/src/vaadin-date-picker-helper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 23 additions & 16 deletions packages/date-picker/src/vaadin-date-picker-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down
7 changes: 7 additions & 0 deletions packages/date-picker/src/vaadin-date-picker-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
37 changes: 31 additions & 6 deletions packages/date-picker/src/vaadin-date-picker-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)',
];
Expand Down Expand Up @@ -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) {
Expand All @@ -613,7 +625,7 @@ export const DatePickerMixin = (subclass) =>
}
}

return inputValid && minMaxValid && inputValidity;
return inputValid && isDateValid && inputValidity;
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -861,6 +883,7 @@ export const DatePickerMixin = (subclass) =>
overlayContent.focusedDate = focusedDate;
overlayContent.selectedDate = selectedDate;
overlayContent.showWeekNumbers = showWeekNumbers;
overlayContent.isDateDisabled = isDateDisabled;
}
}

Expand Down Expand Up @@ -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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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)',
];
}
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/date-picker/src/vaadin-lit-month-calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
11 changes: 11 additions & 0 deletions packages/date-picker/src/vaadin-month-calendar-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 366fd72

Please sign in to comment.