@vaadin/date-picker 25.3.0-alpha7 → 25.3.0-dev.1fa5a51482
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 +231 -1
- package/package.json +13 -13
- package/src/styles/vaadin-date-picker-overlay-content-base-styles.js +12 -0
- package/src/vaadin-date-metadata-controller.d.ts +75 -0
- package/src/vaadin-date-metadata-controller.js +254 -0
- package/src/vaadin-date-picker-mixin.d.ts +60 -0
- package/src/vaadin-date-picker-mixin.js +170 -2
- package/src/vaadin-date-picker-overlay-content-mixin.js +131 -3
- package/src/vaadin-date-picker-overlay-content.js +5 -1
- package/src/vaadin-date-picker-overlay.js +1 -0
- package/src/vaadin-date-picker-year.js +1 -0
- package/src/vaadin-date-picker.d.ts +11 -1
- package/src/vaadin-date-picker.js +8 -0
- package/src/vaadin-month-calendar-mixin.js +51 -2
- package/src/vaadin-month-calendar.js +1 -0
- package/web-types.json +14 -5
- package/web-types.lit.json +9 -2
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2016 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { extractDateParts } from './vaadin-date-picker-helper.js';
|
|
7
|
+
|
|
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;
|
|
14
|
+
|
|
15
|
+
function monthKey(date) {
|
|
16
|
+
return `${date.getFullYear()}-${date.getMonth()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dateKey(date) {
|
|
20
|
+
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function firstOfMonth(date) {
|
|
24
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lastOfMonth(date) {
|
|
28
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addMonths(date, months) {
|
|
32
|
+
return new Date(date.getFullYear(), date.getMonth() + months, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
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.
|
|
43
|
+
*
|
|
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' }`.
|
|
48
|
+
*/
|
|
49
|
+
export class DateMetadataController {
|
|
50
|
+
constructor(host, onChange) {
|
|
51
|
+
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;
|
|
58
|
+
}
|
|
59
|
+
|
|
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();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether any month range is currently being fetched.
|
|
70
|
+
* @return {boolean}
|
|
71
|
+
*/
|
|
72
|
+
get loading() {
|
|
73
|
+
return this.__pendingMonths.size > 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
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.
|
|
82
|
+
*/
|
|
83
|
+
setProvider(provider) {
|
|
84
|
+
if (this.provider === provider) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.provider = provider;
|
|
88
|
+
this.reset();
|
|
89
|
+
}
|
|
90
|
+
|
|
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();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether the month containing the given date has been fully resolved.
|
|
102
|
+
* @param {Date} date
|
|
103
|
+
* @return {boolean}
|
|
104
|
+
*/
|
|
105
|
+
isMonthLoaded(date) {
|
|
106
|
+
return this.__loadedMonths.has(monthKey(date));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 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
|
|
113
|
+
* @return {object | undefined}
|
|
114
|
+
*/
|
|
115
|
+
getMetadata(date) {
|
|
116
|
+
return date ? this.__metadata.get(dateKey(date)) : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
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
|
|
123
|
+
* @return {boolean}
|
|
124
|
+
*/
|
|
125
|
+
isDateDisabled(date) {
|
|
126
|
+
return !!this.getMetadata(date)?.disabled;
|
|
127
|
+
}
|
|
128
|
+
|
|
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
|
+
/**
|
|
142
|
+
* 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.
|
|
146
|
+
*
|
|
147
|
+
* @param {Date} startDate
|
|
148
|
+
* @param {Date} endDate
|
|
149
|
+
*/
|
|
150
|
+
ensureRangeLoaded(startDate, endDate) {
|
|
151
|
+
if (!this.provider || !startDate || !endDate) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const first = addMonths(firstOfMonth(startDate), -PREFETCH_MONTHS);
|
|
156
|
+
const last = addMonths(firstOfMonth(endDate), PREFETCH_MONTHS);
|
|
157
|
+
|
|
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));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (monthsToLoad.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.__groupConsecutiveMonths(monthsToLoad).forEach((group) => this.__loadGroup(group));
|
|
171
|
+
}
|
|
172
|
+
|
|
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;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @private */
|
|
189
|
+
__loadGroup(months) {
|
|
190
|
+
const requestId = this.__requestId;
|
|
191
|
+
months.forEach((month) => this.__pendingMonths.add(monthKey(month)));
|
|
192
|
+
this.__notify();
|
|
193
|
+
|
|
194
|
+
const range = {
|
|
195
|
+
start: extractDateParts(firstOfMonth(months[0])),
|
|
196
|
+
end: extractDateParts(lastOfMonth(months[months.length - 1])),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let result;
|
|
200
|
+
try {
|
|
201
|
+
result = this.provider(range);
|
|
202
|
+
} 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
|
+
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
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @private */
|
|
225
|
+
__resolveGroup(months, entries, requestId) {
|
|
226
|
+
// Ignore results from before the last reset (e.g. provider changed).
|
|
227
|
+
if (requestId !== this.__requestId) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
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
|
+
months.forEach((month) => {
|
|
240
|
+
const key = monthKey(month);
|
|
241
|
+
this.__pendingMonths.delete(key);
|
|
242
|
+
this.__loadedMonths.add(key);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.__notify();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @private */
|
|
249
|
+
__notify() {
|
|
250
|
+
if (this.__onChange) {
|
|
251
|
+
this.__onChange();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -18,6 +18,34 @@ export interface DatePickerDate {
|
|
|
18
18
|
year: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface DatePickerDateRange {
|
|
22
|
+
/**
|
|
23
|
+
* The first date of the range (inclusive).
|
|
24
|
+
*/
|
|
25
|
+
start: DatePickerDate;
|
|
26
|
+
/**
|
|
27
|
+
* The last date of the range (inclusive).
|
|
28
|
+
*/
|
|
29
|
+
end: DatePickerDate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Metadata resolved on demand for a single date by `dateMetadataProvider`. Extends
|
|
34
|
+
* `DatePickerDate` with the date's metadata. Extra fields can be added and used by the
|
|
35
|
+
* synchronous generators (e.g. `isDateDisabled`) as context.
|
|
36
|
+
*/
|
|
37
|
+
export interface DatePickerDateMetadata extends DatePickerDate {
|
|
38
|
+
/**
|
|
39
|
+
* Whether the date cannot be selected.
|
|
40
|
+
*/
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Custom part name(s) added to the date cell's `part` attribute, so a theme can style the
|
|
44
|
+
* date via `::part()`. Either a single name or several separated by spaces.
|
|
45
|
+
*/
|
|
46
|
+
part?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
export interface DatePickerI18n {
|
|
22
50
|
/**
|
|
23
51
|
* An array with the full names of months starting
|
|
@@ -236,6 +264,31 @@ export declare class DatePickerMixinClass {
|
|
|
236
264
|
*/
|
|
237
265
|
isDateDisabled: (date: DatePickerDate) => boolean;
|
|
238
266
|
|
|
267
|
+
/**
|
|
268
|
+
* A batch function that fetches metadata for a range of dates the calendar is about to render.
|
|
269
|
+
* It receives a `DatePickerDateRange` object and returns, or resolves with, an array of
|
|
270
|
+
* `DatePickerDateMetadata` objects (a `DatePickerDate` extended with metadata such as
|
|
271
|
+
* `disabled` and custom `part` names) for the dates that have metadata within that range.
|
|
272
|
+
*
|
|
273
|
+
* Unlike `isDateDisabled`, which is called once per date, this function is called for a range
|
|
274
|
+
* of dates at a time, and again as the calendar renders further dates. The size of the range
|
|
275
|
+
* is decided by the calendar and may span multiple months. It may return a `Promise`, in which
|
|
276
|
+
* case the affected dates render in a non-selectable pending state until it resolves.
|
|
277
|
+
*
|
|
278
|
+
* `disabled` from the metadata is combined with `min`, `max` and `isDateDisabled`: a date is
|
|
279
|
+
* disabled if it is out of range, or `isDateDisabled` returns `true`, or its metadata marks it
|
|
280
|
+
* disabled. `part` names are added to the date cell so a theme can style it via `::part()`.
|
|
281
|
+
*
|
|
282
|
+
* Both `disabled` and `part` are returned by this one function, rather than by separate
|
|
283
|
+
* generators, so a single backend query (for example in Flow) can answer both at once instead
|
|
284
|
+
* of being split into two passes over the same data.
|
|
285
|
+
*
|
|
286
|
+
* Keep a stable reference to the function. Assigning a new function resets the internal cache
|
|
287
|
+
* and re-fetches every visible range. To reload after the underlying data changes while keeping
|
|
288
|
+
* the same function, call `clearCache()`.
|
|
289
|
+
*/
|
|
290
|
+
dateMetadataProvider: (range: DatePickerDateRange) => DatePickerDateMetadata[] | Promise<DatePickerDateMetadata[]>;
|
|
291
|
+
|
|
239
292
|
/**
|
|
240
293
|
* Opens the dropdown.
|
|
241
294
|
*/
|
|
@@ -245,4 +298,11 @@ export declare class DatePickerMixinClass {
|
|
|
245
298
|
* Closes the dropdown.
|
|
246
299
|
*/
|
|
247
300
|
close(): void;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Clears the cached date metadata and reloads it from `dateMetadataProvider`. Call this when the
|
|
304
|
+
* data behind the provider has changed (for example a date became booked) so the calendar
|
|
305
|
+
* reflects it, without having to replace the provider function.
|
|
306
|
+
*/
|
|
307
|
+
clearCache(): void;
|
|
248
308
|
}
|
|
@@ -12,6 +12,7 @@ import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
|
|
|
12
12
|
import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
|
|
13
13
|
import { InputConstraintsMixin } from '@vaadin/field-base/src/input-constraints-mixin.js';
|
|
14
14
|
import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
|
|
15
|
+
import { DateMetadataController } from './vaadin-date-metadata-controller.js';
|
|
15
16
|
import {
|
|
16
17
|
dateAllowed,
|
|
17
18
|
dateEquals,
|
|
@@ -208,6 +209,39 @@ export const DatePickerMixin = (subclass) =>
|
|
|
208
209
|
type: Function,
|
|
209
210
|
},
|
|
210
211
|
|
|
212
|
+
/**
|
|
213
|
+
* A batch function that fetches metadata for a range of dates the calendar is about to
|
|
214
|
+
* render. It receives a `DatePickerDateRange` object (`{ start, end }` of `DatePickerDate`)
|
|
215
|
+
* and returns, or resolves with, an array of metadata objects — a `DatePickerDate` extended
|
|
216
|
+
* with metadata fields such as `disabled` and custom `part` names, e.g.
|
|
217
|
+
* `{ year, month, day, disabled: true, part: 'busy' }` — for the dates that have metadata
|
|
218
|
+
* within that range.
|
|
219
|
+
*
|
|
220
|
+
* Unlike `isDateDisabled`, which is called once per date, this function is called for a
|
|
221
|
+
* range of dates at a time, and again as the calendar renders further dates. The size of
|
|
222
|
+
* the range is decided by the calendar and may span multiple months. It may return a
|
|
223
|
+
* `Promise`, in which case the affected dates render in a non-selectable pending state
|
|
224
|
+
* until it resolves.
|
|
225
|
+
*
|
|
226
|
+
* `disabled` from the metadata is combined with `min`, `max` and `isDateDisabled`: a date
|
|
227
|
+
* is disabled if it is out of the min/max range, or `isDateDisabled` returns `true`, or its
|
|
228
|
+
* metadata marks it disabled. `part` names are added to the date cell's `part` attribute so
|
|
229
|
+
* a theme can style specific dates via `::part()`.
|
|
230
|
+
*
|
|
231
|
+
* Both `disabled` and `part` are returned by this one function, rather than by separate
|
|
232
|
+
* generators, so a single backend query (for example in Flow) can answer both at once
|
|
233
|
+
* instead of being split into two passes over the same data.
|
|
234
|
+
*
|
|
235
|
+
* Keep a stable reference to the function. Assigning a new function resets the internal
|
|
236
|
+
* cache and re-fetches every visible range. To reload after the underlying data changes
|
|
237
|
+
* while keeping the same function, call `clearCache()`.
|
|
238
|
+
*
|
|
239
|
+
* @type {function(DatePickerDateRange): Array<DatePickerDateMetadata> | Promise<Array<DatePickerDateMetadata>> | undefined}
|
|
240
|
+
*/
|
|
241
|
+
dateMetadataProvider: {
|
|
242
|
+
type: Function,
|
|
243
|
+
},
|
|
244
|
+
|
|
211
245
|
/**
|
|
212
246
|
* The earliest date that can be selected. All earlier dates will be disabled.
|
|
213
247
|
* @type {Date | undefined}
|
|
@@ -261,7 +295,8 @@ export const DatePickerMixin = (subclass) =>
|
|
|
261
295
|
return [
|
|
262
296
|
'_selectedDateChanged(_selectedDate, __effectiveI18n)',
|
|
263
297
|
'_focusedDateChanged(_focusedDate, __effectiveI18n)',
|
|
264
|
-
'__updateOverlayContent(_overlayContent, __effectiveI18n, label, _minDate, _maxDate, _focusedDate, _selectedDate, showWeekNumbers, isDateDisabled, __enteredDate)',
|
|
298
|
+
'__updateOverlayContent(_overlayContent, __effectiveI18n, label, _minDate, _maxDate, _focusedDate, _selectedDate, showWeekNumbers, isDateDisabled, dateMetadataProvider, __enteredDate)',
|
|
299
|
+
'__dateMetadataProviderChanged(dateMetadataProvider)',
|
|
265
300
|
'__updateOverlayContentTheme(_overlayContent, _theme)',
|
|
266
301
|
'__updateOverlayContentFullScreen(_overlayContent, _fullscreen)',
|
|
267
302
|
];
|
|
@@ -437,6 +472,13 @@ export const DatePickerMixin = (subclass) =>
|
|
|
437
472
|
|
|
438
473
|
this.addController(new VirtualKeyboardController(this));
|
|
439
474
|
|
|
475
|
+
// Owns the cache of dates resolved by `dateMetadataProvider`. It lives on the date-picker
|
|
476
|
+
// rather than the overlay content so validation works even when the overlay is never opened.
|
|
477
|
+
// The open overlay reads the same controller to render months and show the loading spinner.
|
|
478
|
+
this._dateMetadataController = new DateMetadataController(this, () => this.__onDateMetadataChanged());
|
|
479
|
+
this.addController(this._dateMetadataController);
|
|
480
|
+
this._dateMetadataController.setProvider(this.dateMetadataProvider);
|
|
481
|
+
|
|
440
482
|
this._overlayElement = this.$.overlay;
|
|
441
483
|
}
|
|
442
484
|
|
|
@@ -486,6 +528,26 @@ export const DatePickerMixin = (subclass) =>
|
|
|
486
528
|
this.$.overlay.close();
|
|
487
529
|
}
|
|
488
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Clears the cached date metadata and reloads it from `dateMetadataProvider`. Call this when
|
|
533
|
+
* the data behind the provider has changed (for example a date became booked) so the calendar
|
|
534
|
+
* reflects it, without having to replace the provider function.
|
|
535
|
+
*
|
|
536
|
+
* The visible range is reloaded when the overlay is open, and the selected value is
|
|
537
|
+
* re-validated once its month resolves.
|
|
538
|
+
*/
|
|
539
|
+
clearCache() {
|
|
540
|
+
const controller = this._dateMetadataController;
|
|
541
|
+
if (!controller) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Drops the cache and notifies, which re-renders the open overlay in the pending state and
|
|
545
|
+
// reloads its visible range (via __updateCalendars). Also reload the selected month so a
|
|
546
|
+
// value can be re-validated even while the overlay is closed.
|
|
547
|
+
controller.reset();
|
|
548
|
+
this.__ensureSelectedDateLoaded();
|
|
549
|
+
}
|
|
550
|
+
|
|
489
551
|
/** @private */
|
|
490
552
|
__ensureContent() {
|
|
491
553
|
if (this._overlayContent) {
|
|
@@ -578,7 +640,9 @@ export const DatePickerMixin = (subclass) =>
|
|
|
578
640
|
const inputValue = this._inputElementValue;
|
|
579
641
|
const inputValid = !inputValue || (!!this._selectedDate && inputValue === this.__formatDate(this._selectedDate));
|
|
580
642
|
const isDateValid =
|
|
581
|
-
!this._selectedDate ||
|
|
643
|
+
!this._selectedDate ||
|
|
644
|
+
(dateAllowed(this._selectedDate, this._minDate, this._maxDate, this.isDateDisabled) &&
|
|
645
|
+
!this.__isDateDisabledByProvider(this._selectedDate));
|
|
582
646
|
|
|
583
647
|
let inputValidity = true;
|
|
584
648
|
if (this.inputElement && this.inputElement.checkValidity) {
|
|
@@ -588,6 +652,93 @@ export const DatePickerMixin = (subclass) =>
|
|
|
588
652
|
return inputValid && isDateValid && inputValidity;
|
|
589
653
|
}
|
|
590
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Returns true if the given date is known to be disabled by `dateMetadataProvider`. The
|
|
657
|
+
* result comes from the controller's cache of already-loaded ranges. The month containing a
|
|
658
|
+
* selected date is loaded on demand (see `_selectedDateChanged`), so a value typed while the
|
|
659
|
+
* overlay is closed is re-validated once the provider answers (see `__onDateMetadataChanged`).
|
|
660
|
+
* Until then the date is treated as allowed, matching the overlay's rendering.
|
|
661
|
+
* @private
|
|
662
|
+
*/
|
|
663
|
+
__isDateDisabledByProvider(date) {
|
|
664
|
+
const controller = this._dateMetadataController;
|
|
665
|
+
return !!controller && !!controller.provider && controller.isDateDisabled(date);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** @private */
|
|
669
|
+
__dateMetadataProviderChanged(dateMetadataProvider) {
|
|
670
|
+
// The controller is created in `ready()`; `setProvider` is called there for the initial value.
|
|
671
|
+
if (this._dateMetadataController) {
|
|
672
|
+
this._dateMetadataController.setProvider(dateMetadataProvider);
|
|
673
|
+
this.__ensureSelectedDateLoaded();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Asks the controller to resolve the month containing the selected date, so that a value set
|
|
679
|
+
* or typed while the overlay is closed can be validated against the provider without opening
|
|
680
|
+
* the overlay. When the month resolves, `__onDateMetadataChanged` re-runs validation.
|
|
681
|
+
* @private
|
|
682
|
+
*/
|
|
683
|
+
__ensureSelectedDateLoaded() {
|
|
684
|
+
const controller = this._dateMetadataController;
|
|
685
|
+
if (controller?.provider && this._selectedDate && !controller.isMonthLoaded(this._selectedDate)) {
|
|
686
|
+
this.__awaitingProviderValidation = true;
|
|
687
|
+
controller.ensureRangeLoaded(this._selectedDate, this._selectedDate);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** @private */
|
|
692
|
+
__onDateMetadataChanged() {
|
|
693
|
+
const controller = this._dateMetadataController;
|
|
694
|
+
// Push the new loading and cache state to the open overlay so it re-renders the months and
|
|
695
|
+
// updates the spinner. When the overlay is closed there is nothing to update.
|
|
696
|
+
if (this._overlayContent) {
|
|
697
|
+
this._overlayContent.loading = controller.loading;
|
|
698
|
+
this._overlayContent._dateMetadataVersion += 1;
|
|
699
|
+
}
|
|
700
|
+
// Re-validate once the month containing the selected value has resolved, so a value typed
|
|
701
|
+
// while the picker was closed (autoOpenDisabled) is rejected as soon as the provider answers.
|
|
702
|
+
if (this.__awaitingProviderValidation && this._selectedDate && controller.isMonthLoaded(this._selectedDate)) {
|
|
703
|
+
this.__awaitingProviderValidation = false;
|
|
704
|
+
this._requestValidation();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
this.__adjustInitialFocusForProvider();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Moves the overlay's initial focus off a date the provider turns out to disable, once the
|
|
712
|
+
* provider has answered for that month. Only touches the auto-picked initial date and only
|
|
713
|
+
* while the user has not navigated away, so disabled dates the user focuses on purpose (which
|
|
714
|
+
* stay keyboard-focusable) are left alone.
|
|
715
|
+
* @private
|
|
716
|
+
*/
|
|
717
|
+
__adjustInitialFocusForProvider() {
|
|
718
|
+
const content = this._overlayContent;
|
|
719
|
+
const controller = this._dateMetadataController;
|
|
720
|
+
const initial = this.__initialFocusDate;
|
|
721
|
+
if (!content || !initial || !controller.provider) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
// The user has moved focus; stop trying to adjust the initial date.
|
|
725
|
+
if (!dateEquals(content.focusedDate, initial)) {
|
|
726
|
+
this.__initialFocusDate = null;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
// Wait until the provider has answered for the initial month.
|
|
730
|
+
if (!controller.isMonthLoaded(initial)) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.__initialFocusDate = null;
|
|
734
|
+
if (controller.isDateDisabled(initial)) {
|
|
735
|
+
const closest = content.__closestSelectableDate(initial);
|
|
736
|
+
if (closest) {
|
|
737
|
+
content.focusDate(closest);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
591
742
|
/**
|
|
592
743
|
* Override method inherited from `FocusMixin`
|
|
593
744
|
* to not call `_setFocused(true)` when focus
|
|
@@ -771,6 +922,10 @@ export const DatePickerMixin = (subclass) =>
|
|
|
771
922
|
this._applyInputValue(selectedDate);
|
|
772
923
|
}
|
|
773
924
|
|
|
925
|
+
// Preload the provider's answer for the selected month so the value can be validated even
|
|
926
|
+
// when the overlay is never opened.
|
|
927
|
+
this.__ensureSelectedDateLoaded();
|
|
928
|
+
|
|
774
929
|
this.value = this._formatISO(selectedDate);
|
|
775
930
|
this._ignoreFocusedDateChange = true;
|
|
776
931
|
this._focusedDate = selectedDate;
|
|
@@ -842,9 +997,13 @@ export const DatePickerMixin = (subclass) =>
|
|
|
842
997
|
selectedDate,
|
|
843
998
|
showWeekNumbers,
|
|
844
999
|
isDateDisabled,
|
|
1000
|
+
dateMetadataProvider,
|
|
845
1001
|
enteredDate,
|
|
846
1002
|
) {
|
|
847
1003
|
if (overlayContent) {
|
|
1004
|
+
// Share the date-picker's controller so the overlay renders from the same cache that
|
|
1005
|
+
// validation uses. Assigned before the other properties, which trigger `__updateCalendars`.
|
|
1006
|
+
overlayContent._dateMetadataController = this._dateMetadataController;
|
|
848
1007
|
overlayContent.i18n = effectiveI18n;
|
|
849
1008
|
overlayContent.label = label;
|
|
850
1009
|
overlayContent.minDate = minDate;
|
|
@@ -853,6 +1012,7 @@ export const DatePickerMixin = (subclass) =>
|
|
|
853
1012
|
overlayContent.selectedDate = selectedDate;
|
|
854
1013
|
overlayContent.showWeekNumbers = showWeekNumbers;
|
|
855
1014
|
overlayContent.isDateDisabled = isDateDisabled;
|
|
1015
|
+
overlayContent.dateMetadataProvider = dateMetadataProvider;
|
|
856
1016
|
overlayContent.enteredDate = enteredDate;
|
|
857
1017
|
}
|
|
858
1018
|
}
|
|
@@ -901,6 +1061,12 @@ export const DatePickerMixin = (subclass) =>
|
|
|
901
1061
|
content.focusedDate = scrollFocusDate;
|
|
902
1062
|
this._ignoreFocusedDateChange = false;
|
|
903
1063
|
|
|
1064
|
+
// When opening without a selected value, remember the auto-picked initial date so it can be
|
|
1065
|
+
// moved off a provider-disabled date once the provider answers (see __onDateMetadataChanged).
|
|
1066
|
+
// A date the user selected themselves is left in place even if the provider disables it.
|
|
1067
|
+
this.__initialFocusDate = this._selectedDate ? null : scrollFocusDate;
|
|
1068
|
+
this.__adjustInitialFocusForProvider();
|
|
1069
|
+
|
|
904
1070
|
window.addEventListener('scroll', this._boundOnScroll, true);
|
|
905
1071
|
|
|
906
1072
|
if (this._focusOverlayOnOpen) {
|
|
@@ -964,6 +1130,8 @@ export const DatePickerMixin = (subclass) =>
|
|
|
964
1130
|
|
|
965
1131
|
/** @protected */
|
|
966
1132
|
_onOverlayClosed() {
|
|
1133
|
+
this.__initialFocusDate = null;
|
|
1134
|
+
|
|
967
1135
|
// Reset `aria-hidden` state.
|
|
968
1136
|
if (this.__showOthers) {
|
|
969
1137
|
this.__showOthers();
|