@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.
@@ -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 { dateAfterXMonths, dateAllowed, dateEquals, getClosestDate } from './vaadin-date-picker-helper.js';
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,13 +173,47 @@ export const DatePickerOverlayContentMixin = (superClass) =>
138
173
 
139
174
  static get observers() {
140
175
  return [
141
- '__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme, isDateDisabled, enteredDate)',
176
+ '__updateCalendarsConfig(calendars, i18n, minDate, maxDate, showWeekNumbers, isDateDisabled, _theme, _dateMetadataController)',
177
+ '__updateCalendarsState(calendars, selectedDate, focusedDate, enteredDate, _ignoreTaps)',
142
178
  '__updateCancelButton(_cancelButton, i18n)',
143
- '__updateTodayButton(_todayButton, i18n, minDate, maxDate, isDateDisabled)',
144
179
  '__updateYears(years, selectedDate, _theme)',
145
180
  ];
146
181
  }
147
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
+
148
217
  /**
149
218
  * Whether to scroll to a sub-month position when scrolling to a date.
150
219
  * This is active if the month scroller is not large enough to fit a
@@ -291,27 +360,60 @@ export const DatePickerOverlayContentMixin = (superClass) =>
291
360
  }
292
361
  }
293
362
 
294
- /** @private */
295
- __updateTodayButton(todayButton, i18n, minDate, maxDate, isDateDisabled) {
363
+ /**
364
+ * Applies the today button's label and whether today can be selected.
365
+ */
366
+ updateTodayButton() {
367
+ const todayButton = this._todayButton;
296
368
  if (todayButton) {
297
- todayButton.textContent = i18n?.today;
298
- todayButton.disabled = !this._isTodayAllowed(minDate, maxDate, isDateDisabled);
369
+ todayButton.textContent = this.i18n?.today;
370
+ todayButton.disabled = !this._isTodayAllowed();
299
371
  }
300
372
  }
301
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
+
401
+ /**
402
+ * Config that changes rarely (locale, allowed range, week numbers, theme).
403
+ * Split from the interaction state below so a keyboard/selection update
404
+ * does not re-assign every config property on every calendar.
405
+ * @private
406
+ */
302
407
  // eslint-disable-next-line @typescript-eslint/max-params
303
- __updateCalendars(
408
+ __updateCalendarsConfig(
304
409
  calendars,
305
410
  i18n,
306
411
  minDate,
307
412
  maxDate,
308
- selectedDate,
309
- focusedDate,
310
413
  showWeekNumbers,
311
- ignoreTaps,
312
- theme,
313
414
  isDateDisabled,
314
- enteredDate,
415
+ theme,
416
+ dateMetadataController,
315
417
  ) {
316
418
  if (calendars?.length) {
317
419
  calendars.forEach((calendar) => {
@@ -319,17 +421,38 @@ export const DatePickerOverlayContentMixin = (superClass) =>
319
421
  calendar.minDate = minDate;
320
422
  calendar.maxDate = maxDate;
321
423
  calendar.isDateDisabled = isDateDisabled;
424
+ calendar._dateMetadataController = dateMetadataController;
425
+ // Subscribe to controller updates (subscribing again is a no-op).
426
+ dateMetadataController?.subscribe(calendar);
427
+ calendar.showWeekNumbers = showWeekNumbers;
428
+
429
+ setOrRemoveAttribute(calendar, 'theme', theme);
430
+ });
431
+ }
432
+ }
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
+
445
+ /**
446
+ * Interaction state that changes often (focus, selection, entered date, taps).
447
+ * @private
448
+ */
449
+ __updateCalendarsState(calendars, selectedDate, focusedDate, enteredDate, ignoreTaps) {
450
+ if (calendars?.length) {
451
+ calendars.forEach((calendar) => {
322
452
  calendar.focusedDate = focusedDate;
323
453
  calendar.selectedDate = selectedDate;
324
- calendar.showWeekNumbers = showWeekNumbers;
325
- calendar.ignoreTaps = ignoreTaps;
326
454
  calendar.enteredDate = enteredDate;
327
-
328
- if (theme) {
329
- calendar.setAttribute('theme', theme);
330
- } else {
331
- calendar.removeAttribute('theme');
332
- }
455
+ calendar.ignoreTaps = ignoreTaps;
333
456
  });
334
457
  }
335
458
  }
@@ -340,11 +463,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
340
463
  years.forEach((year) => {
341
464
  year.selectedDate = selectedDate;
342
465
 
343
- if (theme) {
344
- year.setAttribute('theme', theme);
345
- } else {
346
- year.removeAttribute('theme');
347
- }
466
+ setOrRemoveAttribute(year, 'theme', theme);
348
467
  });
349
468
  }
350
469
  }
