Skip to content
Closed
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7cc70a1
Add isDateAvailable property
dethell Jan 5, 2023
6a49802
Further changes to allow disabling arbitrary dates
dethell Jan 6, 2023
7df3432
Starting to add tests for isDateAvailable changes
dethell Jan 9, 2023
e58f796
Add validation and disabled tests
dethell Jan 17, 2023
0c119cf
refactor: Refactoring per review comments
dethell Sep 21, 2023
848efbe
feat(date-picker): Add disabled function to demo
dethell Sep 26, 2023
41865b2
feat(date-picker): update month-calendar tests
dethell Oct 5, 2023
8c687f2
feat(date-picker): Update datepicker playground to ensure disabled fu…
dethell Oct 5, 2023
2474fe7
feat(date-picker): month-calendar should not use isDateDisabled funct…
dethell Oct 5, 2023
efadb48
fix(date-picker): change per review.
dethell Oct 6, 2023
077d79f
feat(date-picker): Update isDateDisabled contract to use DatePickerDa…
dethell Oct 12, 2023
f9b077e
feat(date-picker): Update date-picker html demo to provide sample isD…
dethell Oct 12, 2023
47cd2bb
feat(date-picker): Update keyboard logic for disabled dates
dethell Oct 16, 2023
93c8e1d
Merge remote-tracking branch 'upstream/main'
dethell Oct 18, 2023
e67599d
feat(date-picker): Update date-picker demo
dethell Oct 27, 2023
8bc4cfd
fix(date-picker): Fix bug in overlay mixin _selectDate function
dethell Oct 27, 2023
a963bd4
fix(date-picker): update disabled dates test to use DatePickerDate type
dethell Oct 27, 2023
46a33d4
feat(date-picker): Add tests for keyboard navigation
dethell Oct 27, 2023
1c103f8
fix(date-picker): Update getDayAriaDisabled logic to account for cust…
dethell Nov 8, 2023
459a783
fix(date-picker): update month-calendar tests for disabled date function
dethell Nov 8, 2023
3b2446d
fix(date-picker): fix lint issues
dethell Nov 10, 2023
07e8687
chore(date-picker): move keyboard-related disabled date tests to keyb…
dethell Nov 14, 2023
2268b6a
Merge remote-tracking branch 'upstream/main'
dethell Nov 14, 2023
59750ee
fix(date-picker): update keyboard validation tests
dethell Nov 15, 2023
1dd26c8
handle async property update in a Lit test
tomivirkki Nov 16, 2023
1010d3c
fix(date-picker): per review - use dateAllowed helper function instea…
dethell Nov 16, 2023
9ab7e8a
test: align tests with existing tests
tomivirkki Nov 17, 2023
4e93815
feat(date-picker): Update keyboard test for disabled dates to test ENTER
dethell Nov 21, 2023
2143f0e
Merge branch 'main' into dethell-jh
tomivirkki Nov 22, 2023
f0793b9
test: split cases into separate tests
tomivirkki Nov 22, 2023
a1d37c1
test: fix test errors in the console
tomivirkki Nov 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
Comment on lines +12 to +19
Copy link
Member

@tomivirkki tomivirkki Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We recently had a brief internal discussion regarding the parameter format for the
isDateDisabled function, considering the diverse range of use cases. Two potential options:

Option 1: DatePickerDate ({year: 2023, month: 10, day: 10}):

  • More structured and explicit, allowing users to access individual components of the date easily.
  • Might be easier for disabling specific weekdays or days of months

Option 2: ISO Formatted Date String ("2023-11-10"):

  • Familiar format, commonly used in various contexts.
  • Same format as the component's value
  • Might be easier for disabling specific dates (stored in DB as local date for example)

If a decision between these two options proves challenging, we could consider offering flexibility by providing both ISO formatted date strings and date objects with individual components. For example, an extended DatePickerDate could include both formats as follows:

{
  year: 2023,
  month: 10,
  day: 10,
  date: "2023-11-10"
}

@rolfsmeds @yuriy-fix

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the ISO date be optional or should we expect that the DatePickerDate structure always contains all four fields?

Copy link
Member

@tomivirkki tomivirkki Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying DatePickerDate might be breaking, so better introduce a new type or extend it (if this is the option we want to go with).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now it seems to be working well enough with DatePickerDate. If a developer needs more detailed logic it's easy enough to convert it to a Javascript Date object.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rolfsmeds and @yuriy-fix , can you weigh in on this discussion and see if we should alter the DatePickerDate shape per @tomivirkki 's comments or can we proceed with merging this PR as it stands? All other concerns have been addressed save this one.

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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the heavy lifting function that does all the work of deciding a date needs to be disabled. So all the components that need to disable a date end up feeding here.

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 @@ -599,7 +610,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 @@ -611,7 +623,7 @@ export const DatePickerMixin = (subclass) =>
}
}

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

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

Expand Down Expand Up @@ -930,9 +953,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 @@ -11,6 +11,7 @@ import { addListener, setTouchAction } from '@vaadin/component-base/src/gestures
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 { dateAllowed } from './vaadin-date-picker-helper.js';

/**
* @polymerMixin
Expand Down Expand Up @@ -107,6 +108,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 +146,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 +315,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 +333,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 +375,14 @@ export const DatePickerOverlayContentMixin = (superClass) =>
* @protected
*/
_selectDate(dateToSelect) {
if (!this._dateAllowed(dateToSelect)) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might need to discuss this change. I updated this to be a boolean function so that the keyboard Enter behavior could be prevented for disabled dates. That allows us to hover on disabled dates without allowing selection of those dates.

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 +793,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 +950,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)) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this one place we want to bypass any custom isDateDisabled function since we are allowed to focus the date, just not select it.

this.focusDate(dateToFocus, keepMonth);
} else if (this._dateAllowed(this.focusedDate)) {
// Move to min or max date
Expand Down Expand Up @@ -1009,18 +1029,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