@vaadin/date-time-picker 24.6.0-alpha7 → 24.6.0-alpha8

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.
@@ -0,0 +1,923 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2019 - 2024 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
7
+ import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
8
+ import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
9
+ import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
10
+ import {
11
+ dateEquals,
12
+ formatUTCISODate,
13
+ normalizeUTCDate,
14
+ parseUTCDate,
15
+ } from '@vaadin/date-picker/src/vaadin-date-picker-helper.js';
16
+ import { datePickerI18nDefaults } from '@vaadin/date-picker/src/vaadin-date-picker-mixin.js';
17
+ import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
18
+ import { formatISOTime, parseISOTime, validateTime } from '@vaadin/time-picker/src/vaadin-time-picker-helper.js';
19
+ import { timePickerI18nDefaults } from '@vaadin/time-picker/src/vaadin-time-picker-mixin.js';
20
+
21
+ const datePickerI18nProps = Object.keys(datePickerI18nDefaults);
22
+ const timePickerI18nProps = Object.keys(timePickerI18nDefaults);
23
+
24
+ /**
25
+ * A controller to initialize slotted picker.
26
+ *
27
+ * @private
28
+ */
29
+ class PickerSlotController extends SlotController {
30
+ constructor(host, type) {
31
+ super(host, `${type}-picker`, `vaadin-${type}-picker`, {
32
+ initializer: (picker, host) => {
33
+ // Ensure initial value-changed is fired on the slotted pickers
34
+ // synchronously in Lit version to avoid overriding host value.
35
+ if (picker.performUpdate) {
36
+ picker.performUpdate();
37
+ }
38
+ const prop = `__${type}Picker`;
39
+ host[prop] = picker;
40
+ },
41
+ });
42
+ }
43
+ }
44
+
45
+ /**
46
+ * A mixin providing common date-time-picker functionality.
47
+ *
48
+ * @polymerMixin
49
+ * @mixes DisabledMixin
50
+ * @mixes FieldMixin
51
+ * @mixes FocusMixin
52
+ */
53
+ export const DateTimePickerMixin = (superClass) =>
54
+ class DateTimePickerMixinClass extends FieldMixin(FocusMixin(DisabledMixin(superClass))) {
55
+ static get properties() {
56
+ return {
57
+ /**
58
+ * The name of the control, which is submitted with the form data.
59
+ */
60
+ name: {
61
+ type: String,
62
+ },
63
+
64
+ /**
65
+ * The value for this element.
66
+ *
67
+ * Supported date time format is based on ISO 8601 (without a time zone designator):
68
+ * - Minute precision `"YYYY-MM-DDThh:mm"` (default)
69
+ * - Second precision `"YYYY-MM-DDThh:mm:ss"`
70
+ * - Millisecond precision `"YYYY-MM-DDThh:mm:ss.fff"`
71
+ * @type {string}
72
+ */
73
+ value: {
74
+ type: String,
75
+ notify: true,
76
+ value: '',
77
+ observer: '__valueChanged',
78
+ sync: true,
79
+ },
80
+
81
+ /**
82
+ * The earliest allowed value (date and time) that can be selected. All earlier values will be disabled.
83
+ *
84
+ * Supported date time format is based on ISO 8601 (without a time zone designator):
85
+ * - Minute precision `"YYYY-MM-DDThh:mm"`
86
+ * - Second precision `"YYYY-MM-DDThh:mm:ss"`
87
+ * - Millisecond precision `"YYYY-MM-DDThh:mm:ss.fff"`
88
+ *
89
+ * @type {string | undefined}
90
+ */
91
+ min: {
92
+ type: String,
93
+ observer: '__minChanged',
94
+ sync: true,
95
+ },
96
+
97
+ /**
98
+ * The latest value (date and time) that can be selected. All later values will be disabled.
99
+ *
100
+ * Supported date time format is based on ISO 8601 (without a time zone designator):
101
+ * - Minute precision `"YYYY-MM-DDThh:mm"`
102
+ * - Second precision `"YYYY-MM-DDThh:mm:ss"`
103
+ * - Millisecond precision `"YYYY-MM-DDThh:mm:ss.fff"`
104
+ *
105
+ * @type {string | undefined}
106
+ */
107
+ max: {
108
+ type: String,
109
+ observer: '__maxChanged',
110
+ sync: true,
111
+ },
112
+
113
+ /**
114
+ * The earliest value that can be selected. All earlier values will be disabled.
115
+ * @private
116
+ */
117
+ __minDateTime: {
118
+ type: Date,
119
+ value: '',
120
+ sync: true,
121
+ },
122
+
123
+ /**
124
+ * The latest value that can be selected. All later values will be disabled.
125
+ * @private
126
+ */
127
+ __maxDateTime: {
128
+ type: Date,
129
+ value: '',
130
+ sync: true,
131
+ },
132
+
133
+ /**
134
+ * A placeholder string for the date field.
135
+ * @attr {string} date-placeholder
136
+ */
137
+ datePlaceholder: {
138
+ type: String,
139
+ sync: true,
140
+ },
141
+
142
+ /**
143
+ * A placeholder string for the time field.
144
+ * @attr {string} time-placeholder
145
+ */
146
+ timePlaceholder: {
147
+ type: String,
148
+ sync: true,
149
+ },
150
+
151
+ /**
152
+ * Defines the time interval (in seconds) between the items displayed
153
+ * in the time selection box. The default is 1 hour (i.e. `3600`).
154
+ *
155
+ * It also configures the precision of the time part of the value string. By default
156
+ * the component formats time values as `hh:mm` but setting a step value
157
+ * lower than one minute or one second, format resolution changes to
158
+ * `hh:mm:ss` and `hh:mm:ss.fff` respectively.
159
+ *
160
+ * Unit must be set in seconds, and for correctly configuring intervals
161
+ * in the dropdown, it need to evenly divide a day.
162
+ *
163
+ * Note: it is possible to define step that is dividing an hour in inexact
164
+ * fragments (i.e. 5760 seconds which equals 1 hour 36 minutes), but it is
165
+ * not recommended to use it for better UX.
166
+ */
167
+ step: {
168
+ type: Number,
169
+ sync: true,
170
+ },
171
+
172
+ /**
173
+ * Date which should be visible in the date picker overlay when there is no value selected.
174
+ *
175
+ * The same date formats as for the `value` property are supported but without the time part.
176
+ * @attr {string} initial-position
177
+ */
178
+ initialPosition: {
179
+ type: String,
180
+ sync: true,
181
+ },
182
+
183
+ /**
184
+ * Set true to display ISO-8601 week numbers in the calendar. Notice that
185
+ * displaying week numbers is only supported when `i18n.firstDayOfWeek`
186
+ * is 1 (Monday).
187
+ * @attr {boolean} show-week-numbers
188
+ */
189
+ showWeekNumbers: {
190
+ type: Boolean,
191
+ value: false,
192
+ sync: true,
193
+ },
194
+
195
+ /**
196
+ * Set to true to prevent the overlays from opening automatically.
197
+ * @attr {boolean} auto-open-disabled
198
+ */
199
+ autoOpenDisabled: {
200
+ type: Boolean,
201
+ sync: true,
202
+ },
203
+
204
+ /**
205
+ * Set to true to make this element read-only.
206
+ * @type {boolean}
207
+ */
208
+ readonly: {
209
+ type: Boolean,
210
+ value: false,
211
+ reflectToAttribute: true,
212
+ sync: true,
213
+ },
214
+
215
+ /**
216
+ * Specify that this control should have input focus when the page loads.
217
+ * @type {boolean}
218
+ */
219
+ autofocus: {
220
+ type: Boolean,
221
+ },
222
+
223
+ /**
224
+ * The current selected date time.
225
+ * @private
226
+ */
227
+ __selectedDateTime: {
228
+ type: Date,
229
+ sync: true,
230
+ },
231
+
232
+ /**
233
+ * The object used to localize this component.
234
+ * To change the default localization, replace the entire
235
+ * `i18n` object or just the properties you want to modify.
236
+ *
237
+ * The object is a combination of the i18n properties supported by
238
+ * [`<vaadin-date-picker>`](#/elements/vaadin-date-picker) and
239
+ * [`<vaadin-time-picker>`](#/elements/vaadin-time-picker).
240
+ * @type {!DateTimePickerI18n}
241
+ */
242
+ i18n: {
243
+ type: Object,
244
+ sync: true,
245
+ value: () => ({ ...datePickerI18nDefaults, ...timePickerI18nDefaults }),
246
+ },
247
+
248
+ /**
249
+ * A space-delimited list of CSS class names to set on the overlay elements
250
+ * of the internal components controlled by the `<vaadin-date-time-picker>`:
251
+ *
252
+ * - [`<vaadin-date-picker>`](#/elements/vaadin-date-picker#property-overlayClass)
253
+ * - [`<vaadin-time-picker>`](#/elements/vaadin-time-picker#property-overlayClass)
254
+ *
255
+ * @attr {string} overlay-class
256
+ */
257
+ overlayClass: {
258
+ type: String,
259
+ },
260
+
261
+ /**
262
+ * The current slotted date picker.
263
+ * @private
264
+ */
265
+ __datePicker: {
266
+ type: Object,
267
+ sync: true,
268
+ observer: '__datePickerChanged',
269
+ },
270
+
271
+ /**
272
+ * The current slotted time picker.
273
+ * @private
274
+ */
275
+ __timePicker: {
276
+ type: Object,
277
+ sync: true,
278
+ observer: '__timePickerChanged',
279
+ },
280
+ };
281
+ }
282
+
283
+ static get observers() {
284
+ return [
285
+ '__selectedDateTimeChanged(__selectedDateTime)',
286
+ '__datePlaceholderChanged(datePlaceholder, __datePicker)',
287
+ '__timePlaceholderChanged(timePlaceholder, __timePicker)',
288
+ '__stepChanged(step, __timePicker)',
289
+ '__initialPositionChanged(initialPosition, __datePicker)',
290
+ '__showWeekNumbersChanged(showWeekNumbers, __datePicker)',
291
+ '__requiredChanged(required, __datePicker, __timePicker)',
292
+ '__invalidChanged(invalid, __datePicker, __timePicker)',
293
+ '__disabledChanged(disabled, __datePicker, __timePicker)',
294
+ '__readonlyChanged(readonly, __datePicker, __timePicker)',
295
+ '__i18nChanged(i18n, __datePicker, __timePicker)',
296
+ '__autoOpenDisabledChanged(autoOpenDisabled, __datePicker, __timePicker)',
297
+ '__themeChanged(_theme, __datePicker, __timePicker)',
298
+ '__overlayClassChanged(overlayClass, __datePicker, __timePicker)',
299
+ '__pickersChanged(__datePicker, __timePicker)',
300
+ '__labelOrAccessibleNameChanged(label, accessibleName, i18n, __datePicker, __timePicker)',
301
+ ];
302
+ }
303
+
304
+ constructor() {
305
+ super();
306
+ // Default value for "min" and "max" properties of vaadin-date-picker (for removing constraint)
307
+ this.__defaultDateMinMaxValue = undefined;
308
+ // Default value for "min" property of vaadin-time-picker (for removing constraint)
309
+ this.__defaultTimeMinValue = '00:00:00.000';
310
+ // Default value for "max" property of vaadin-time-picker (for removing constraint)
311
+ this.__defaultTimeMaxValue = '23:59:59.999';
312
+
313
+ this.__changeEventHandler = this.__changeEventHandler.bind(this);
314
+ this.__valueChangedEventHandler = this.__valueChangedEventHandler.bind(this);
315
+ this.__openedChangedEventHandler = this.__openedChangedEventHandler.bind(this);
316
+ }
317
+
318
+ /** @private */
319
+ get __pickers() {
320
+ return [this.__datePicker, this.__timePicker];
321
+ }
322
+
323
+ /** @private */
324
+ get __formattedValue() {
325
+ const values = this.__pickers.map((picker) => picker.value);
326
+ return values.every(Boolean) ? values.join('T') : '';
327
+ }
328
+
329
+ /** @protected */
330
+ ready() {
331
+ super.ready();
332
+
333
+ this._datePickerController = new PickerSlotController(this, 'date');
334
+ this.addController(this._datePickerController);
335
+
336
+ this._timePickerController = new PickerSlotController(this, 'time');
337
+ this.addController(this._timePickerController);
338
+
339
+ if (this.autofocus && !this.disabled) {
340
+ window.requestAnimationFrame(() => this.focus());
341
+ }
342
+
343
+ this.setAttribute('role', 'group');
344
+
345
+ this._tooltipController = new TooltipController(this);
346
+ this.addController(this._tooltipController);
347
+ this._tooltipController.setPosition('top');
348
+ this._tooltipController.setShouldShow((target) => {
349
+ return target.__datePicker && !target.__datePicker.opened && target.__timePicker && !target.__timePicker.opened;
350
+ });
351
+
352
+ this.ariaTarget = this;
353
+ }
354
+
355
+ focus() {
356
+ if (this.__datePicker) {
357
+ this.__datePicker.focus();
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Override method inherited from `FocusMixin` to validate on blur.
363
+ * @param {boolean} focused
364
+ * @protected
365
+ * @override
366
+ */
367
+ _setFocused(focused) {
368
+ super._setFocused(focused);
369
+
370
+ // Do not validate when focusout is caused by document
371
+ // losing focus, which happens on browser tab switch.
372
+ if (!focused && document.hasFocus()) {
373
+ this._requestValidation();
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Override method inherited from `FocusMixin` to not remove focused
379
+ * state when focus moves between pickers or to the overlay.
380
+ * @param {FocusEvent} event
381
+ * @return {boolean}
382
+ * @protected
383
+ * @override
384
+ */
385
+ _shouldRemoveFocus(event) {
386
+ const target = event.relatedTarget;
387
+ const overlayContent = this.__datePicker._overlayContent;
388
+
389
+ if (
390
+ this.__datePicker.contains(target) ||
391
+ this.__timePicker.contains(target) ||
392
+ (overlayContent && overlayContent.contains(target))
393
+ ) {
394
+ return false;
395
+ }
396
+
397
+ return true;
398
+ }
399
+
400
+ /** @private */
401
+ __syncI18n(target, source, props = Object.keys(source.i18n)) {
402
+ const i18n = { ...target.i18n };
403
+ props.forEach((prop) => {
404
+ // eslint-disable-next-line no-prototype-builtins
405
+ if (source.i18n && source.i18n.hasOwnProperty(prop)) {
406
+ i18n[prop] = source.i18n[prop];
407
+ }
408
+ });
409
+ target.i18n = i18n;
410
+ }
411
+
412
+ /** @private */
413
+ __changeEventHandler(event) {
414
+ event.stopPropagation();
415
+
416
+ if (this.__dispatchChangeForValue === this.value) {
417
+ this._requestValidation();
418
+ this.__dispatchChange();
419
+ }
420
+ this.__dispatchChangeForValue = undefined;
421
+ }
422
+
423
+ /** @private */
424
+ __openedChangedEventHandler() {
425
+ const opened = this.__datePicker.opened || this.__timePicker.opened;
426
+ this.style.pointerEvents = opened ? 'auto' : '';
427
+ }
428
+
429
+ /** @private */
430
+ __addInputListeners(node) {
431
+ node.addEventListener('change', this.__changeEventHandler);
432
+ node.addEventListener('value-changed', this.__valueChangedEventHandler);
433
+ node.addEventListener('opened-changed', this.__openedChangedEventHandler);
434
+ }
435
+
436
+ /** @private */
437
+ __removeInputListeners(node) {
438
+ node.removeEventListener('change', this.__changeEventHandler);
439
+ node.removeEventListener('value-changed', this.__valueChangedEventHandler);
440
+ node.removeEventListener('opened-changed', this.__openedChangedEventHandler);
441
+ }
442
+
443
+ /** @private */
444
+ __isDefaultPicker(picker, type) {
445
+ const controller = this[`_${type}PickerController`];
446
+ return controller && picker === controller.defaultNode;
447
+ }
448
+
449
+ /** @private */
450
+ __datePickerChanged(newDatePicker, existingDatePicker) {
451
+ if (!newDatePicker) {
452
+ return;
453
+ }
454
+ if (existingDatePicker) {
455
+ // Remove an existing date picker
456
+ this.__removeInputListeners(existingDatePicker);
457
+ existingDatePicker.remove();
458
+ }
459
+
460
+ this.__addInputListeners(newDatePicker);
461
+
462
+ if (!this.__isDefaultPicker(newDatePicker, 'date')) {
463
+ // Synchronize properties from slotted date picker
464
+ this.datePlaceholder = newDatePicker.placeholder;
465
+ this.initialPosition = newDatePicker.initialPosition;
466
+ this.showWeekNumbers = newDatePicker.showWeekNumbers;
467
+ this.__syncI18n(this, newDatePicker, datePickerI18nProps);
468
+ }
469
+
470
+ // Min and max are always synchronized from date time picker (host) to inner fields because time picker
471
+ // min and max need to be dynamically set depending on currently selected date instead of simple propagation
472
+ newDatePicker.min = this.__formatDateISO(this.__minDateTime, this.__defaultDateMinMaxValue);
473
+ newDatePicker.max = this.__formatDateISO(this.__maxDateTime, this.__defaultDateMinMaxValue);
474
+
475
+ // Disable default internal validation for the component
476
+ newDatePicker.manualValidation = true;
477
+ }
478
+
479
+ /** @private */
480
+ __timePickerChanged(newTimePicker, existingTimePicker) {
481
+ if (!newTimePicker) {
482
+ return;
483
+ }
484
+ if (existingTimePicker) {
485
+ // Remove an existing time picker
486
+ this.__removeInputListeners(existingTimePicker);
487
+ existingTimePicker.remove();
488
+ }
489
+
490
+ this.__addInputListeners(newTimePicker);
491
+
492
+ if (!this.__isDefaultPicker(newTimePicker, 'time')) {
493
+ // Synchronize properties from slotted time picker
494
+ this.timePlaceholder = newTimePicker.placeholder;
495
+ this.step = newTimePicker.step;
496
+ this.__syncI18n(this, newTimePicker, timePickerI18nProps);
497
+ }
498
+
499
+ // Min and max are always synchronized from parent to slotted because time picker min and max
500
+ // need to be dynamically set depending on currently selected date instead of simple propagation
501
+ this.__updateTimePickerMinMax();
502
+
503
+ // Disable default internal validation for the component
504
+ newTimePicker.manualValidation = true;
505
+ }
506
+
507
+ /** @private */
508
+ __updateTimePickerMinMax() {
509
+ if (this.__timePicker && this.__datePicker) {
510
+ const selectedDate = this.__parseDate(this.__datePicker.value);
511
+ const isMinMaxSameDay = dateEquals(this.__minDateTime, this.__maxDateTime, normalizeUTCDate);
512
+
513
+ if ((this.__minDateTime && dateEquals(selectedDate, this.__minDateTime, normalizeUTCDate)) || isMinMaxSameDay) {
514
+ this.__timePicker.min = this.__dateToIsoTimeString(this.__minDateTime);
515
+ } else {
516
+ this.__timePicker.min = this.__defaultTimeMinValue;
517
+ }
518
+
519
+ if ((this.__maxDateTime && dateEquals(selectedDate, this.__maxDateTime, normalizeUTCDate)) || isMinMaxSameDay) {
520
+ this.__timePicker.max = this.__dateToIsoTimeString(this.__maxDateTime);
521
+ } else {
522
+ this.__timePicker.max = this.__defaultTimeMaxValue;
523
+ }
524
+ }
525
+ }
526
+
527
+ /** @private */
528
+ __i18nChanged(_i18n, datePicker, timePicker) {
529
+ if (datePicker) {
530
+ this.__syncI18n(datePicker, this, datePickerI18nProps);
531
+ }
532
+
533
+ if (timePicker) {
534
+ this.__syncI18n(timePicker, this, timePickerI18nProps);
535
+ }
536
+ }
537
+
538
+ /** @private */
539
+ __labelOrAccessibleNameChanged(label, accessibleName, i18n, datePicker, timePicker) {
540
+ const name = accessibleName || label || '';
541
+
542
+ if (datePicker) {
543
+ datePicker.accessibleName = `${name} ${i18n.dateLabel || ''}`.trim();
544
+ }
545
+
546
+ if (timePicker) {
547
+ timePicker.accessibleName = `${name} ${i18n.timeLabel || ''}`.trim();
548
+ }
549
+ }
550
+
551
+ /** @private */
552
+ __datePlaceholderChanged(datePlaceholder, datePicker) {
553
+ if (datePicker) {
554
+ datePicker.placeholder = datePlaceholder;
555
+ }
556
+ }
557
+
558
+ /** @private */
559
+ __timePlaceholderChanged(timePlaceholder, timePicker) {
560
+ if (timePicker) {
561
+ timePicker.placeholder = timePlaceholder;
562
+ }
563
+ }
564
+
565
+ /** @private */
566
+ __stepChanged(step, timePicker) {
567
+ if (timePicker && timePicker.step !== step) {
568
+ timePicker.step = step;
569
+ }
570
+ }
571
+
572
+ /** @private */
573
+ __initialPositionChanged(initialPosition, datePicker) {
574
+ if (datePicker) {
575
+ datePicker.initialPosition = initialPosition;
576
+ }
577
+ }
578
+
579
+ /** @private */
580
+ __showWeekNumbersChanged(showWeekNumbers, datePicker) {
581
+ if (datePicker) {
582
+ datePicker.showWeekNumbers = showWeekNumbers;
583
+ }
584
+ }
585
+
586
+ /** @private */
587
+ __invalidChanged(invalid, datePicker, timePicker) {
588
+ if (datePicker) {
589
+ datePicker.invalid = invalid;
590
+ }
591
+ if (timePicker) {
592
+ timePicker.invalid = invalid;
593
+ }
594
+ }
595
+
596
+ /** @private */
597
+ __requiredChanged(required, datePicker, timePicker) {
598
+ if (datePicker) {
599
+ datePicker.required = required;
600
+ }
601
+ if (timePicker) {
602
+ timePicker.required = required;
603
+ }
604
+
605
+ if (this.__oldRequired && !required) {
606
+ this._requestValidation();
607
+ }
608
+
609
+ this.__oldRequired = required;
610
+ }
611
+
612
+ /** @private */
613
+ __disabledChanged(disabled, datePicker, timePicker) {
614
+ if (datePicker) {
615
+ datePicker.disabled = disabled;
616
+ }
617
+ if (timePicker) {
618
+ timePicker.disabled = disabled;
619
+ }
620
+ }
621
+
622
+ /** @private */
623
+ __readonlyChanged(readonly, datePicker, timePicker) {
624
+ if (datePicker) {
625
+ datePicker.readonly = readonly;
626
+ }
627
+ if (timePicker) {
628
+ timePicker.readonly = readonly;
629
+ }
630
+ }
631
+
632
+ /**
633
+ * String (ISO date) to Date object
634
+ * @param {string} str e.g. 'yyyy-mm-dd'
635
+ * @return {Date | undefined}
636
+ * @private
637
+ */
638
+ __parseDate(str) {
639
+ return parseUTCDate(str);
640
+ }
641
+
642
+ /**
643
+ * Date object to string (ISO date)
644
+ * @param {Date} date
645
+ * @param {string} defaultValue
646
+ * @return {string} e.g. 'yyyy-mm-dd' (or defaultValue when date is falsy)
647
+ * @private
648
+ */
649
+ __formatDateISO(date, defaultValue) {
650
+ if (!date) {
651
+ return defaultValue;
652
+ }
653
+ return formatUTCISODate(date);
654
+ }
655
+
656
+ /**
657
+ * String (ISO date time) to Date object
658
+ * @param {string} str e.g. 'yyyy-mm-ddThh:mm', 'yyyy-mm-ddThh:mm:ss', 'yyyy-mm-ddThh:mm:ss.fff'
659
+ * @return {Date | undefined}
660
+ * @private
661
+ */
662
+ __parseDateTime(str) {
663
+ const [dateValue, timeValue] = str.split('T');
664
+ /* c8 ignore next 3 */
665
+ if (!(dateValue && timeValue)) {
666
+ return;
667
+ }
668
+
669
+ /** @type {Date} */
670
+ const date = this.__parseDate(dateValue);
671
+ if (!date) {
672
+ return;
673
+ }
674
+
675
+ const time = parseISOTime(timeValue);
676
+ if (!time) {
677
+ return;
678
+ }
679
+
680
+ date.setUTCHours(parseInt(time.hours));
681
+ date.setUTCMinutes(parseInt(time.minutes || 0));
682
+ date.setUTCSeconds(parseInt(time.seconds || 0));
683
+ date.setUTCMilliseconds(parseInt(time.milliseconds || 0));
684
+
685
+ return date;
686
+ }
687
+
688
+ /**
689
+ * Date object to string (ISO date time)
690
+ * @param {Date} date
691
+ * @return {string} e.g. 'yyyy-mm-ddThh:mm', 'yyyy-mm-ddThh:mm:ss', 'yyyy-mm-ddThh:mm:ss.fff'
692
+ * (depending on precision defined by "step" property)
693
+ * @private
694
+ */
695
+ __formatDateTime(date) {
696
+ if (!date) {
697
+ return '';
698
+ }
699
+ const dateValue = this.__formatDateISO(date, '');
700
+ const timeValue = this.__dateToIsoTimeString(date);
701
+ return `${dateValue}T${timeValue}`;
702
+ }
703
+
704
+ /**
705
+ * Date object to string (ISO time)
706
+ * @param {Date} date
707
+ * @return {string} e.g. 'hh:mm', 'hh:mm:ss', 'hh:mm:ss.fff' (depending on precision defined by "step" property)
708
+ * @private
709
+ */
710
+ __dateToIsoTimeString(date) {
711
+ return formatISOTime(
712
+ validateTime(
713
+ {
714
+ hours: date.getUTCHours(),
715
+ minutes: date.getUTCMinutes(),
716
+ seconds: date.getUTCSeconds(),
717
+ milliseconds: date.getUTCMilliseconds(),
718
+ },
719
+ this.step,
720
+ ),
721
+ );
722
+ }
723
+
724
+ /**
725
+ * Returns true if the current input value satisfies all constraints (if any)
726
+ *
727
+ * You can override the `checkValidity` method for custom validations.
728
+ * @return {boolean}
729
+ */
730
+ checkValidity() {
731
+ const hasInvalidPickers = this.__pickers.some((picker) => !picker.checkValidity());
732
+ const hasEmptyRequiredPickers = this.required && this.__pickers.some((picker) => !picker.value);
733
+ return !hasInvalidPickers && !hasEmptyRequiredPickers;
734
+ }
735
+
736
+ /**
737
+ * @param {Date} date1
738
+ * @param {Date} date2
739
+ * @return {boolean}
740
+ * @private
741
+ */
742
+ __dateTimeEquals(date1, date2) {
743
+ if (!dateEquals(date1, date2, normalizeUTCDate)) {
744
+ return false;
745
+ }
746
+ return (
747
+ date1.getUTCHours() === date2.getUTCHours() &&
748
+ date1.getUTCMinutes() === date2.getUTCMinutes() &&
749
+ date1.getUTCSeconds() === date2.getUTCSeconds() &&
750
+ date1.getUTCMilliseconds() === date2.getUTCMilliseconds()
751
+ );
752
+ }
753
+
754
+ /** @private */
755
+ __handleDateTimeChange(property, parsedProperty, value, oldValue) {
756
+ if (!value) {
757
+ this[property] = '';
758
+ this[parsedProperty] = '';
759
+ return;
760
+ }
761
+
762
+ const dateTime = this.__parseDateTime(value);
763
+ if (!dateTime) {
764
+ // Invalid date, revert to old value
765
+ this[property] = oldValue;
766
+ return;
767
+ }
768
+ if (!this.__dateTimeEquals(this[parsedProperty], dateTime)) {
769
+ this[parsedProperty] = dateTime;
770
+ }
771
+ }
772
+
773
+ /** @private */
774
+ __valueChanged(value, oldValue) {
775
+ this.__handleDateTimeChange('value', '__selectedDateTime', value, oldValue);
776
+
777
+ if (oldValue !== undefined) {
778
+ this.__dispatchChangeForValue = value;
779
+ }
780
+
781
+ this.toggleAttribute('has-value', !!value);
782
+ this.__updateTimePickerMinMax();
783
+ }
784
+
785
+ /** @private */
786
+ __dispatchChange() {
787
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
788
+ }
789
+
790
+ /** @private */
791
+ __minChanged(value, oldValue) {
792
+ this.__handleDateTimeChange('min', '__minDateTime', value, oldValue);
793
+ if (this.__datePicker) {
794
+ this.__datePicker.min = this.__formatDateISO(this.__minDateTime, this.__defaultDateMinMaxValue);
795
+ }
796
+ this.__updateTimePickerMinMax();
797
+
798
+ if (this.__datePicker && this.__timePicker && this.value) {
799
+ this._requestValidation();
800
+ }
801
+ }
802
+
803
+ /** @private */
804
+ __maxChanged(value, oldValue) {
805
+ this.__handleDateTimeChange('max', '__maxDateTime', value, oldValue);
806
+ if (this.__datePicker) {
807
+ this.__datePicker.max = this.__formatDateISO(this.__maxDateTime, this.__defaultDateMinMaxValue);
808
+ }
809
+ this.__updateTimePickerMinMax();
810
+
811
+ if (this.__datePicker && this.__timePicker && this.value) {
812
+ this._requestValidation();
813
+ }
814
+ }
815
+
816
+ /** @private */
817
+ __selectedDateTimeChanged(selectedDateTime) {
818
+ const formattedValue = this.__formatDateTime(selectedDateTime);
819
+ if (this.value !== formattedValue) {
820
+ this.value = formattedValue;
821
+ }
822
+
823
+ // Setting the date/time picker value below triggers validation of the components.
824
+ // If the inputs are slotted (e.g. when using the Java API) and have an initial value this can
825
+ // happen before date picker ready() which would throw an error when date picker is trying to read
826
+ // `this.$.input` (as a result of value change triggered by setting the value).
827
+ // Workaround the problem by setting custom field value only if date picker is ready.
828
+ const isDatePickerReady = Boolean(this.__datePicker && this.__datePicker.$);
829
+ if (isDatePickerReady && !this.__ignoreInputValueChange) {
830
+ // Ignore value changes until both inputs have a value updated
831
+ // TODO: This logic clears both fields if one of them is cleared :(
832
+ this.__ignoreInputValueChange = true;
833
+ const [dateValue, timeValue] = this.value.split('T');
834
+ this.__datePicker.value = dateValue || '';
835
+ this.__timePicker.value = timeValue || '';
836
+ this.__ignoreInputValueChange = false;
837
+ }
838
+ }
839
+
840
+ /** @private */
841
+ __valueChangedEventHandler() {
842
+ if (this.__ignoreInputValueChange) {
843
+ return;
844
+ }
845
+
846
+ this.__ignoreInputValueChange = true;
847
+ this.__updateTimePickerMinMax();
848
+ this.value = this.__formattedValue;
849
+ this.__ignoreInputValueChange = false;
850
+ }
851
+
852
+ /** @private */
853
+ __autoOpenDisabledChanged(autoOpenDisabled, datePicker, timePicker) {
854
+ if (datePicker) {
855
+ datePicker.autoOpenDisabled = autoOpenDisabled;
856
+ }
857
+ if (timePicker) {
858
+ timePicker.autoOpenDisabled = autoOpenDisabled;
859
+ }
860
+ }
861
+
862
+ /** @private */
863
+ __themeChanged(theme, datePicker, timePicker) {
864
+ if (!datePicker || !timePicker) {
865
+ // Both pickers are not ready yet
866
+ return;
867
+ }
868
+
869
+ [datePicker, timePicker].forEach((picker) => {
870
+ if (theme) {
871
+ picker.setAttribute('theme', theme);
872
+ } else {
873
+ picker.removeAttribute('theme');
874
+ }
875
+ });
876
+ }
877
+
878
+ /** @private */
879
+ __overlayClassChanged(overlayClass, datePicker, timePicker) {
880
+ if (!datePicker || !timePicker) {
881
+ // Both pickers are not ready yet
882
+ return;
883
+ }
884
+
885
+ datePicker.overlayClass = overlayClass;
886
+ timePicker.overlayClass = overlayClass;
887
+ }
888
+
889
+ /** @private */
890
+ __pickersChanged(datePicker, timePicker) {
891
+ if (!datePicker || !timePicker) {
892
+ // Both pickers are not ready yet
893
+ return;
894
+ }
895
+
896
+ if (this.__isDefaultPicker(datePicker, 'date') !== this.__isDefaultPicker(timePicker, 'time')) {
897
+ // Both pickers are not replaced yet
898
+ return;
899
+ }
900
+
901
+ if (datePicker.value) {
902
+ // The new pickers have a value, update the component value
903
+ this.__valueChangedEventHandler();
904
+ } else if (this.value) {
905
+ // The component has a value, update the new pickers values
906
+ this.__selectedDateTimeChanged(this.__selectedDateTime);
907
+
908
+ // When using Polymer version, mix and max observers are triggered initially
909
+ // before `ready()` and by that time pickers are not yet initialized, so we
910
+ // run initial validation here. Lit version runs observers differently and
911
+ // this observer is executed first - ignore it to prevent validating twice.
912
+ if ((this.min && this.__minDateTime) || (this.max && this.__maxDateTime)) {
913
+ this._requestValidation();
914
+ }
915
+ }
916
+ }
917
+
918
+ /**
919
+ * Fired when the user commits a value change.
920
+ *
921
+ * @event change
922
+ */
923
+ };