@@ -354,7 +473,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
354
473
  * @protected
355
474
  */
356
475
  _selectDate(dateToSelect) {
357
- if (!this._dateAllowed(dateToSelect)) {
476
+ if (!this._dateSelectable(dateToSelect)) {
358
477
  return false;
359
478
  }
360
479
  this.selectedDate = dateToSelect;
@@ -420,11 +539,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
420
539
  * @private
421
540
  */
422
541
  _calculateWeekScrollOffset(date) {
423
- // Get first day of month
424
- const temp = new Date(0, 0);
425
- temp.setFullYear(date.getFullYear());
426
- temp.setMonth(date.getMonth());
427
- temp.setDate(1);
542
+ const temp = firstOfMonth(date);
428
543
  // Determine week (=row index) of date within the month
429
544
  let week = 0;
430
545
  while (temp.getDate() < date.getDate()) {
@@ -452,12 +567,14 @@ export const DatePickerOverlayContentMixin = (superClass) =>
452
567
  const monthPosition = this._monthScroller.position;
453
568
  this._visibleMonthIndex = Math.floor(monthPosition);
454
569
  this._yearScroller.position = (monthPosition + this._originDate.getMonth()) / 12;
570
+ this.__scheduleLoadVisibleDateMetadata();
455
571
  }
456
572
 
457
573
  /** @private */
458
574
  _repositionMonthScroller() {
459
575
  this._monthScroller.position = this._yearScroller.position * 12 - this._originDate.getMonth();
460
576
  this._visibleMonthIndex = Math.floor(this._monthScroller.position);
577
+ this.__scheduleLoadVisibleDateMetadata();
461
578
  }
462
579
 
463
580
  /** @private */
@@ -624,8 +741,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
624
741
 
625
742
  /** @private */
626
743
  _differenceInMonths(date1, date2) {
627
- const months = (date1.getFullYear() - date2.getFullYear()) * 12;
628
- return months - date2.getMonth() + date1.getMonth();
744
+ return monthIndex(date1) - monthIndex(date2);
629
745
  }
630
746
 
631
747
  /** @private */
@@ -853,13 +969,11 @@ export const DatePickerOverlayContentMixin = (superClass) =>
853
969
 
854
970
  /** @private */
855
971
  _getDateDiff(months, days) {
856
- const date = new Date(0, 0);
857
- date.setFullYear(this.focusedDate.getFullYear());
858
- date.setMonth(this.focusedDate.getMonth() + months);
859
- if (days) {
860
- date.setDate(this.focusedDate.getDate() + days);
861
- }
862
- return date;
972
+ return createDate(
973
+ this.focusedDate.getFullYear(),
974
+ this.focusedDate.getMonth() + months,
975
+ days ? this.focusedDate.getDate() + days : 1,
976
+ );
863
977
  }
864
978
 
865
979
  /** @private */
@@ -889,16 +1003,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
889
1003
 
890
1004
  /** @private */
891
1005
  _moveFocusInsideMonth(focusedDate, property) {
892
- const dateToFocus = new Date(0, 0);
893
- dateToFocus.setFullYear(focusedDate.getFullYear());
894
-
895
- if (property === 'minDate') {
896
- dateToFocus.setMonth(focusedDate.getMonth());
897
- dateToFocus.setDate(1);
898
- } else {
899
- dateToFocus.setMonth(focusedDate.getMonth() + 1);
900
- dateToFocus.setDate(0);
901
- }
1006
+ const dateToFocus = property === 'minDate' ? firstOfMonth(focusedDate) : lastOfMonth(focusedDate);
902
1007
 
903
1008
  if (this._dateAllowed(dateToFocus)) {
904
1009
  this.focusDate(dateToFocus);
@@ -917,17 +1022,18 @@ export const DatePickerOverlayContentMixin = (superClass) =>
917
1022
  }
918
1023
 
919
1024
  /** @private */
920
- _isTodayAllowed(min, max, isDateDisabled) {
921
- return this._dateAllowed(this._getTodayMidnight(), min, max, isDateDisabled);
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());
922
1032
  }
923
1033
 
924
1034
  /** @private */
925
1035
  _getTodayMidnight() {
926
1036
  const today = new Date();
927
- const todayMidnight = new Date(0, 0);
928
- todayMidnight.setFullYear(today.getFullYear());
929
- todayMidnight.setMonth(today.getMonth());
930
- todayMidnight.setDate(today.getDate());
931
- return todayMidnight;
1037
+ return createDate(today.getFullYear(), today.getMonth(), today.getDate());
932
1038
  }
933
1039
  };
@@ -12,12 +12,14 @@ import { html, LitElement } from 'lit';
12
12
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
13
13
  import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
14
14
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
15
+ import { loaderStyles } from '@vaadin/component-base/src/styles/loader-styles.js';
15
16
  import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
16
17
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
17
18
  import { overlayContentStyles } from './styles/vaadin-date-picker-overlay-content-base-styles.js';
18
19
  import { DatePickerOverlayContentMixin } from './vaadin-date-picker-overlay-content-mixin.js';
19
20
 
20
21
  /**
22
+ * @attr {string} theme - The theme variants to apply to the component.
21
23
  * @customElement vaadin-date-picker-overlay-content
22
24
  * @extends HTMLElement
23
25
  * @private
@@ -30,7 +32,7 @@ class DatePickerOverlayContent extends DatePickerOverlayContentMixin(
30
32
  }
31
33
 
32
34
  static get styles() {
33
- return overlayContentStyles;
35
+ return [loaderStyles, overlayContentStyles];
34
36
  }
35
37
 
36
38
  static get lumoInjector() {
@@ -43,6 +45,8 @@ class DatePickerOverlayContent extends DatePickerOverlayContentMixin(
43
45
  <slot name="months"></slot>
44
46
  <slot name="years"></slot>
45
47
 
48
+ <div part="loader" aria-hidden="true"></div>
49
+
46
50
  <div role="toolbar" part="toolbar">
47
51
  <slot name="today-button"></slot>
48
52
  <div
@@ -16,6 +16,7 @@ import { DatePickerOverlayMixin } from './vaadin-date-picker-overlay-mixin.js';
16
16
  /**
17
17
  * An element used internally by `<vaadin-date-picker>`. Not intended to be used separately.
18
18
  *
19
+ * @attr {string} theme - The theme variants to apply to the component.
19
20
  * @customElement vaadin-date-picker-overlay
20
21
  * @extends HTMLElement
21
22
  * @private
@@ -13,11 +13,12 @@ import { datePickerYearStyles } from './styles/vaadin-date-picker-year-base-styl
13
13
  /**
14
14
  * An element used internally by `<vaadin-date-picker>`. Not intended to be used separately.
15
15
  *
16
+ * @attr {string} theme - The theme variants to apply to the component.
16
17
  * @customElement vaadin-date-picker-year
17
18
  * @extends HTMLElement
18
19
  * @private
19
20
  */
20
- export class DatePickerYear extends ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))) {
21
+ class DatePickerYear extends ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))) {
21
22
  static get is() {
22
23
  return 'vaadin-date-picker-year';
23
24
  }
@@ -63,3 +64,5 @@ export class DatePickerYear extends ThemableMixin(PolylitMixin(LumoInjectionMixi
63
64
  }
64
65
 
65
66
  defineCustomElement(DatePickerYear);
67
+
68
+ export { DatePickerYear };
@@ -7,7 +7,13 @@ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
7
7
  import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
8
8
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
9
9
  import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
10
- export { DatePickerDate, DatePickerI18n } from './vaadin-date-picker-mixin.js';
10
+ export {
11
+ DatePickerDate,
12
+ DatePickerDateMetadata,
13
+ DatePickerDateMetadataProvider,
14
+ DatePickerDateRange,
15
+ DatePickerI18n,
16
+ } from './vaadin-date-picker-mixin.js';
11
17
 
12
18
  /**
13
19
  * Fired when the user commits a value change.
@@ -128,6 +134,7 @@ export interface DatePickerEventMap extends HTMLElementEventMap, DatePickerCusto
128
134
  * ----------------------|--------------------
129
135
  * `years-toggle-button` | Fullscreen mode years scroller toggle
130
136
  * `toolbar` | Toolbar with slotted buttons
137
+ * `loader` | Loading spinner shown while the date metadata provider is resolving
131
138
  *
132
139
  * The following state attributes are available on the `<vaadin-date-picker-overlay-content>` element:
133
140
  *
@@ -136,6 +143,7 @@ export interface DatePickerEventMap extends HTMLElementEventMap, DatePickerCusto
136
143
  * `desktop` | Set when the overlay content is in desktop mode
137
144
  * `fullscreen` | Set when the overlay content is in fullscreen mode
138
145
  * `years-visible` | Set when the year scroller is visible in fullscreen mode
146
+ * `loading` | Set while the date metadata provider is resolving
139
147
  *
140
148
  * In order to style the month calendar, use `<vaadin-month-calendar>` shadow DOM parts:
141
149
  *
@@ -148,6 +156,7 @@ export interface DatePickerEventMap extends HTMLElementEventMap, DatePickerCusto
148
156
  * `week-number` | Week number element
149
157
  * `date` | Date element
150
158
  * `disabled` | Disabled date element
159
+ * `loading` | Date element in a month whose metadata is currently being fetched
151
160
  * `focused` | Focused date element
152
161
  * `selected` | Selected date element
153
162
  * `today` | Date element corresponding to the current day
@@ -92,6 +92,7 @@ import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
92
92
  * ----------------------|--------------------
93
93
  * `years-toggle-button` | Fullscreen mode years scroller toggle
94
94
  * `toolbar` | Toolbar with slotted buttons
95
+ * `loader` | Loading spinner shown while the date metadata provider is resolving
95
96
  *
96
97
  * The following state attributes are available on the `<vaadin-date-picker-overlay-content>` element:
97
98
  *
@@ -100,6 +101,7 @@ import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
100
101
  * `desktop` | Set when the overlay content is in desktop mode
101
102
  * `fullscreen` | Set when the overlay content is in fullscreen mode
102
103
  * `years-visible` | Set when the year scroller is visible in fullscreen mode
104
+ * `loading` | Set while the date metadata provider is resolving
103
105
  *
104
106
  * In order to style the month calendar, use `<vaadin-month-calendar>` shadow DOM parts:
105
107
  *
@@ -112,6 +114,7 @@ import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
112
114
  * `week-number` | Week number element
113
115
  * `date` | Date element
114
116
  * `disabled` | Disabled date element
117
+ * `loading` | Date element in a month whose metadata is currently being fetched
115
118
  * `focused` | Focused date element
116
119
  * `selected` | Selected date element
117
120
  * `today` | Date element corresponding to the current day
@@ -150,6 +153,7 @@ import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
150
153
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
151
154
  * @fires {CustomEvent} validated - Fired whenever the field is validated.
152
155
  *
156
+ * @attr {string} theme - The theme variants to apply to the component.
153
157
  * @customElement vaadin-date-picker
154
158
  * @extends HTMLElement
155
159
  */
@@ -4,8 +4,17 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
7
+ import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
7
8
  import { addListener } from '@vaadin/component-base/src/gestures.js';
8
- import { dateAllowed, dateEquals, getISOWeekNumber, normalizeDate } from './vaadin-date-picker-helper.js';
9
+ import {
10
+ dateAllowed,
11
+ dateEquals,
12
+ dateSelectable,
13
+ firstOfMonth,
14
+ getISOWeekNumber,
15
+ lastOfMonth,
16
+ normalizeDate,
17
+ } from './vaadin-date-picker-helper.js';
9
18
 
10
19
  export const MonthCalendarMixin = (superClass) =>
11
20
  class MonthCalendarMixinClass extends FocusMixin(superClass) {
@@ -87,6 +96,18 @@ export const MonthCalendarMixin = (superClass) =>
87
96
  value: () => false,
88
97
  },
89
98
 
99
+ /**
100
+ * The date-picker's controller resolving the metadata returned by its
101
+ * `dateMetadataProvider`. Assigned by the overlay content, which also subscribes this
102
+ * calendar to it.
103
+ * @protected
104
+ */
105
+ _dateMetadataController: {
106
+ type: Object,
107
+ attribute: false,
108
+ sync: true,
109
+ },
110
+
90
111
  enteredDate: {
91
112
  type: Date,
92
113
  },
@@ -148,17 +169,8 @@ export const MonthCalendarMixin = (superClass) =>
148
169
  * @protected
149
170
  */
150
171
  __computeDisabled(month, minDate, maxDate) {
151
- // First day of the month
152
- const firstDate = new Date(0, 0);
153
- firstDate.setFullYear(month.getFullYear());
154
- firstDate.setMonth(month.getMonth());
155
- firstDate.setDate(1);
156
-
157
- // Last day of the month
158
- const lastDate = new Date(0, 0);
159
- lastDate.setFullYear(month.getFullYear());
160
- lastDate.setMonth(month.getMonth() + 1);
161
- lastDate.setDate(0);
172
+ const firstDate = firstOfMonth(month);
173
+ const lastDate = lastOfMonth(month);
162
174
 
163
175
  if (
164
176
  minDate &&
@@ -225,11 +237,8 @@ export const MonthCalendarMixin = (superClass) =>
225
237
 
226
238
  /** @private */
227
239
  __focusedDateChanged(focusedDate, days) {
228
- if (Array.isArray(days) && days.some((date) => dateEquals(date, focusedDate))) {
229
- this.removeAttribute('aria-hidden');
230
- } else {
231
- this.setAttribute('aria-hidden', 'true');
232
- }
240
+ const hasFocusedDate = Array.isArray(days) && days.some((date) => dateEquals(date, focusedDate));
241
+ setOrRemoveAttribute(this, 'aria-hidden', !hasFocusedDate);
233
242
  }
234
243
 
235
244
  /** @protected */
@@ -253,11 +262,7 @@ export const MonthCalendarMixin = (superClass) =>
253
262
  if (month === undefined || i18n === undefined) {
254
263
  return [];
255
264
  }
256
- // First day of the month (at midnight).
257
- const date = new Date(0, 0);
258
- date.setFullYear(month.getFullYear());
259
- date.setMonth(month.getMonth());
260
- date.setDate(1);
265
+ const date = firstOfMonth(month);
261
266
 
262
267
  // Rewind to first day of the week.
263
268
  while (date.getDay() !== i18n.firstDayOfWeek) {
@@ -330,11 +335,7 @@ export const MonthCalendarMixin = (superClass) =>
330
335
 
331
336
  /** @private */
332
337
  _showWeekNumbersChanged(showWeekNumbers, i18n) {
333
- if (this.__computeShowWeekSeparator(showWeekNumbers, i18n)) {
334
- this.setAttribute('week-numbers', '');
335
- } else {
336
- this.removeAttribute('week-numbers');
337
- }
338
+ this.toggleAttribute('week-numbers', this.__computeShowWeekSeparator(showWeekNumbers, i18n));
338
339
  }
339
340
 
340
341
  // eslint-disable-next-line @typescript-eslint/max-params
@@ -345,6 +346,10 @@ export const MonthCalendarMixin = (superClass) =>
345
346
  result.push('disabled');
346
347
  }
347
348
 
349
+ if (date && this.__isMonthPending()) {
350
+ result.push('loading');
351
+ }
352
+
348
353
  if (dateEquals(date, focusedDate) && (hasFocus || dateEquals(date, enteredDate))) {
349
354
  result.push('focused');
350
355
  }
@@ -365,6 +370,12 @@ export const MonthCalendarMixin = (superClass) =>
365
370
  result.push('future');
366
371
  }
367
372
 
373
+ // Only a string can name parts, so anything else the provider set is ignored.
374
+ const customParts = date && this._dateMetadataController?.getMetadata(date)?.part;
375
+ if (customParts && typeof customParts === 'string') {
376
+ result.push(customParts);
377
+ }
378
+
368
379
  return result.join(' ');
369
380
  }
370
381
 
@@ -378,14 +389,29 @@ export const MonthCalendarMixin = (superClass) =>
378
389
  return String(this.__isDaySelected(date, selectedDate));
379
390
  }
380
391
 
392
+ /**
393
+ * Whether the displayed month is currently being fetched, which is the same state the overlay
394
+ * reports with its spinner. A month that has not been asked about yet is not pending: nothing is
395
+ * loading, so there is nothing to report.
396
+ * @private
397
+ */
398
+ __isMonthPending() {
399
+ return !!this._dateMetadataController?.isMonthPending(this.month);
400
+ }
401
+
381
402
  /** @private */
382
403
  __isDayDisabled(date, minDate, maxDate, isDateDisabled) {
383
- return !dateAllowed(date, minDate, maxDate, isDateDisabled);
404
+ return !dateSelectable(date, minDate, maxDate, isDateDisabled, this._dateMetadataController);
384
405
  }
385
406
 
386
407
  /** @private */
387
408
  __computeDayAriaDisabled(date, min, max, isDateDisabled) {
388
- if (date === undefined || (min === undefined && max === undefined && isDateDisabled === undefined)) {
409
+ if (date === undefined) {
410
+ return 'false';
411
+ }
412
+
413
+ const hasProvider = !!this._dateMetadataController?.provider;
414
+ if (!hasProvider && min === undefined && max === undefined && isDateDisabled === undefined) {
389
415
  return 'false';
390
416
  }
391
417
 
@@ -12,6 +12,7 @@ import { monthCalendarStyles } from './styles/vaadin-month-calendar-base-styles.
12
12
  import { MonthCalendarMixin } from './vaadin-month-calendar-mixin.js';
13
13
 
14
14
  /**
15
+ * @attr {string} theme - The theme variants to apply to the component.
15
16
  * @customElement vaadin-month-calendar
16
17
  * @extends HTMLElement
17
18
  * @private