Skip to content

Commit 366fd72

Browse files
yuriy-fixdethellarobertsbnrtomivirkkiweb-padawan
authored
feat: Add isDateDisabled functionality (#7075)
* 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>
1 parent 3f23b33 commit 366fd72

17 files changed

+717
-65
lines changed

dev/date-picker.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
<script type="module">
1010
import '@vaadin/date-picker';
1111
import '@vaadin/tooltip';
12+
const isDateDisabled = (date) => {
13+
// Exclude weekends and the 16th day of each month:
14+
const checkDate = new Date(0, 0);
15+
checkDate.setFullYear(date.year);
16+
checkDate.setMonth(date.month);
17+
checkDate.setDate(date.day);
18+
return checkDate.getDay() === 0 || checkDate.getDay() === 6 || checkDate.getDate() === 16;
19+
}
20+
const picker = document.querySelector('vaadin-date-picker');
21+
picker.isDateDisabled = isDateDisabled;
22+
picker.min = '2023-11-01';
1223
</script>
1324
</head>
1425
<body>

packages/date-picker/src/vaadin-date-picker-helper.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ declare function dateEquals(date1: Date | null, date2: Date | null): boolean;
1717
*
1818
* @returns True if the date is in the range
1919
*/
20-
declare function dateAllowed(date: Date, min: Date | null, max: Date | null): boolean;
20+
declare function dateAllowed(
21+
date: Date,
22+
min: Date | null,
23+
max: Date | null,
24+
isDateDisabled: (DatePickerDate) => boolean | null,
25+
): boolean;
2126

2227
/**
2328
* Get closest date from array of dates.

packages/date-picker/src/vaadin-date-picker-helper.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,37 @@ export function dateEquals(date1, date2) {
5353
);
5454
}
5555

56+
/**
57+
* Extracts the basic component parts of a date (day, month and year)
58+
* to the expected format.
59+
* @param {!Date} date
60+
* @return {{day: number, month: number, year: number}}
61+
*/
62+
export function extractDateParts(date) {
63+
return {
64+
day: date.getDate(),
65+
month: date.getMonth(),
66+
year: date.getFullYear(),
67+
};
68+
}
69+
5670
/**
5771
* Check if the given date is in the range of allowed dates.
5872
*
5973
* @param {!Date} date The date to check
6074
* @param {Date} min Range start
6175
* @param {Date} max Range end
76+
* @param {function(!DatePickerDate): boolean} isDateDisabled Callback to check if the date is disabled
6277
* @return {boolean} True if the date is in the range
6378
*/
64-
export function dateAllowed(date, min, max) {
65-
return (!min || date >= min) && (!max || date <= max);
79+
export function dateAllowed(date, min, max, isDateDisabled) {
80+
let dateIsDisabled = false;
81+
if (typeof isDateDisabled === 'function' && !!date) {
82+
const dateToCheck = extractDateParts(date);
83+
dateIsDisabled = isDateDisabled(dateToCheck);
84+
}
85+
86+
return (!min || date >= min) && (!max || date <= max) && !dateIsDisabled;
6687
}
6788

6889
/**
@@ -90,20 +111,6 @@ export function getClosestDate(date, dates) {
90111
});
91112
}
92113

93-
/**
94-
* Extracts the basic component parts of a date (day, month and year)
95-
* to the expected format.
96-
* @param {!Date} date
97-
* @return {{day: number, month: number, year: number}}
98-
*/
99-
export function extractDateParts(date) {
100-
return {
101-
day: date.getDate(),
102-
month: date.getMonth(),
103-
year: date.getFullYear(),
104-
};
105-
}
106-
107114
/**
108115
* Get difference in months between today and given months value.
109116
*

packages/date-picker/src/vaadin-date-picker-mixin.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ export declare class DatePickerMixinClass {
237237
*/
238238
max: string | undefined;
239239

240+
/**
241+
* A function to be used to determine whether the user can select a given date.
242+
* Receives a `DatePickerDate` object of the date to be selected and should return a
243+
* boolean.
244+
*/
245+
isDateDisabled: (date: DatePickerDate) => boolean;
246+
240247
/**
241248
* Opens the dropdown.
242249
*/

packages/date-picker/src/vaadin-date-picker-mixin.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,17 @@ export const DatePickerMixin = (subclass) =>
302302
sync: true,
303303
},
304304

305+
/**
306+
* A function to be used to determine whether the user can select a given date.
307+
* Receives a `DatePickerDate` object of the date to be selected and should return a
308+
* boolean.
309+
*
310+
* @type {function(DatePickerDate): boolean | undefined}
311+
*/
312+
isDateDisabled: {
313+
type: Function,
314+
},
315+
305316
/**
306317
* The earliest date that can be selected. All earlier dates will be disabled.
307318
* @type {Date | undefined}
@@ -365,7 +376,7 @@ export const DatePickerMixin = (subclass) =>
365376
return [
366377
'_selectedDateChanged(_selectedDate, i18n)',
367378
'_focusedDateChanged(_focusedDate, i18n)',
368-
'__updateOverlayContent(_overlayContent, i18n, label, _minDate, _maxDate, _focusedDate, _selectedDate, showWeekNumbers)',
379+
'__updateOverlayContent(_overlayContent, i18n, label, _minDate, _maxDate, _focusedDate, _selectedDate, showWeekNumbers, isDateDisabled)',
369380
'__updateOverlayContentTheme(_overlayContent, _theme)',
370381
'__updateOverlayContentFullScreen(_overlayContent, _fullscreen)',
371382
];
@@ -601,7 +612,8 @@ export const DatePickerMixin = (subclass) =>
601612
checkValidity() {
602613
const inputValue = this._inputElementValue;
603614
const inputValid = !inputValue || (!!this._selectedDate && inputValue === this.__formatDate(this._selectedDate));
604-
const minMaxValid = !this._selectedDate || dateAllowed(this._selectedDate, this._minDate, this._maxDate);
615+
const isDateValid =
616+
!this._selectedDate || dateAllowed(this._selectedDate, this._minDate, this._maxDate, this.isDateDisabled);
605617

606618
let inputValidity = true;
607619
if (this.inputElement) {
@@ -613,7 +625,7 @@ export const DatePickerMixin = (subclass) =>
613625
}
614626
}
615627

616-
return inputValid && minMaxValid && inputValidity;
628+
return inputValid && isDateValid && inputValidity;
617629
}
618630

619631
/**
@@ -852,7 +864,17 @@ export const DatePickerMixin = (subclass) =>
852864

853865
/** @private */
854866
// eslint-disable-next-line max-params
855-
__updateOverlayContent(overlayContent, i18n, label, minDate, maxDate, focusedDate, selectedDate, showWeekNumbers) {
867+
__updateOverlayContent(
868+
overlayContent,
869+
i18n,
870+
label,
871+
minDate,
872+
maxDate,
873+
focusedDate,
874+
selectedDate,
875+
showWeekNumbers,
876+
isDateDisabled,
877+
) {
856878
if (overlayContent) {
857879
overlayContent.i18n = i18n;
858880
overlayContent.label = label;
@@ -861,6 +883,7 @@ export const DatePickerMixin = (subclass) =>
861883
overlayContent.focusedDate = focusedDate;
862884
overlayContent.selectedDate = selectedDate;
863885
overlayContent.showWeekNumbers = showWeekNumbers;
886+
overlayContent.isDateDisabled = isDateDisabled;
864887
}
865888
}
866889

@@ -932,9 +955,11 @@ export const DatePickerMixin = (subclass) =>
932955
const initialPosition =
933956
this._selectedDate || this._overlayContent.initialPosition || parsedInitialPosition || new Date();
934957

935-
return parsedInitialPosition || dateAllowed(initialPosition, this._minDate, this._maxDate)
958+
return parsedInitialPosition || dateAllowed(initialPosition, this._minDate, this._maxDate, this.isDateDisabled)
936959
? initialPosition
937-
: getClosestDate(initialPosition, [this._minDate, this._maxDate]);
960+
: this._minDate || this._maxDate
961+
? getClosestDate(initialPosition, [this._minDate, this._maxDate])
962+
: new Date();
938963
}
939964

940965
/**

packages/date-picker/src/vaadin-date-picker-overlay-content-mixin.js

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
1010
import { addListener, setTouchAction } from '@vaadin/component-base/src/gestures.js';
1111
import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
1212
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
13-
import { dateAfterXMonths, dateEquals, extractDateParts, getClosestDate } from './vaadin-date-picker-helper.js';
13+
import {
14+
dateAfterXMonths,
15+
dateAllowed,
16+
dateEquals,
17+
extractDateParts,
18+
getClosestDate,
19+
} from './vaadin-date-picker-helper.js';
1420

1521
/**
1622
* @polymerMixin
@@ -107,6 +113,17 @@ export const DatePickerOverlayContentMixin = (superClass) =>
107113
sync: true,
108114
},
109115

116+
/**
117+
* A function to be used to determine whether the user can select a given date.
118+
* Receives a `DatePickerDate` object of the date to be selected and should return a
119+
* boolean.
120+
*
121+
* @type {function(DatePickerDate): boolean | undefined}
122+
*/
123+
isDateDisabled: {
124+
type: Function,
125+
},
126+
110127
/**
111128
* Input label
112129
*/
@@ -134,9 +151,9 @@ export const DatePickerOverlayContentMixin = (superClass) =>
134151

135152
static get observers() {
136153
return [
137-
'__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme)',
154+
'__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme, isDateDisabled)',
138155
'__updateCancelButton(_cancelButton, i18n)',
139-
'__updateTodayButton(_todayButton, i18n, minDate, maxDate)',
156+
'__updateTodayButton(_todayButton, i18n, minDate, maxDate, isDateDisabled)',
140157
'__updateYears(years, selectedDate, _theme)',
141158
];
142159
}
@@ -303,10 +320,10 @@ export const DatePickerOverlayContentMixin = (superClass) =>
303320
}
304321

