@vaadin/date-picker 25.3.0-alpha7 → 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.
@@ -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 || dateAllowed(this._selectedDate, this._minDate, this._maxDate, this.isDateDisabled);
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
- if (theme) {
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();