@vaadin/date-picker 25.2.7 → 25.3.0-alpha10

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,47 @@ 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
+ * The date focused when the overlay opens is not moved if the provider reports it disabled. Use
311
+ * `initialPosition` to open on a date that can be selected.
312
+ *
313
+ * A value is checked against the provider even if the overlay is never opened, which loads the
314
+ * month holding it. Until that month answers the value is valid, and it is re-validated once the
315
+ * answer arrives, so `checkValidity()` can report a value as valid and then invalid.
316
+ *
317
+ * `part` from the metadata adds part names to the date, so a theme can style specific dates with
318
+ * `::part()` — e.g. `{ year, month, day, part: 'busy' }`. Give a single name or several separated
319
+ * by spaces. Do not use built-in names like `disabled` and `selected`.
320
+ *
321
+ * Keep a stable reference to the function. Assigning a new function clears the cache and
322
+ * re-fetches every visible range. To re-fetch while keeping the same function, because the data
323
+ * behind it changed, call `clearCache()`.
324
+ */
325
+ dateMetadataProvider: DatePickerDateMetadataProvider | null | undefined;
326
+
239
327
  /**
240
328
  * Opens the dropdown.
241
329
  */
@@ -245,4 +333,9 @@ export declare class DatePickerMixinClass {
245
333
  * Closes the dropdown.
246
334
  */
247
335
  close(): void;
336
+
337
+ /**
338
+ * Clears the `dateMetadataProvider` cache and reloads the date metadata.
339
+ */
340
+ clearCache(): void;
248
341
  }
@@ -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,51 @@ 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
+ * The date focused when the overlay opens is not moved if the provider reports it disabled.
240
+ * Use `initialPosition` to open on a date that can be selected.
241
+ *
242
+ * A value is checked against the provider even if the overlay is never opened, which loads the
243
+ * month holding it. Until that month answers the value is valid, and it is re-validated once
244
+ * the answer arrives, so `checkValidity()` can report a value as valid and then invalid.
245
+ *
246
+ * `part` from the metadata adds part names to the date, so a theme can style specific dates
247
+ * with `::part()` — e.g. `{ year, month, day, part: 'busy' }`. Give a single name or several
248
+ * separated by spaces. Do not use built-in names like `disabled` and `selected`.
249
+ *
250
+ * Keep a stable reference to the function. Assigning a new function clears the cache and
251
+ * re-fetches every visible range. To re-fetch while keeping the same function, because the
252
+ * data behind it changed, call `clearCache()`.
253
+ *
254
+ * @type {DatePickerDateMetadataProvider | null | undefined}
255
+ */
256
+ dateMetadataProvider: {
257
+ type: Function,
258
+ },
259
+
211
260
  /**
212
261
  * The earliest date that can be selected. All earlier dates will be disabled.
213
262
  * @type {Date | undefined}
@@ -272,7 +321,7 @@ export const DatePickerMixin = (subclass) =>
272
321
  }
273
322
 
274
323
  static get constraints() {
275
- return [...super.constraints, 'min', 'max'];
324
+ return [...super.constraints, 'min', 'max', 'dateMetadataProvider'];
276
325
  }
277
326
 
278
327
  constructor() {
@@ -280,6 +329,9 @@ export const DatePickerMixin = (subclass) =>
280
329
 
281
330
  this._boundOnClick = this._onClick.bind(this);
282
331
  this._boundOnScroll = this._onScroll.bind(this);
332
+
333
+ this._dateMetadataController = new DateMetadataController(this, () => this.__onDateMetadataChanged());
334
+ this.addController(this._dateMetadataController);
283
335
  }
284
336
 
285
337
  /**
@@ -322,6 +374,10 @@ export const DatePickerMixin = (subclass) =>
322
374
  * // Translation of the Cancel button text.
323
375
  * cancel: 'Cancel',
324
376
  *
377
+ * // Accessible name of the overlay content, announced by screen readers
378
+ * // when the overlay opens.
379
+ * dialogAccessibleName: 'Calendar',
380
+ *
325
381
  * // Used for adjusting the year value when parsing dates with short years.
326
382
  * // The year values between 0 and 99 are evaluated and adjusted.
327
383
  * // Example: for a referenceDate of 1970-10-30;
@@ -444,6 +500,11 @@ export const DatePickerMixin = (subclass) =>
444
500
  updated(props) {
445
501
  super.updated(props);
446
502
 
503
+ if (props.has('dateMetadataProvider')) {
504
+ this._dateMetadataController.setProvider(this.dateMetadataProvider);
505
+ this.__reloadDateMetadata();
506
+ }
507
+
447
508
  if (props.has('showWeekNumbers') || props.has('__effectiveI18n')) {
448
509
  // Currently only supported for locales that start the week on Monday.
449
510
  this.toggleAttribute('week-numbers', this.showWeekNumbers && this.__effectiveI18n.firstDayOfWeek === 1);
@@ -486,6 +547,28 @@ export const DatePickerMixin = (subclass) =>
486
547
  this.$.overlay.close();
487
548
  }
488
549
 
550
+ /**
551
+ * Clears the `dateMetadataProvider` cache and reloads the date metadata.
552
+ */
553
+ clearCache() {
554
+ this._dateMetadataController.clearCache();
555
+ this.__reloadDateMetadata();
556
+ }
557
+
558
+ /**
559
+ * Asks for what the dropped cache was holding: the months the overlay is showing, and the month
560
+ * of the value being validated. Requested from here rather than from the controller's
561
+ * notification, which would turn a provider that keeps failing into an endless retry, since a
562
+ * failed month is dropped and so becomes missing again.
563
+ * @private
564
+ */
565
+ __reloadDateMetadata() {
566
+ if (this.opened) {
567
+ this._overlayContent?.loadVisibleDateMetadata();
568
+ }
569
+ this.__ensureSelectedDateLoaded();
570
+ }
571
+
489
572
  /** @private */
