@vaadin/date-picker 25.3.0-dev.1fa5a51482 → 25.3.0-rc1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/date-picker",
3
- "version": "25.3.0-dev.1fa5a51482",
3
+ "version": "25.3.0-rc1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,21 +35,21 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@open-wc/dedupe-mixin": "^1.3.0",
38
- "@vaadin/a11y-base": "25.3.0-dev.1fa5a51482",
39
- "@vaadin/button": "25.3.0-dev.1fa5a51482",
40
- "@vaadin/component-base": "25.3.0-dev.1fa5a51482",
41
- "@vaadin/field-base": "25.3.0-dev.1fa5a51482",
42
- "@vaadin/input-container": "25.3.0-dev.1fa5a51482",
43
- "@vaadin/overlay": "25.3.0-dev.1fa5a51482",
44
- "@vaadin/vaadin-themable-mixin": "25.3.0-dev.1fa5a51482",
38
+ "@vaadin/a11y-base": "25.3.0-rc1",
39
+ "@vaadin/button": "25.3.0-rc1",
40
+ "@vaadin/component-base": "25.3.0-rc1",
41
+ "@vaadin/field-base": "25.3.0-rc1",
42
+ "@vaadin/input-container": "25.3.0-rc1",
43
+ "@vaadin/overlay": "25.3.0-rc1",
44
+ "@vaadin/vaadin-themable-mixin": "25.3.0-rc1",
45
45
  "lit": "^3.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@vaadin/aura": "25.3.0-dev.1fa5a51482",
49
- "@vaadin/chai-plugins": "25.3.0-dev.1fa5a51482",
50
- "@vaadin/test-runner-commands": "25.3.0-dev.1fa5a51482",
48
+ "@vaadin/aura": "25.3.0-rc1",
49
+ "@vaadin/chai-plugins": "25.3.0-rc1",
50
+ "@vaadin/test-runner-commands": "25.3.0-rc1",
51
51
  "@vaadin/testing-helpers": "^2.0.0",
52
- "@vaadin/vaadin-lumo-styles": "25.3.0-dev.1fa5a51482",
52
+ "@vaadin/vaadin-lumo-styles": "25.3.0-rc1",
53
53
  "sinon": "^22.0.0"
54
54
  },
55
55
  "customElements": "custom-elements.json",
@@ -57,5 +57,5 @@
57
57
  "web-types.json",
58
58
  "web-types.lit.json"
59
59
  ],
60
- "gitHead": "1cd5964b758ffbce3ddb6248a1997f55f8e0ebf6"
60
+ "gitHead": "d4809590b2f61bd607e376ab3dec84f4bfb059a7"
61
61
  }
@@ -50,9 +50,6 @@ export const overlayContentStyles = css`
50
50
  display: none !important;
51
51
  }
52
52
 
53
- /* Loading spinner shown while data is being loaded (currently while the disabled dates provider
54
- resolves). Positioned over the month scroller, which paints its own transformed layers, so
55
- grid stacking is not enough. */
56
53
  [part='loader'] {
57
54
  position: absolute;
58
55
  z-index: 1;
@@ -10,6 +10,9 @@ export const monthCalendarStyles = css`
10
10
  :host {
11
11
  display: block;
12
12
  padding: var(--vaadin-date-picker-month-padding, var(--vaadin-padding-s));
13
+ -webkit-tap-highlight-color: transparent;
14
+ -webkit-user-select: none;
15
+ user-select: none;
13
16
  }
14
17
 