305322
/** @private */
306-
__updateTodayButton(todayButton, i18n, minDate, maxDate) {
323+
__updateTodayButton(todayButton, i18n, minDate, maxDate, isDateDisabled) {
307324
if (todayButton) {
308325
todayButton.textContent = i18n && i18n.today;
309-
todayButton.disabled = !this._isTodayAllowed(minDate, maxDate);
326+
todayButton.disabled = !this._isTodayAllowed(minDate, maxDate, isDateDisabled);
310327
}
311328
}
312329

@@ -321,12 +338,14 @@ export const DatePickerOverlayContentMixin = (superClass) =>
321338
showWeekNumbers,
322339
ignoreTaps,
323340
theme,
341+
isDateDisabled,
324342
) {
325343
if (calendars && calendars.length) {
326344
calendars.forEach((calendar) => {
327345
calendar.i18n = i18n;
328346
calendar.minDate = minDate;
329347
calendar.maxDate = maxDate;
348+
calendar.isDateDisabled = isDateDisabled;
330349
calendar.focusedDate = focusedDate;
331350
calendar.selectedDate = selectedDate;
332351
calendar.showWeekNumbers = showWeekNumbers;
@@ -361,10 +380,14 @@ export const DatePickerOverlayContentMixin = (superClass) =>
361380
* @protected
362381
*/
363382
_selectDate(dateToSelect) {
383+
if (!this._dateAllowed(dateToSelect)) {
384+
return false;
385+
}
364386
this.selectedDate = dateToSelect;
365387
this.dispatchEvent(
366388
new CustomEvent('date-selected', { detail: { date: dateToSelect }, bubbles: true, composed: true }),
367389
);
390+
return true;
368391
}
369392

370393
/** @private */
@@ -775,9 +798,10 @@ export const DatePickerOverlayContentMixin = (superClass) =>
775798
handled = true;
776799
break;
777800
case 'Enter':
778-
this._selectDate(this.focusedDate);
779-
this._close();
780-
handled = true;
801+
if (this._selectDate(this.focusedDate)) {
802+
this._close();
803+
handled = true;
804+
}
781805
break;
782806
case ' ':
783807
this.__toggleDate(this.focusedDate);
@@ -931,7 +955,8 @@ export const DatePickerOverlayContentMixin = (superClass) =>
931955

932956
/** @private */
933957
_focusAllowedDate(dateToFocus, diff, keepMonth) {
934-
if (this._dateAllowed(dateToFocus)) {
958+
// For this check we do consider the isDateDisabled function because disabled dates are allowed to be focused, just not outside min/max
959+
if (this._dateAllowed(dateToFocus, undefined, undefined, () => false)) {
935960
this.focusDate(dateToFocus, keepMonth);
936961
} else if (this._dateAllowed(this.focusedDate)) {
937962
// Move to min or max date
@@ -1009,18 +1034,18 @@ export const DatePickerOverlayContentMixin = (superClass) =>
10091034
}
10101035

10111036
/** @private */
1012-
_dateAllowed(date, min = this.minDate, max = this.maxDate) {
1013-
return (!min || date >= min) && (!max || date <= max);
1037+
_dateAllowed(date, min = this.minDate, max = this.maxDate, isDateDisabled = this.isDateDisabled) {
1038+
return dateAllowed(date, min, max, isDateDisabled);
10141039
}
10151040

10161041
/** @private */
1017-
_isTodayAllowed(min, max) {
1042+
_isTodayAllowed(min, max, isDateDisabled) {
10181043
const today = new Date();
10191044
const todayMidnight = new Date(0, 0);
10201045
todayMidnight.setFullYear(today.getFullYear());
10211046
todayMidnight.setMonth(today.getMonth());
10221047
todayMidnight.setDate(today.getDate());
1023-
return this._dateAllowed(todayMidnight, min, max);
1048+
return this._dateAllowed(todayMidnight, min, max, isDateDisabled);
10241049
}
10251050

10261051
/**

packages/date-picker/src/vaadin-lit-month-calendar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class MonthCalendar extends MonthCalendarMixin(ThemableMixin(PolylitMixin(LitEle
6161
${week.map((date) => {
6262
const isFocused = dateEquals(date, this.focusedDate);
6363
const isSelected = dateEquals(date, this.selectedDate);
64-
const isDisabled = !dateAllowed(date, this.minDate, this.maxDate);
64+
const isDisabled = !dateAllowed(date, this.minDate, this.maxDate, this.isDateDisabled);
6565
6666
const parts = [
6767
'date',

packages/date-picker/src/vaadin-month-calendar-mixin.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ export const MonthCalendarMixin = (superClass) =>
8080
sync: true,
8181
},
8282

83+
/**
84+
* A function to be used to determine whether the user can select a given date.
85+
* Receives a `DatePickerDate` object of the date to be selected and should return a
86+
* boolean.
87+
* @type {Function | undefined}
88+
*/
89+
isDateDisabled: {
90+
type: Function,
91+
value: () => false,
92+
},
93+
8394
disabled: {
8495
type: Boolean,
8596
reflectToAttribute: true,

0 commit comments

Comments
 (0)