@vaadin/date-picker 25.3.0-alpha8 → 25.3.0-alpha9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +544 -9
- package/package.json +13 -13
- package/src/styles/vaadin-date-picker-overlay-content-base-styles.js +9 -0
- package/src/vaadin-date-metadata-controller.d.ts +94 -0
- package/src/vaadin-date-metadata-controller.js +276 -0
- package/src/vaadin-date-picker-helper.d.ts +17 -0
- package/src/vaadin-date-picker-helper.js +90 -5
- package/src/vaadin-date-picker-mixin.d.ts +90 -0
- package/src/vaadin-date-picker-mixin.js +137 -7
- package/src/vaadin-date-picker-overlay-content-mixin.js +150 -50
- package/src/vaadin-date-picker-overlay-content.js +4 -1
- package/src/vaadin-date-picker-year.js +3 -1
- package/src/vaadin-date-picker.d.ts +10 -1
- package/src/vaadin-date-picker.js +3 -0
- package/src/vaadin-month-calendar-mixin.js +55 -29
- package/web-types.json +36 -7
- package/web-types.lit.json +19 -5
|
@@ -18,6 +18,44 @@ export interface DatePickerDate {
|
|
|
18
18
|
year: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A range of dates the provider is asked about. Always covers whole months: `start` is the first
|
|
23
|
+
* day of a month and `end` the last day of a month, so a provider may group its query by month.
|
|
24
|
+
*/
|
|
25
|
+
export interface DatePickerDateRange {
|
|
26
|
+
/**
|
|
27
|
+
* The first date of the range (inclusive).
|
|
28
|
+
*/
|
|
29
|
+
start: DatePickerDate;
|
|
30
|
+
/**
|
|
31
|
+
* The last date of the range (inclusive).
|
|
32
|
+
*/
|
|
33
|
+
end: DatePickerDate;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Metadata resolved on demand for a single date.
|
|
38
|
+
*/
|
|
39
|
+
export interface DatePickerDateMetadata extends DatePickerDate {
|
|
40
|
+
/**
|
|
41
|
+
* Whether the date cannot be selected.
|
|
42
|
+
*/
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Part names to add to the date, so a theme can style it with `::part()`. A single name, or
|
|
46
|
+
* several separated by spaces. Do not use built-in names like `disabled` and `selected`.
|
|
47
|
+
*/
|
|
48
|
+
part?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A function called with the range of dates the calendar is about to show, returning or resolving
|
|
53
|
+
* with the metadata for the dates in that range.
|
|
54
|
+
*/
|
|
55
|
+
export type DatePickerDateMetadataProvider = (
|
|
56
|
+
range: DatePickerDateRange,
|
|
57
|
+
) => DatePickerDateMetadata[] | Promise<DatePickerDateMetadata[] | null | undefined> | null | undefined;
|
|
58
|
+
|
|
21
59
|
export interface DatePickerI18n {
|
|
22
60
|
/**
|
|
23
61
|
* An array with the full names of months starting
|
|
@@ -47,6 +85,11 @@ export interface DatePickerI18n {
|
|
|
47
85
|
* Translation of the Cancel button text.
|
|
48
86
|
*/
|
|
49
87
|
cancel?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Accessible name of the overlay content, announced by screen readers when
|
|
90
|
+
* the overlay opens.
|
|
91
|
+
*/
|
|
92
|
+
dialogAccessibleName?: string;
|
|
50
93
|
/**
|
|
51
94
|
* Used for adjusting the year value when parsing dates with short years.
|
|
52
95
|
* The year values between 0 and 99 are evaluated and adjusted.
|
|
@@ -174,6 +217,10 @@ export declare class DatePickerMixinClass {
|
|
|
174
217
|
* // Translation of the Cancel button text.
|
|
175
218
|
* cancel: 'Cancel',
|
|
176
219
|
*
|
|
220
|
+
* // Accessible name of the overlay content, announced by screen readers
|
|
221
|
+
* // when the overlay opens.
|
|
222
|
+
* dialogAccessibleName: 'Calendar',
|
|
223
|
+
*
|
|
177
224
|
* // Used for adjusting the year value when parsing dates with short years.
|
|
178
225
|
* // The year values between 0 and 99 are evaluated and adjusted.
|
|
179
226
|
* // Example: for a referenceDate of 1970-10-30;
|
|
@@ -236,6 +283,44 @@ export declare class DatePickerMixinClass {
|
|
|
236
283
|
*/
|
|
237
284
|
isDateDisabled: (date: DatePickerDate) => boolean;
|
|
238
285
|
|
|
286
|
+
/**
|
|
287
|
+
* A batch function that fetches metadata for a range of dates the calendar is about to
|
|
288
|
+
* render. It receives a `DatePickerDateRange` and returns, or resolves with, an array of
|
|
289
|
+
* `DatePickerDateMetadata` objects — a `DatePickerDate` extended with metadata such as
|
|
290
|
+
* `disabled`, e.g. `{ year, month, day, disabled: true }` — for the dates that have metadata
|
|
291
|
+
* within that range. Dates it does not mention have no metadata. `month` is 0-based: 0 is
|
|
292
|
+
* January and 11 is December.
|
|
293
|
+
*
|
|
294
|
+
* Unlike `isDateDisabled`, which is called once per date, this function is called for a range of
|
|
295
|
+
* dates at a time, and again as the calendar renders further dates. The size of the range is
|
|
296
|
+
* decided by the calendar and may span several months, and may include months it already has
|
|
297
|
+
* metadata for, whose entries are then ignored.
|
|
298
|
+
*
|
|
299
|
+
* It may return a `Promise`, so the answer can come from a server. Until it resolves, the
|
|
300
|
+
* affected dates render with the `loading` part but stay selectable, and a loading spinner is
|
|
301
|
+
* shown. Nothing is disabled before the provider has actually reported it, so a slow provider
|
|
302
|
+
* does not make the calendar unusable. If it throws or rejects, the error is logged and the
|
|
303
|
+
* affected months are requested again the next time the user navigates.
|
|
304
|
+
*
|
|
305
|
+
* `disabled` from the metadata is combined with `min`, `max` and `isDateDisabled`: a date is
|
|
306
|
+
* disabled if it is out of the min/max range, or `isDateDisabled` returns `true`, or its metadata
|
|
307
|
+
* marks it disabled. That decides what the calendar renders as disabled, what can be selected, and
|
|
308
|
+
* whether the field is valid.
|
|
309
|
+
*
|
|
310
|
+
* A value is checked against the provider even if the overlay is never opened, which loads the
|
|
311
|
+
* month holding it. Until that month answers the value is valid, and it is re-validated once the
|
|
312
|
+
* answer arrives, so `checkValidity()` can report a value as valid and then invalid.
|
|
313
|
+
*
|
|
314
|
+
* `part` from the metadata adds part names to the date, so a theme can style specific dates with
|
|
315
|
+
* `::part()` — e.g. `{ year, month, day, part: 'busy' }`. Give a single name or several separated
|
|
316
|
+
* by spaces. Do not use built-in names like `disabled` and `selected`.
|
|
317
|
+
*
|
|
318
|
+
* Keep a stable reference to the function. Assigning a new function clears the cache and
|
|
319
|
+
* re-fetches every visible range. To re-fetch while keeping the same function, because the data
|
|
320
|
+
* behind it changed, call `clearCache()`.
|
|
321
|
+
*/
|
|
322
|
+
dateMetadataProvider: DatePickerDateMetadataProvider | null | undefined;
|
|
323
|
+
|
|
239
324
|
/**
|
|
240
325
|
* Opens the dropdown.
|
|
241
326
|
*/
|
|
@@ -245,4 +330,9 @@ export declare class DatePickerMixinClass {
|
|
|
245
330
|
* Closes the dropdown.
|
|
246
331
|
*/
|
|
247
332
|
close(): void;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Clears the `dateMetadataProvider` cache and reloads the date metadata.
|
|
336
|
+
*/
|
|
337
|
+
clearCache(): void;
|
|
248
338
|
}
|
|
@@ -8,13 +8,16 @@ import { DelegateFocusMixin } from '@vaadin/a11y-base/src/delegate-focus-mixin.j
|
|
|
8
8
|
import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
9
9
|
import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
|
|
10
10
|
import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
|
|
11
|
+
import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
|
|
11
12
|
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
|
|
12
13
|
import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
|
|
13
14
|
import { InputConstraintsMixin } from '@vaadin/field-base/src/input-constraints-mixin.js';
|
|
14
15
|
import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
|
|
16
|
+
import { DateMetadataController } from './vaadin-date-metadata-controller.js';
|
|
15
17
|
import {
|
|
16
18
|
dateAllowed,
|
|
17
19
|
dateEquals,
|
|
20
|
+
dateSelectable,
|
|
18
21
|
extractDateParts,
|
|
19
22
|
formatISODate,
|
|
20
23
|
getAdjustedYear,
|
|
@@ -42,6 +45,7 @@ export const datePickerI18nDefaults = Object.freeze({
|
|
|
42
45
|
firstDayOfWeek: 0,
|
|
43
46
|
today: 'Today',
|
|
44
47
|
cancel: 'Cancel',
|
|
48
|
+
dialogAccessibleName: 'Calendar',
|
|
45
49
|
referenceDate: '',
|
|
46
50
|
formatDate(d) {
|
|
47
51
|
const yearStr = String(d.year).replace(/\d+/u, (y) => '0000'.substr(y.length) + y);
|
|
@@ -208,6 +212,48 @@ export const DatePickerMixin = (subclass) =>
|
|
|
208
212
|
type: Function,
|
|
209
213
|
},
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* A batch function that fetches metadata for a range of dates the calendar is about to
|
|
217
|
+
* render. It receives a `DatePickerDateRange` and returns, or resolves with, an array of
|
|
218
|
+
* `DatePickerDateMetadata` objects — a `DatePickerDate` extended with metadata such as
|
|
219
|
+
* `disabled`, e.g. `{ year, month, day, disabled: true }` — for the dates that have metadata
|
|
220
|
+
* within that range. Dates it does not mention have no metadata. `month` is 0-based: 0 is
|
|
221
|
+
* January and 11 is December.
|
|
222
|
+
*
|
|
223
|
+
* Unlike `isDateDisabled`, which is called once per date, this function is called for a
|
|
224
|
+
* range of dates at a time, and again as the calendar renders further dates. The size of the
|
|
225
|
+
* range is decided by the calendar and may span several months, and may include months it
|
|
226
|
+
* already has metadata for, whose entries are then ignored.
|
|
227
|
+
*
|
|
228
|
+
* It may return a `Promise`, so the answer can come from a server. Until it resolves, the
|
|
229
|
+
* affected dates render with the `loading` part but stay selectable, and a loading spinner
|
|
230
|
+
* is shown. Nothing is disabled before the provider has actually reported it, so a slow
|
|
231
|
+
* provider does not make the calendar unusable. If it throws or rejects, the error is logged
|
|
232
|
+
* and the affected months are requested again the next time the user navigates.
|
|
233
|
+
*
|
|
234
|
+
* `disabled` from the metadata is combined with `min`, `max` and `isDateDisabled`: a date is
|
|
235
|
+
* disabled if it is out of the min/max range, or `isDateDisabled` returns `true`, or its
|
|
236
|
+
* metadata marks it disabled. That decides what the calendar renders as disabled, what can be
|
|
237
|
+
* selected, and whether the field is valid.
|
|
238
|
+
*
|
|
239
|
+
* A value is checked against the provider even if the overlay is never opened, which loads the
|
|
240
|
+
* month holding it. Until that month answers the value is valid, and it is re-validated once
|
|
241
|
+
* the answer arrives, so `checkValidity()` can report a value as valid and then invalid.
|
|
242
|
+
*
|
|
243
|
+
* `part` from the metadata adds part names to the date, so a theme can style specific dates
|
|
244
|
+
* with `::part()` — e.g. `{ year, month, day, part: 'busy' }`. Give a single name or several
|
|
245
|
+
* separated by spaces. Do not use built-in names like `disabled` and `selected`.
|
|
246
|
+
*
|
|
247
|
+
* Keep a stable reference to the function. Assigning a new function clears the cache and
|
|
248
|
+
* re-fetches every visible range. To re-fetch while keeping the same function, because the
|
|
249
|
+
* data behind it changed, call `clearCache()`.
|
|
250
|
+
*
|
|
251
|
+
* @type {DatePickerDateMetadataProvider | null | undefined}
|
|
252
|
+
*/
|
|
253
|
+
dateMetadataProvider: {
|
|
254
|
+
type: Function,
|
|
255
|
+
},
|
|
256
|
+
|
|
211
257
|
/**
|
|
212
258
|
* The earliest date that can be selected. All earlier dates will be disabled.
|
|
213
259
|
* @type {Date | undefined}
|
|
@@ -272,7 +318,7 @@ export const DatePickerMixin = (subclass) =>
|
|
|
272
318
|
}
|
|
273
319
|
|
|
274
320
|
static get constraints() {
|
|
275
|
-
return [...super.constraints, 'min', 'max'];
|
|
321
|
+
return [...super.constraints, 'min', 'max', 'dateMetadataProvider'];
|
|
276
322
|
}
|
|
277
323
|
|
|
278
324
|
constructor() {
|
|
@@ -280,6 +326,9 @@ export const DatePickerMixin = (subclass) =>
|
|
|
280
326
|
|
|
281
327
|
this._boundOnClick = this._onClick.bind(this);
|
|
282
328
|
this._boundOnScroll = this._onScroll.bind(this);
|
|
329
|
+
|
|
330
|
+
this._dateMetadataController = new DateMetadataController(this, () => this.__onDateMetadataChanged());
|
|
331
|
+
this.addController(this._dateMetadataController);
|
|
283
332
|
}
|
|
284
333
|
|
|
285
334
|
/**
|
|
@@ -322,6 +371,10 @@ export const DatePickerMixin = (subclass) =>
|
|
|
322
371
|
* // Translation of the Cancel button text.
|
|
323
372
|
* cancel: 'Cancel',
|
|
324
373
|
*
|
|
374
|
+
* // Accessible name of the overlay content, announced by screen readers
|
|
375
|
+
* // when the overlay opens.
|
|
376
|
+
* dialogAccessibleName: 'Calendar',
|
|
377
|
+
*
|
|
325
378
|
* // Used for adjusting the year value when parsing dates with short years.
|
|
326
379
|
* // The year values between 0 and 99 are evaluated and adjusted.
|
|
327
380
|
* // Example: for a referenceDate of 1970-10-30;
|
|
@@ -444,6 +497,11 @@ export const DatePickerMixin = (subclass) =>
|
|
|
444
497
|
updated(props) {
|
|
445
498
|
super.updated(props);
|
|
446
499
|
|
|
500
|
+
if (props.has('dateMetadataProvider')) {
|
|
501
|
+
this._dateMetadataController.setProvider(this.dateMetadataProvider);
|
|
502
|
+
this.__reloadDateMetadata();
|
|
503
|
+
}
|
|
504
|
+
|
|
447
505
|
if (props.has('showWeekNumbers') || props.has('__effectiveI18n')) {
|
|
448
506
|
// Currently only supported for locales that start the week on Monday.
|
|
449
507
|
this.toggleAttribute('week-numbers', this.showWeekNumbers && this.__effectiveI18n.firstDayOfWeek === 1);
|
|
@@ -486,6 +544,28 @@ export const DatePickerMixin = (subclass) =>
|
|
|
486
544
|
this.$.overlay.close();
|
|
487
545
|
}
|
|
488
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Clears the `dateMetadataProvider` cache and reloads the date metadata.
|
|
549
|
+
*/
|
|
550
|
+
clearCache() {
|
|
551
|
+
this._dateMetadataController.clearCache();
|
|
552
|
+
this.__reloadDateMetadata();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Asks for what the dropped cache was holding: the months the overlay is showing, and the month
|
|
557
|
+
* of the value being validated. Requested from here rather than from the controller's
|
|
558
|
+
* notification, which would turn a provider that keeps failing into an endless retry, since a
|
|
559
|
+
* failed month is dropped and so becomes missing again.
|
|
560
|
+
* @private
|
|
561
|
+
*/
|
|
562
|
+
__reloadDateMetadata() {
|
|
563
|
+
if (this.opened) {
|
|
564
|
+
this._overlayContent?.loadVisibleDateMetadata();
|
|
565
|
+
}
|
|
566
|
+
this.__ensureSelectedDateLoaded();
|
|
567
|
+
}
|
|
568
|
+
|
|
489
569
|
/** @private */
|
|
490
570
|
__ensureContent() {
|
|
491
571
|
if (this._overlayContent) {
|
|
@@ -578,7 +658,14 @@ export const DatePickerMixin = (subclass) =>
|
|
|
578
658
|
const inputValue = this._inputElementValue;
|
|
579
659
|
const inputValid = !inputValue || (!!this._selectedDate && inputValue === this.__formatDate(this._selectedDate));
|
|
580
660
|
const isDateValid =
|
|
581
|
-
!this._selectedDate ||
|
|
661
|
+
!this._selectedDate ||
|
|
662
|
+
dateSelectable(
|
|
663
|
+
this._selectedDate,
|
|
664
|
+
this._minDate,
|
|
665
|
+
this._maxDate,
|
|
666
|
+
this.isDateDisabled,
|
|
667
|
+
this._dateMetadataController,
|
|
668
|
+
);
|
|
582
669
|
|
|
583
670
|
let inputValidity = true;
|
|
584
671
|
if (this.inputElement && this.inputElement.checkValidity) {
|
|
@@ -588,6 +675,47 @@ export const DatePickerMixin = (subclass) =>
|
|
|
588
675
|
return inputValid && isDateValid && inputValidity;
|
|
589
676
|
}
|
|
590
677
|
|
|
678
|
+
/**
|
|
679
|
+
* Asks the controller for the month holding the selected date, so a value that was set or typed
|
|
680
|
+
* without ever opening the overlay is still checked against the provider. Validation is re-run
|
|
681
|
+
* from the host callback once the month resolves.
|
|
682
|
+
* @private
|
|
683
|
+
*/
|
|
684
|
+
__ensureSelectedDateLoaded() {
|
|
685
|
+
const controller = this._dateMetadataController;
|
|
686
|
+
const awaiting = !!(controller?.provider && this._selectedDate && !controller.isMonthLoaded(this._selectedDate));
|
|
687
|
+
// Always assigned, so clearing the value or removing the provider while a request is in
|
|
688
|
+
// flight disarms the pending re-validation, and a later answer for some other month does not
|
|
689
|
+
// re-validate a value that never waited for it.
|
|
690
|
+
this.__awaitingProviderValidation = awaiting;
|
|
691
|
+
if (awaiting) {
|
|
692
|
+
controller.ensureRangeLoaded(this._selectedDate, this._selectedDate);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Called by the date metadata controller, one microtask after its state changed
|
|
698
|
+
* and coalesced, so this never writes reactive state from inside an update. The
|
|
699
|
+
* rendered months refresh on their own because they subscribe to the controller.
|
|
700
|
+
* @private
|
|
701
|
+
*/
|
|
702
|
+
__onDateMetadataChanged() {
|
|
703
|
+
const controller = this._dateMetadataController;
|
|
704
|
+
|
|
705
|
+
// Only the open overlay has a spinner to update and a today button to re-evaluate.
|
|
706
|
+
if (this._overlayContent) {
|
|
707
|
+
this._overlayContent.loading = controller.isLoading();
|
|
708
|
+
this._overlayContent.updateTodayButton();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Runs whether or not the overlay was ever opened, which is the case this exists for: a value
|
|
712
|
+
// set or typed with the overlay closed is reported invalid as soon as its month answers.
|
|
713
|
+
if (this.__awaitingProviderValidation && this._selectedDate && controller.isMonthLoaded(this._selectedDate)) {
|
|
714
|
+
this.__awaitingProviderValidation = false;
|
|
715
|
+
this._requestValidation();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
591
719
|
/**
|
|
592
720
|
* Override method inherited from `FocusMixin`
|
|
593
721
|
* to not call `_setFocused(true)` when focus
|
|
@@ -775,6 +903,8 @@ export const DatePickerMixin = (subclass) =>
|
|
|
775
903
|
this._ignoreFocusedDateChange = true;
|
|
776
904
|
this._focusedDate = selectedDate;
|
|
777
905
|
this._ignoreFocusedDateChange = false;
|
|
906
|
+
|
|
907
|
+
this.__ensureSelectedDateLoaded();
|
|
778
908
|
}
|
|
779
909
|
|
|
780
910
|
/** @private */
|
|
@@ -845,6 +975,8 @@ export const DatePickerMixin = (subclass) =>
|
|
|
845
975
|
enteredDate,
|
|
846
976
|
) {
|
|
847
977
|
if (overlayContent) {
|
|
978
|
+
// Reuse the date-picker's controller so the overlay shares the same cache.
|
|
979
|
+
overlayContent._dateMetadataController = this._dateMetadataController;
|
|
848
980
|
overlayContent.i18n = effectiveI18n;
|
|
849
981
|
overlayContent.label = label;
|
|
850
982
|
overlayContent.minDate = minDate;
|
|
@@ -860,11 +992,7 @@ export const DatePickerMixin = (subclass) =>
|
|
|
860
992
|
/** @private */
|
|
861
993
|
__updateOverlayContentTheme(overlayContent, theme) {
|
|
862
994
|
if (overlayContent) {
|
|
863
|
-
|
|
864
|
-
overlayContent.setAttribute('theme', theme);
|
|
865
|
-
} else {
|
|
866
|
-
overlayContent.removeAttribute('theme');
|
|
867
|
-
}
|
|
995
|
+
setOrRemoveAttribute(overlayContent, 'theme', theme);
|
|
868
996
|
}
|
|
869
997
|
}
|
|
870
998
|
|
|
@@ -964,6 +1092,8 @@ export const DatePickerMixin = (subclass) =>
|
|
|
964
1092
|
|
|
965
1093
|
/** @protected */
|
|
966
1094
|
_onOverlayClosed() {
|
|
1095
|
+
this._overlayContent?.cancelLoadVisibleDateMetadata();
|
|
1096
|
+
|
|
967
1097
|
// Reset `aria-hidden` state.
|
|
968
1098
|
if (this.__showOthers) {
|
|
969
1099
|
this.__showOthers();
|
|
@@ -5,10 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { timeOut } from '@vaadin/component-base/src/async.js';
|
|
7
7
|
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
|
8
|
+
import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
|
|
8
9
|
import { addListener } from '@vaadin/component-base/src/gestures.js';
|
|
9
10
|
import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
|
|
10
11
|
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createDate,
|
|
14
|
+
dateAfterXMonths,
|
|
15
|
+
dateAllowed,
|
|
16
|
+
dateEquals,
|
|
17
|
+
dateSelectable,
|
|
18
|
+
firstOfMonth,
|
|
19
|
+
getClosestDate,
|
|
20
|
+
lastOfMonth,
|
|
21
|
+
monthDate,
|
|
22
|
+
monthIndex,
|
|
23
|
+
} from './vaadin-date-picker-helper.js';
|
|
12
24
|
|
|
13
25
|
export const DatePickerOverlayContentMixin = (superClass) =>
|
|
14
26
|
class DatePickerOverlayContentMixin extends superClass {
|
|
@@ -106,6 +118,17 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
106
118
|
type: Function,
|
|
107
119
|
},
|
|
108
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Reflected while data is being loaded, so the overlay can show a loading spinner.
|
|
123
|
+
* Currently set while the date metadata provider is resolving.
|
|
124
|
+
* @protected
|
|
125
|
+
*/
|
|
126
|
+
loading: {
|
|
127
|
+
type: Boolean,
|
|
128
|
+
value: false,
|
|
129
|
+
reflectToAttribute: true,
|
|
130
|
+
},
|
|
131
|
+
|
|
109
132
|
enteredDate: {
|
|
110
133
|
type: Date,
|
|
111
134
|
sync: true,
|
|
@@ -124,6 +147,18 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
124
147
|
type: Object,
|
|
125
148
|
},
|
|
126
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The date-picker's date metadata controller, assigned by the host. Declared as a property,
|
|
152
|
+
* and a dependency of `__updateCalendarsConfig` below, so that the calendars are handed it
|
|
153
|
+
* and subscribed whenever it arrives, rather than relying on the host assigning it before
|
|
154
|
+
* the properties that happen to trigger that observer.
|
|
155
|
+
* @protected
|
|
156
|
+
*/
|
|
157
|
+
_dateMetadataController: {
|
|
158
|
+
type: Object,
|
|
159
|
+
sync: true,
|
|
160
|
+
},
|
|
161
|
+
|
|
127
162
|
calendars: {
|
|
128
163
|
type: Array,
|
|
129
164
|
value: () => [],
|
|
@@ -138,14 +173,47 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
138
173
|
|
|
139
174
|
static get observers() {
|
|
140
175
|
return [
|
|
141
|
-
'__updateCalendarsConfig(calendars, i18n, minDate, maxDate, showWeekNumbers, isDateDisabled, _theme)',
|
|
176
|
+
'__updateCalendarsConfig(calendars, i18n, minDate, maxDate, showWeekNumbers, isDateDisabled, _theme, _dateMetadataController)',
|
|
142
177
|
'__updateCalendarsState(calendars, selectedDate, focusedDate, enteredDate, _ignoreTaps)',
|
|
143
178
|
'__updateCancelButton(_cancelButton, i18n)',
|
|
144
|
-
'__updateTodayButton(_todayButton, i18n, minDate, maxDate, isDateDisabled)',
|
|
145
179
|
'__updateYears(years, selectedDate, _theme)',
|
|
146
180
|
];
|
|
147
181
|
}
|
|
148
182
|
|
|
183
|
+
/** @protected */
|
|
184
|
+
disconnectedCallback() {
|
|
185
|
+
super.disconnectedCallback();
|
|
186
|
+
|
|
187
|
+
this.cancelLoadVisibleDateMetadata();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @protected */
|
|
191
|
+
updated(props) {
|
|
192
|
+
super.updated(props);
|
|
193
|
+
|
|
194
|
+
if (props.has('loading')) {
|
|
195
|
+
setOrRemoveAttribute(this, 'aria-busy', this.loading);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (props.has('i18n')) {
|
|
199
|
+
setOrRemoveAttribute(this, 'aria-label', this.i18n?.dialogAccessibleName);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (props.has('calendars') || props.has('_dateMetadataController')) {
|
|
203
|
+
this.loadVisibleDateMetadata();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (
|
|
207
|
+
props.has('_todayButton') ||
|
|
208
|
+
props.has('i18n') ||
|
|
209
|
+
props.has('minDate') ||
|
|
210
|
+
props.has('maxDate') ||
|
|
211
|
+
props.has('isDateDisabled')
|
|
212
|
+
) {
|
|
213
|
+
this.updateTodayButton();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
149
217
|
/**
|
|
150
218
|
* Whether to scroll to a sub-month position when scrolling to a date.
|
|
151
219
|
* This is active if the month scroller is not large enough to fit a
|
|
@@ -292,14 +360,44 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
292
360
|
}
|
|
293
361
|
}
|
|
294
362
|
|
|
295
|
-
/**
|
|
296
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Applies the today button's label and whether today can be selected.
|
|
365
|
+
*/
|
|
366
|
+
updateTodayButton() {
|
|
367
|
+
const todayButton = this._todayButton;
|
|
297
368
|
if (todayButton) {
|
|
298
|
-
todayButton.textContent = i18n?.today;
|
|
299
|
-
todayButton.disabled = !this._isTodayAllowed(
|
|
369
|
+
todayButton.textContent = this.i18n?.today;
|
|
370
|
+
todayButton.disabled = !this._isTodayAllowed();
|
|
300
371
|
}
|
|
301
372
|
}
|
|
302
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Requests the date metadata for the months currently rendered. Months already loaded or in flight
|
|
376
|
+
* are skipped.
|
|
377
|
+
*/
|
|
378
|
+
loadVisibleDateMetadata() {
|
|
379
|
+
const controller = this._dateMetadataController;
|
|
380
|
+
if (!controller) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Reduced to month indexes so the outermost months can be picked with plain arithmetic.
|
|
384
|
+
const indexes = (this.calendars ?? [])
|
|
385
|
+
.map((calendar) => calendar.month)
|
|
386
|
+
.filter(Boolean)
|
|
387
|
+
.map((month) => monthIndex(month));
|
|
388
|
+
if (indexes.length === 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
controller.ensureRangeLoaded(monthDate(Math.min(...indexes)), monthDate(Math.max(...indexes)));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Drops a date metadata load that navigating has scheduled but that has not run yet.
|
|
396
|
+
*/
|
|
397
|
+
cancelLoadVisibleDateMetadata() {
|
|
398
|
+
this._loadDateMetadataDebouncer?.cancel();
|
|
399
|
+
}
|
|
400
|
+
|
|
303
401
|
/**
|
|
304
402
|
* Config that changes rarely (locale, allowed range, week numbers, theme).
|
|
305
403
|
* Split from the interaction state below so a keyboard/selection update
|
|
@@ -307,24 +405,43 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
307
405
|
* @private
|
|
308
406
|
*/
|
|
309
407
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
310
|
-
__updateCalendarsConfig(
|
|
408
|
+
__updateCalendarsConfig(
|
|
409
|
+
calendars,
|
|
410
|
+
i18n,
|
|
411
|
+
minDate,
|
|
412
|
+
maxDate,
|
|
413
|
+
showWeekNumbers,
|
|
414
|
+
isDateDisabled,
|
|
415
|
+
theme,
|
|
416
|
+
dateMetadataController,
|
|
417
|
+
) {
|
|
311
418
|
if (calendars?.length) {
|
|
312
419
|
calendars.forEach((calendar) => {
|
|
313
420
|
calendar.i18n = i18n;
|
|
314
421
|
calendar.minDate = minDate;
|
|
315
422
|
calendar.maxDate = maxDate;
|
|
316
423
|
calendar.isDateDisabled = isDateDisabled;
|
|
424
|
+
calendar._dateMetadataController = dateMetadataController;
|
|
425
|
+
// Subscribe to controller updates (subscribing again is a no-op).
|
|
426
|
+
dateMetadataController?.subscribe(calendar);
|
|
317
427
|
calendar.showWeekNumbers = showWeekNumbers;
|
|
318
428
|
|
|
319
|
-
|
|
320
|
-
calendar.setAttribute('theme', theme);
|
|
321
|
-
} else {
|
|
322
|
-
calendar.removeAttribute('theme');
|
|
323
|
-
}
|
|
429
|
+
setOrRemoveAttribute(calendar, 'theme', theme);
|
|
324
430
|
});
|
|
325
431
|
}
|
|
326
432
|
}
|
|
327
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Debounced variant used while navigating, so a continuous scroll triggers one load once it
|
|
436
|
+
* settles instead of a request per intermediate position.
|
|
437
|
+
* @private
|
|
438
|
+
*/
|
|
439
|
+
__scheduleLoadVisibleDateMetadata() {
|
|
440
|
+
this._loadDateMetadataDebouncer = Debouncer.debounce(this._loadDateMetadataDebouncer, timeOut.after(200), () =>
|
|
441
|
+
this.loadVisibleDateMetadata(),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
328
445
|
/**
|
|
329
446
|
* Interaction state that changes often (focus, selection, entered date, taps).
|
|
330
447
|
* @private
|
|
@@ -346,11 +463,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
346
463
|
years.forEach((year) => {
|
|
347
464
|
year.selectedDate = selectedDate;
|
|
348
465
|
|
|
349
|
-
|
|
350
|
-
year.setAttribute('theme', theme);
|
|
351
|
-
} else {
|
|
352
|
-
year.removeAttribute('theme');
|
|
353
|
-
}
|
|
466
|
+
setOrRemoveAttribute(year, 'theme', theme);
|
|
354
467
|
});
|
|
355
468
|
}
|
|
356
469
|
}
|
|
@@ -360,7 +473,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
360
473
|
* @protected
|
|
361
474
|
*/
|
|
362
475
|
_selectDate(dateToSelect) {
|
|
363
|
-
if (!this.
|
|
476
|
+
if (!this._dateSelectable(dateToSelect)) {
|
|
364
477
|
return false;
|
|
365
478
|
}
|
|
366
479
|
this.selectedDate = dateToSelect;
|
|
@@ -426,11 +539,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
426
539
|
* @private
|
|
427
540
|
*/
|
|
428
541
|
_calculateWeekScrollOffset(date) {
|
|
429
|
-
|
|
430
|
-
const temp = new Date(0, 0);
|
|
431
|
-
temp.setFullYear(date.getFullYear());
|
|
432
|
-
temp.setMonth(date.getMonth());
|
|
433
|
-
temp.setDate(1);
|
|
542
|
+
const temp = firstOfMonth(date);
|
|
434
543
|
// Determine week (=row index) of date within the month
|
|
435
544
|
let week = 0;
|
|
436
545
|
while (temp.getDate() < date.getDate()) {
|
|
@@ -458,12 +567,14 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
458
567
|
const monthPosition = this._monthScroller.position;
|
|
459
568
|
this._visibleMonthIndex = Math.floor(monthPosition);
|
|
460
569
|
this._yearScroller.position = (monthPosition + this._originDate.getMonth()) / 12;
|
|
570
|
+
this.__scheduleLoadVisibleDateMetadata();
|
|
461
571
|
}
|
|
462
572
|
|
|
463
573
|
/** @private */
|
|
464
574
|
_repositionMonthScroller() {
|
|
465
575
|
this._monthScroller.position = this._yearScroller.position * 12 - this._originDate.getMonth();
|
|
466
576
|
this._visibleMonthIndex = Math.floor(this._monthScroller.position);
|
|
577
|
+
this.__scheduleLoadVisibleDateMetadata();
|
|
467
578
|
}
|
|
468
579
|
|
|
469
580
|
/** @private */
|
|
@@ -630,8 +741,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
630
741
|
|
|
631
742
|
/** @private */
|
|
632
743
|
_differenceInMonths(date1, date2) {
|
|
633
|
-
|
|
634
|
-
return months - date2.getMonth() + date1.getMonth();
|
|
744
|
+
return monthIndex(date1) - monthIndex(date2);
|
|
635
745
|
}
|
|
636
746
|
|
|
637
747
|
/** @private */
|
|
@@ -859,13 +969,11 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
859
969
|
|
|
860
970
|
/** @private */
|
|
861
971
|
_getDateDiff(months, days) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
return date;
|
|
972
|
+
return createDate(
|
|
973
|
+
this.focusedDate.getFullYear(),
|
|
974
|
+
this.focusedDate.getMonth() + months,
|
|
975
|
+
days ? this.focusedDate.getDate() + days : 1,
|
|
976
|
+
);
|
|
869
977
|
}
|
|
870
978
|
|
|
871
979
|
/** @private */
|
|
@@ -895,16 +1003,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
895
1003
|
|
|
896
1004
|
/** @private */
|
|
897
1005
|
_moveFocusInsideMonth(focusedDate, property) {
|
|
898
|
-
const dateToFocus =
|
|
899
|
-
dateToFocus.setFullYear(focusedDate.getFullYear());
|
|
900
|
-
|
|
901
|
-
if (property === 'minDate') {
|
|
902
|
-
dateToFocus.setMonth(focusedDate.getMonth());
|
|
903
|
-
dateToFocus.setDate(1);
|
|
904
|
-
} else {
|
|
905
|
-
dateToFocus.setMonth(focusedDate.getMonth() + 1);
|
|
906
|
-
dateToFocus.setDate(0);
|
|
907
|
-
}
|
|
1006
|
+
const dateToFocus = property === 'minDate' ? firstOfMonth(focusedDate) : lastOfMonth(focusedDate);
|
|
908
1007
|
|
|
909
1008
|
if (this._dateAllowed(dateToFocus)) {
|
|
910
1009
|
this.focusDate(dateToFocus);
|
|
@@ -923,17 +1022,18 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
923
1022
|
}
|
|
924
1023
|
|
|
925
1024
|
/** @private */
|
|
926
|
-
|
|
927
|
-
return
|
|
1025
|
+
_dateSelectable(date) {
|
|
1026
|
+
return dateSelectable(date, this.minDate, this.maxDate, this.isDateDisabled, this._dateMetadataController);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/** @private */
|
|
1030
|
+
_isTodayAllowed() {
|
|
1031
|
+
return this._dateSelectable(this._getTodayMidnight());
|
|
928
1032
|
}
|
|
929
1033
|
|
|
930
1034
|
/** @private */
|
|
931
1035
|
_getTodayMidnight() {
|
|
932
1036
|
const today = new Date();
|
|
933
|
-
|
|
934
|
-
todayMidnight.setFullYear(today.getFullYear());
|
|
935
|
-
todayMidnight.setMonth(today.getMonth());
|
|
936
|
-
todayMidnight.setDate(today.getDate());
|
|
937
|
-
return todayMidnight;
|
|
1037
|
+
return createDate(today.getFullYear(), today.getMonth(), today.getDate());
|
|
938
1038
|
}
|
|
939
1039
|
};
|