15
18
  [part='month-header'] {
@@ -3,73 +3,92 @@
3
3
  * Copyright (c) 2016 - 2026 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import type { ReactiveController, ReactiveControllerHost } from 'lit';
7
- import type { DatePickerDateMetadata, DatePickerDateRange } from './vaadin-date-picker-mixin.js';
8
-
9
- export type DateMetadataProvider = (
10
- range: DatePickerDateRange,
11
- ) => DatePickerDateMetadata[] | Promise<DatePickerDateMetadata[]>;
6
+ import type { ReactiveController, ReactiveControllerHost, ReactiveElement } from 'lit';
7
+ import type { DatePickerDateMetadata, DatePickerDateMetadataProvider } from './vaadin-date-picker-mixin.js';
12
8
 
13
9
  /**
14
- * A reactive controller that resolves the metadata (disabled state, custom part
15
- * names, ...) for the dates shown by the date-picker's `dateMetadataProvider`.
16
- * It calls the provider once for a range of months, caches the resolved months
17
- * so scrolling back and forth does not re-fetch, and prefetches a buffer of
18
- * months around the requested range.
10
+ * A reactive controller that resolves the metadata (currently the disabled state)
11
+ * for the dates shown by the date-picker's `dateMetadataProvider`.
12
+ *
13
+ * The provider is called for a range of months and may return an array
14
+ * synchronously or a `Promise`, so results from a server (Flow) or a remote
15
+ * availability service can be awaited. The range and each returned entry identify
16
+ * a date by an ISO 8601 string, e.g. `{ date: '2026-01-01', disabled: true }`.
17
+ *
18
+ * `ARCHITECTURE.md` in this package records the reasoning behind the request,
19
+ * caching, notification and failure behavior.
19
20
  */
20
- export declare class DateMetadataController implements ReactiveController {
21
- constructor(host: ReactiveControllerHost, onChange: () => void);
21
+ export class DateMetadataController implements ReactiveController {
22
+ /**
23
+ * The controller host element.
24
+ */
25
+ host: HTMLElement & ReactiveControllerHost;
22
26
 
23
27
  /**
24
28
  * The provider function, or `null` when none is set.
25
29
  */
26
- provider: DateMetadataProvider | null;
30
+ provider: DatePickerDateMetadataProvider | null;
31
+
32
+ constructor(host: HTMLElement & ReactiveControllerHost, onChange?: () => void);
33
+
34
+ hostConnected(): void;
27
35
 
28
36
  /**
29
- * Whether any month range is currently being fetched.
37
+ * Registers an element to be re-rendered whenever the resolved metadata or the
38
+ * loading state changes. The element must render from its bindings, and must not be
39
+ * the one whose own observer triggers a load. It stays registered for the
40
+ * controller's lifetime.
30
41
  */
31
- readonly loading: boolean;
42
+ subscribe(element: ReactiveElement): void;
32
43
 
33
- hostDisconnected(): void;
44
+ /**
45
+ * Whether any month range is currently being fetched.
46
+ */
47
+ isLoading(): boolean;
34
48
 
35
49
  /**
36
- * Sets the provider function and clears the cache. Passing the same provider
37
- * again is a no-op. Callers should keep a stable provider reference.
50
+ * Sets the provider function and clears the cache. Passing the same provider again
51
+ * is a no-op, so callers should keep a stable reference.
38
52
  */
39
- setProvider(provider: DateMetadataProvider | null | undefined): void;
53
+ setProvider(provider: DatePickerDateMetadataProvider | null | undefined): void;
40
54
 
41
55
  /**
42
56
  * Clears the cache and invalidates any in-flight requests.
43
57
  */
44
- reset(): void;
58
+ clearCache(): void;
45
59
 
46
60
  /**
47
- * Whether the month containing the given date has been fully resolved.
61
+ * Whether the provider has answered for the month containing the given date.
62
+ * A month whose request failed is not loaded and will be requested again.
48
63
  */
49
- isMonthLoaded(date: Date): boolean;
64
+ isMonthLoaded(date: Date | null | undefined): boolean;
50
65
 
51
66
  /**
52
- * The metadata resolved for the given date, or `undefined` when the date has
53
- * no metadata or its month has not been resolved yet.
67
+ * Whether the month containing the given date is currently being fetched. A month
68
+ * that has not been asked about is not pending, so this reports the same state as
69
+ * `isLoading()` does for the whole cache.
54
70
  */
55
- getMetadata(date: Date): DatePickerDateMetadata | undefined;
71
+ isMonthPending(date: Date | null | undefined): boolean;
56
72
 
57
73
  /**
58
- * Whether the given date is disabled by the provider. Only returns `true` for
59
- * dates in an already-resolved month.
74
+ * The metadata resolved for the given date, or `undefined` when the date has
75
+ * no metadata or its month has not been resolved yet. Returns the entry the
76
+ * provider supplied, which the caller must not modify.
60
77
  */
61
- isDateDisabled(date: Date): boolean;
78
+ getMetadata(date: Date | null | undefined): DatePickerDateMetadata | undefined;
62
79
 
63
80
  /**
64
- * Whether the given date cannot be selected yet: it is disabled by the
65
- * provider, or its month has not been resolved. Returns `false` when no
66
- * provider is set.
81
+ * Whether the given date is disabled by its metadata.
67
82
  */
68
- isDateBlocked(date: Date): boolean;
83
+ isDateDisabled(date: Date | null | undefined): boolean;
69
84
 
70
85
  /**
71
86
  * Ensures the provider has been consulted for the inclusive range between the
72
- * given dates, expanded by a prefetch buffer of months on each side.
87
+ * given dates, rounded out to whole blocks of months. Months already loaded or in
88
+ * flight are skipped, and the ones left over are requested with a single call.
89
+ *
90
+ * Each call that finds a missing month issues its own request, so a caller that
91
+ * loads on scroll should debounce.
73
92
  */
74
- ensureRangeLoaded(startDate: Date, endDate: Date): void;
93
+ ensureRangeLoaded(startDate: Date | null | undefined, endDate: Date | null | undefined): void;
75
94
  }
@@ -3,252 +3,266 @@
3
3
  * Copyright (c) 2016 - 2026 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { extractDateParts } from './vaadin-date-picker-helper.js';
6
+ import { microTask } from '@vaadin/component-base/src/async.js';
7
+ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8
+ import { issueWarning } from '@vaadin/component-base/src/warnings.js';
9
+ import { formatISODate, lastOfMonth, monthDate, monthIndex, parseDate } from './vaadin-date-picker-helper.js';
7
10
 
8
- /**
9
- * Number of months fetched before and after the requested range, so that
10
- * scrolling a few months in either direction does not trigger a new request.
11
- * Mirrors how the grid prefetches rows around the viewport.
12
- */
13
- const PREFETCH_MONTHS = 6;
11
+ // Counted from January of year 0, so a block is one calendar year.
12
+ const BLOCK_MONTHS = 12;
14
13
 
15
- function monthKey(date) {
16
- return `${date.getFullYear()}-${date.getMonth()}`;
14
+ function blockStart(month) {
15
+ return Math.floor(month / BLOCK_MONTHS) * BLOCK_MONTHS;
17
16
  }
18
17
 
19
- function dateKey(date) {
20
- return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
21
- }
18
+ const PENDING_MONTH = Object.freeze({ pending: true });
22
19
 
23
- function firstOfMonth(date) {
24
- return new Date(date.getFullYear(), date.getMonth(), 1);
20
+ // The date of an entry, or `undefined` when its `date` is not one. Checked for being a string first,
21
+ // since coercing another type to one can throw, and an entry comes from outside.
22
+ function entryDate(entry) {
23
+ return typeof entry?.date === 'string' ? parseDate(entry.date) : undefined;
25
24
  }
26
25
 
27
- function lastOfMonth(date) {
28
- return new Date(date.getFullYear(), date.getMonth() + 1, 0);
29
- }
26
+ function groupEntriesByMonth(months, entries) {
27
+ const result = new Map(months.map((month) => [month, new Map()]));
28
+
29
+ if (Array.isArray(entries)) {
30
+ entries.forEach((entry) => {
31
+ const date = entryDate(entry);
32
+ if (date) {
33
+ result.get(monthIndex(date))?.set(date.getDate(), entry);
34
+ } else {
35
+ issueWarning('Ignored `dateMetadataProvider` entries whose `date` is not an ISO 8601 date.');
36
+ }
37
+ });
38
+ } else if (entries != null) {
39
+ issueWarning('Expected `dateMetadataProvider` to return an array of date metadata objects.');
40
+ }
30
41
 
31
- function addMonths(date, months) {
32
- return new Date(date.getFullYear(), date.getMonth() + months, 1);
42
+ return result;
33
43
  }
34
44
 
35
45
  /**
36
- * A reactive controller that resolves the metadata (disabled state, custom part
37
- * names, ...) for the dates shown by the date-picker's `dateMetadataProvider`.
38
- * It calls the provider once for a range of months (never one date at a time),
39
- * caches the resolved months so scrolling back and forth does not re-fetch them,
40
- * and prefetches a buffer of months around the requested range. While any
41
- * request is in flight, {@link #loading} is `true` so the overlay can show a
42
- * spinner.
46
+ * A reactive controller that resolves the metadata (currently the disabled state)
47
+ * for the dates shown by the date-picker's `dateMetadataProvider`.
48
+ *
49
+ * The provider is called for a range of months and may return an array
50
+ * synchronously or a `Promise`, so results from a server (Flow) or a remote
51
+ * availability service can be awaited. The range and each returned entry identify
52
+ * a date by an ISO 8601 string, e.g. `{ date: '2026-01-01', disabled: true }`.
43
53
  *
44
- * The provider may return an array synchronously or a `Promise`, so results
45
- * from a server (Flow) or a remote availability service can be awaited. Each
46
- * returned entry is a `DatePickerDate` extended with metadata fields, e.g.
47
- * `{ year, month, day, disabled: true, part: 'busy' }`.
54
+ * `ARCHITECTURE.md` in this package records the reasoning behind the request,
55
+ * caching, notification and failure behavior.
48
56
  */
49
57
  export class DateMetadataController {
58
+ /**
59
+ * The controller host element.
60
+ * @type {import('lit').ReactiveControllerHost & HTMLElement}
61
+ */
62
+ host;
63
+
64
+ /**
65
+ * The provider function, or `null` when none is set.
66
+ * @type {Function | null}
67
+ */
68
+ provider = null;
69
+
70
+ /** @type {(() => void) | undefined} */
71
+ #onChange;
72
+
73
+ /**
74
+ * What is known about each month, keyed by month index: a shared marker while its request is in
75
+ * flight, or a record holding the resolved entries by day. A month is absent until it is loaded,
76
+ * and absent again if its request failed.
77
+ * @type {Map<number, { pending: boolean, entries?: Map<number, object> }>}
78
+ */
79
+ #months = new Map();
80
+
81
+ /** @type {Set<import('lit').ReactiveElement>} */
82
+ #subscribers = new Set();
83
+
84
+ #requestId = 0;
85
+
86
+ /** @type {import('@vaadin/component-base/src/debounce.js').Debouncer} */
87
+ #notifyDebouncer;
88
+
50
89
  constructor(host, onChange) {
51
90
  this.host = host;
52
- this.__onChange = onChange;
53
- this.provider = null;
54
- this.__metadata = new Map();
55
- this.__loadedMonths = new Set();
56
- this.__pendingMonths = new Set();
57
- this.__requestId = 0;
91
+ this.#onChange = onChange;
92
+ }
93
+
94
+ hostConnected() {
95
+ this.#notify();
58
96
  }
59
97
 
60
- hostDisconnected() {
61
- // Invalidate in-flight requests so their late results don't touch a
62
- // detached host, and drop the pending (loading) state. The resolved cache
63
- // is kept so reopening the overlay does not re-fetch.
64
- this.__requestId += 1;
65
- this.__pendingMonths = new Set();
98
+ /**
99
+ * Registers an element to be re-rendered whenever the resolved metadata or the
100
+ * loading state changes. The element must render from its bindings, and must not be
101
+ * the one whose own observer triggers a load. It stays registered for the
102
+ * controller's lifetime.
103
+ *
104
+ * @param {import('lit').ReactiveElement} element
105
+ */
106
+ subscribe(element) {
107
+ this.#subscribers.add(element);
66
108
  }
67
109
 
68
110
  /**
69
111
  * Whether any month range is currently being fetched.
70
112
  * @return {boolean}
71
113
  */
72
- get loading() {
73
- return this.__pendingMonths.size > 0;
114
+ isLoading() {
115
+ for (const { pending } of this.#months.values()) {
116
+ if (pending) {
117
+ return true;
118
+ }
119
+ }
120
+ return false;
74
121
  }
75
122
 
76
123
  /**
77
- * Sets the provider function and clears the cache. Passing the same provider
78
- * again is a no-op, so the cache survives unrelated re-renders — callers
79
- * should therefore keep a stable provider reference rather than passing a new
80
- * function on every update, which would otherwise reset the cache and
81
- * re-fetch every visible range.
124
+ * Sets the provider function and clears the cache. Passing the same provider again
125
+ * is a no-op, so callers should keep a stable reference.
126
+ *
127
+ * @param {Function | null | undefined} provider
82
128
  */
83
129
  setProvider(provider) {
84
- if (this.provider === provider) {
130
+ const next = provider ?? null;
131
+ if (this.provider === next) {
85
132
  return;
86
133
  }
87
- this.provider = provider;
88
- this.reset();
134
+ this.provider = next;
135
+ this.clearCache();
89
136
  }
90
137
 
91
- /** Clears the cache and invalidates any in-flight requests. */
92
- reset() {
93
- this.__metadata = new Map();
94
- this.__loadedMonths = new Set();
95
- this.__pendingMonths = new Set();
96
- this.__requestId += 1;
97
- this.__notify();
138
+ /**
139
+ * Clears the cache and invalidates any in-flight requests.
140
+ */
141
+ clearCache() {
142
+ this.#months.clear();
143
+ this.#requestId += 1;
144
+ this.#notify();
98
145
  }
99
146
 
100
147
  /**
101
- * Whether the month containing the given date has been fully resolved.
102
- * @param {Date} date
148
+ * Whether the provider has answered for the month containing the given date.
149
+ * A month whose request failed is not loaded and will be requested again.
150
+ * @param {Date | null | undefined} date
103
151
  * @return {boolean}
104
152
  */
105
153
  isMonthLoaded(date) {
106
- return this.__loadedMonths.has(monthKey(date));
154
+ return !!this.#resolvedMonth(date);
155
+ }
156
+
157
+ /**
158
+ * Whether the month containing the given date is currently being fetched. A month
159
+ * that has not been asked about is not pending, so this reports the same state as
160
+ * `isLoading()` does for the whole cache.
161
+ * @param {Date | null | undefined} date
162
+ * @return {boolean}
163
+ */
164
+ isMonthPending(date) {
165
+ return !!date && !!this.#months.get(monthIndex(date))?.pending;
107
166
  }
108
167
 
109
168
  /**
110
169
  * The metadata resolved for the given date, or `undefined` when the date has
111
- * no metadata or its month has not been resolved yet.
112
- * @param {Date} date
170
+ * no metadata or its month has not been resolved yet. Returns the entry the
171
+ * provider supplied, which the caller must not modify.
172
+ * @param {Date | null | undefined} date
113
173
  * @return {object | undefined}
114
174
  */
115
175
  getMetadata(date) {
116
- return date ? this.__metadata.get(dateKey(date)) : undefined;
176
+ return this.#resolvedMonth(date)?.entries.get(date.getDate());
117
177
  }
118
178
 
119
179
  /**
120
- * Whether the given date is disabled by the provider. Only returns `true` for
121
- * dates in an already-resolved month.
122
- * @param {Date} date
180
+ * Whether the given date is disabled by its metadata.
181
+ * @param {Date | null | undefined} date
123
182
  * @return {boolean}
124
183
  */
125
184
  isDateDisabled(date) {
126
185
  return !!this.getMetadata(date)?.disabled;
127
186
  }
128
187
 
129
- /**
130
- * Whether the given date cannot be selected yet: it is disabled by the
131
- * provider, or its month has not been resolved so its state is still unknown.
132
- * Returns `false` when no provider is set. Used to block selection and to
133
- * render dates as non-selectable while their month is loading.
134
- * @param {Date} date
135
- * @return {boolean}
136
- */
137
- isDateBlocked(date) {
138
- return !!this.provider && !!date && (this.isDateDisabled(date) || !this.isMonthLoaded(date));
139
- }
140
-
141
188
  /**
142
189
  * Ensures the provider has been consulted for the inclusive range between the
143
- * given dates, expanded by a prefetch buffer of months on each side. Months
144
- * that are already loaded or in flight are skipped, and consecutive missing
145
- * months are grouped into a single provider call.
190
+ * given dates, rounded out to whole blocks of months. Months already loaded or in
191
+ * flight are skipped, and the ones left over are requested with a single call.
192
+ *
193
+ * Each call that finds a missing month issues its own request, so a caller that
194
+ * loads on scroll should debounce.
146
195
  *
147
- * @param {Date} startDate
148
- * @param {Date} endDate
196
+ * @param {Date | null | undefined} startDate
197
+ * @param {Date | null | undefined} endDate
149
198
  */
150
199
  ensureRangeLoaded(startDate, endDate) {
151
200
  if (!this.provider || !startDate || !endDate) {
152
201
  return;
153
202
  }
154
203
 
155
- const first = addMonths(firstOfMonth(startDate), -PREFETCH_MONTHS);
156
- const last = addMonths(firstOfMonth(endDate), PREFETCH_MONTHS);
204
+ const first = blockStart(monthIndex(startDate));
205
+ const last = blockStart(monthIndex(endDate)) + BLOCK_MONTHS - 1;
157
206
 
158
- const monthsToLoad = [];
159
- for (let month = first; month <= last; month = addMonths(month, 1)) {
160
- const key = monthKey(month);
161
- if (!this.__loadedMonths.has(key) && !this.__pendingMonths.has(key)) {
162
- monthsToLoad.push(new Date(month));
207
+ const months = [];
208
+ for (let month = first; month <= last; month++) {
209
+ if (!this.#months.has(month)) {
210
+ months.push(month);
163
211
  }
164
212
  }
165
213
 
166
- if (monthsToLoad.length === 0) {
167
- return;
214
+ if (months.length > 0) {
215
+ this.#loadMonths(months);
168
216
  }
169
-
170
- this.__groupConsecutiveMonths(monthsToLoad).forEach((group) => this.__loadGroup(group));
171
217
  }
172
218
 
173
- /** @private */
174
- __groupConsecutiveMonths(months) {
175
- const groups = [];
176
- months.forEach((month) => {
177
- const group = groups.at(-1);
178
- const previous = group?.at(-1);
179
- if (previous && monthKey(addMonths(previous, 1)) === monthKey(month)) {
180
- group.push(month);
181
- } else {
182
- groups.push([month]);
183
- }
184
- });
185
- return groups;
219
+ #resolvedMonth(date) {
220
+ const month = date && this.#months.get(monthIndex(date));
221
+ return month && !month.pending ? month : undefined;
186
222
  }
187
223
 
188
- /** @private */
189
- __loadGroup(months) {
190
- const requestId = this.__requestId;
191
- months.forEach((month) => this.__pendingMonths.add(monthKey(month)));
192
- this.__notify();
224
+ async #loadMonths(months) {
225
+ const requestId = this.#requestId;
226
+ months.forEach((month) => this.#months.set(month, PENDING_MONTH));
227
+ this.#notify();
193
228
 
194
229
  const range = {
195
- start: extractDateParts(firstOfMonth(months[0])),
196
- end: extractDateParts(lastOfMonth(months[months.length - 1])),
230
+ start: formatISODate(monthDate(months[0])),
231
+ end: formatISODate(lastOfMonth(monthDate(months.at(-1)))),
197
232
  };
198
233
 
199
- let result;
234
+ let entries;
200
235
  try {
201
- result = this.provider(range);
236
+ const data = await this.provider(range);
237
+ entries = groupEntriesByMonth(months, data);
202
238
  } catch (error) {
203
- // Treat a throwing provider the same as a rejected promise: clear the
204
- // pending state and disable nothing, rather than letting the error
205
- // propagate out of the scroll/render path that triggered the load.
206
239
  console.error(error);
207
- this.__resolveGroup(months, [], requestId);
208
- return;
209
- }
210
-
211
- if (result && typeof result.then === 'function') {
212
- result.then(
213
- (dates) => this.__resolveGroup(months, dates, requestId),
214
- (error) => {
215
- console.error(error);
216
- this.__resolveGroup(months, [], requestId);
217
- },
218
- );
219
- } else {
220
- this.__resolveGroup(months, result, requestId);
221
240
  }
222
- }
223
241
 
224
- /** @private */
225
- __resolveGroup(months, entries, requestId) {
226
- // Ignore results from before the last reset (e.g. provider changed).
227
- if (requestId !== this.__requestId) {
242
+ if (requestId !== this.#requestId) {
228
243
  return;
229
244
  }
230
245
 
231
- if (Array.isArray(entries)) {
232
- entries.forEach((entry) => {
233
- if (entry) {
234
- this.__metadata.set(`${entry.year}-${entry.month}-${entry.day}`, entry);
235
- }
236
- });
237
- }
238
-
239
246
  months.forEach((month) => {
240
- const key = monthKey(month);
241
- this.__pendingMonths.delete(key);
242
- this.__loadedMonths.add(key);
247
+ if (entries) {
248
+ this.#months.set(month, { pending: false, entries: entries.get(month) });
249
+ } else {
250
+ this.#months.delete(month);
251
+ }
243
252
  });
244
253
 
245
- this.__notify();
254
+ this.#notify();
246
255
  }
247
256
 
248
- /** @private */
249
- __notify() {
250
- if (this.__onChange) {
251
- this.__onChange();
257
+ #notify() {
258
+ this.#subscribers.forEach((element) => element.requestUpdate());
259
+
260
+ if (this.#onChange) {
261
+ this.#notifyDebouncer = Debouncer.debounce(this.#notifyDebouncer, microTask, () => {
262
+ if (this.host.isConnected) {
263
+ this.#onChange();
264
+ }
265
+ });
252
266
  }
253
267
  }
254
268
  }