490
573
  __ensureContent() {
491
574
  if (this._overlayContent) {
@@ -578,7 +661,14 @@ export const DatePickerMixin = (subclass) =>
578
661
  const inputValue = this._inputElementValue;
579
662
  const inputValid = !inputValue || (!!this._selectedDate && inputValue === this.__formatDate(this._selectedDate));
580
663
  const isDateValid =
581
- !this._selectedDate || dateAllowed(this._selectedDate, this._minDate, this._maxDate, this.isDateDisabled);
664
+ !this._selectedDate ||
665
+ dateSelectable(
666
+ this._selectedDate,
667
+ this._minDate,
668
+ this._maxDate,
669
+ this.isDateDisabled,
670
+ this._dateMetadataController,
671
+ );
582
672
 
583
673
  let inputValidity = true;
584
674
  if (this.inputElement && this.inputElement.checkValidity) {
@@ -588,6 +678,47 @@ export const DatePickerMixin = (subclass) =>
588
678
  return inputValid && isDateValid && inputValidity;
589
679
  }
590
680
 
681
+ /**
682
+ * Asks the controller for the month holding the selected date, so a value that was set or typed
683
+ * without ever opening the overlay is still checked against the provider. Validation is re-run
684
+ * from the host callback once the month resolves.
685
+ * @private
686
+ */
687
+ __ensureSelectedDateLoaded() {
688
+ const controller = this._dateMetadataController;
689
+ const awaiting = !!(controller?.provider && this._selectedDate && !controller.isMonthLoaded(this._selectedDate));
690
+ // Always assigned, so clearing the value or removing the provider while a request is in
691
+ // flight disarms the pending re-validation, and a later answer for some other month does not
692
+ // re-validate a value that never waited for it.
693
+ this.__awaitingProviderValidation = awaiting;
694
+ if (awaiting) {
695
+ controller.ensureRangeLoaded(this._selectedDate, this._selectedDate);
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Called by the date metadata controller, one microtask after its state changed
701
+ * and coalesced, so this never writes reactive state from inside an update. The
702
+ * rendered months refresh on their own because they subscribe to the controller.
703
+ * @private
704
+ */
705
+ __onDateMetadataChanged() {
706
+ const controller = this._dateMetadataController;
707
+
708
+ // Only the open overlay has a spinner to update and a today button to re-evaluate.
709
+ if (this._overlayContent) {
710
+ this._overlayContent.loading = controller.isLoading();
711
+ this._overlayContent.updateTodayButton();
712
+ }
713
+
714
+ // Runs whether or not the overlay was ever opened, which is the case this exists for: a value
715
+ // set or typed with the overlay closed is reported invalid as soon as its month answers.
716
+ if (this.__awaitingProviderValidation && this._selectedDate && controller.isMonthLoaded(this._selectedDate)) {
717
+ this.__awaitingProviderValidation = false;
718
+ this._requestValidation();
719
+ }
720
+ }
721
+
591
722
  /**
592
723
  * Override method inherited from `FocusMixin`
593
724
  * to not call `_setFocused(true)` when focus
@@ -775,6 +906,8 @@ export const DatePickerMixin = (subclass) =>
775
906
  this._ignoreFocusedDateChange = true;
776
907
  this._focusedDate = selectedDate;
777
908
  this._ignoreFocusedDateChange = false;
909
+
910
+ this.__ensureSelectedDateLoaded();
778
911
  }
779
912
 
780
913
  /** @private */
@@ -845,6 +978,8 @@ export const DatePickerMixin = (subclass) =>
845
978
  enteredDate,
846
979
  ) {
847
980
  if (overlayContent) {
981
+ // Reuse the date-picker's controller so the overlay shares the same cache.
982
+ overlayContent._dateMetadataController = this._dateMetadataController;
848
983
  overlayContent.i18n = effectiveI18n;
849
984
  overlayContent.label = label;
850
985
  overlayContent.minDate = minDate;
@@ -860,11 +995,7 @@ export const DatePickerMixin = (subclass) =>
860
995
  /** @private */
861
996
  __updateOverlayContentTheme(overlayContent, theme) {
862
997
  if (overlayContent) {
863
- if (theme) {
864
- overlayContent.setAttribute('theme', theme);
865
- } else {
866
- overlayContent.removeAttribute('theme');
867
- }
998
+ setOrRemoveAttribute(overlayContent, 'theme', theme);
868
999
  }
869
1000
  }
870
1001
 
@@ -964,6 +1095,8 @@ export const DatePickerMixin = (subclass) =>
964
1095
 
965
1096
  /** @protected */
966
1097
  _onOverlayClosed() {
1098
+ this._overlayContent?.cancelLoadVisibleDateMetadata();
1099
+
967
1100
  // Reset `aria-hidden` state.
968
1101
  if (this.__showOthers) {
969
1102
  this.__showOthers();