@vaadin/date-picker 24.2.0-alpha6 → 24.2.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.
@@ -8,90 +8,28 @@ import './vaadin-month-calendar.js';
8
8
  import './vaadin-date-picker-month-scroller.js';
9
9
  import './vaadin-date-picker-year-scroller.js';
10
10
  import './vaadin-date-picker-year.js';
11
- import { flush } from '@polymer/polymer/lib/utils/flush.js';
12
- import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
13
11
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
14
- import { timeOut } from '@vaadin/component-base/src/async.js';
15
12
  import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
16
- import { Debouncer } from '@vaadin/component-base/src/debounce.js';
17
13
  import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
18
- import { addListener, setTouchAction } from '@vaadin/component-base/src/gestures.js';
19
- import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
20
- import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
21
- import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
22
- import { dateAfterXMonths, dateEquals, extractDateParts, getClosestDate } from './vaadin-date-picker-helper.js';
14
+ import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
15
+ import { DatePickerOverlayContentMixin } from './vaadin-date-picker-overlay-content-mixin.js';
16
+ import { overlayContentStyles } from './vaadin-date-picker-overlay-content-styles.js';
17
+
18
+ registerStyles('vaadin-date-picker-overlay-content', overlayContentStyles, {
19
+ moduleId: 'vaadin-date-picker-overlay-content-styles',
20
+ });
23
21
 
24
22
  /**
25
23
  * @extends HTMLElement
26
24
  * @private
27
25
  */
28
- class DatePickerOverlayContent extends ControllerMixin(ThemableMixin(DirMixin(PolymerElement))) {
26
+ class DatePickerOverlayContent extends DatePickerOverlayContentMixin(
27
+ ControllerMixin(ThemableMixin(DirMixin(PolymerElement))),
28
+ ) {
29
29
  static get template() {
30
30
  return html`
31
- <style>
32
- :host {
33
- display: flex;
34
- flex-direction: column;
35
- height: 100%;
36
- width: 100%;
37
- outline: none;
38
- }
39
-
40
- [part='overlay-header'] {
41
- display: flex;
42
- flex-shrink: 0;
43
- flex-wrap: nowrap;
44
- align-items: center;
45
- }
46
-
47
- :host(:not([fullscreen])) [part='overlay-header'] {
48
- display: none;
49
- }
50
-
51
- [part='label'] {
52
- flex-grow: 1;
53
- }
54
-
55
- [hidden] {
56
- display: none !important;
57
- }
58
-
59
- [part='years-toggle-button'] {
60
- display: flex;
61
- }
62
-
63
- #scrollers {
64
- display: flex;
65
- height: 100%;
66
- width: 100%;
67
- position: relative;
68
- overflow: hidden;
69
- }
70
-
71
- :host([desktop]) ::slotted([slot='months']) {
72
- right: 50px;
73
- transform: none !important;
74
- }
75
-
76
- :host([desktop]) ::slotted([slot='years']) {
77
- transform: none !important;
78
- }
79
-
80
- :host(.animate) ::slotted([slot='months']),
81
- :host(.animate) ::slotted([slot='years']) {
82
- transition: all 200ms;
83
- }
84
-
85
- [part='toolbar'] {
86
- display: flex;
87
- justify-content: space-between;
88
- z-index: 2;
89
- flex-shrink: 0;
90
- }
91
- </style>
92
-
93
31
  <div part="overlay-header" on-touchend="_preventDefault" aria-hidden="true">
94
- <div part="label">[[_formatDisplayed(selectedDate, i18n.formatDate, label)]]</div>
32
+ <div part="label">[[_formatDisplayed(selectedDate, i18n, label)]]</div>
95
33
  <div part="clear-button" hidden$="[[!selectedDate]]"></div>
96
34
  <div part="toggle-button"></div>
97
35
 
@@ -116,950 +54,14 @@ class DatePickerOverlayContent extends ControllerMixin(ThemableMixin(DirMixin(Po
116
54
  return 'vaadin-date-picker-overlay-content';
117
55
  }
118
56
 
119
- static get properties() {
120
- return {
121
- scrollDuration: {
122
- type: Number,
123
- value: 300,
124
- },
125
-
126
- /**
127
- * The value for this element.
128
- */
129
- selectedDate: {
130
- type: Date,
131
- value: null,
132
- },
133
-
134
- /**
135
- * Date value which is focused using keyboard.
136
- */
137
- focusedDate: {
138
- type: Date,
139
- notify: true,
140
- observer: '_focusedDateChanged',
141
- },
142
-
143
- _focusedMonthDate: Number,
144
-
145
- /**
146
- * Date which should be visible when there is no value selected.
147
- */
148
- initialPosition: {
149
- type: Date,
150
- observer: '_initialPositionChanged',
151
- },
152
-
153
- _originDate: {
154
- value: new Date(),
155
- },
156
-
157
- _visibleMonthIndex: Number,
158
-
159
- _desktopMode: {
160
- type: Boolean,
161
- observer: '_desktopModeChanged',
162
- },
163
-
164
- _desktopMediaQuery: {
165
- type: String,
166
- value: '(min-width: 375px)',
167
- },
168
-
169
- _translateX: {
170
- observer: '_translateXChanged',
171
- },
172
-
173
- _yearScrollerWidth: {
174
- value: 50,
175
- },
176
-
177
- i18n: {
178
- type: Object,
179
- },
180
-
181
- showWeekNumbers: {
182
- type: Boolean,
183
- value: false,
184
- },
185
-
186
- _ignoreTaps: Boolean,
187
-
188
- _notTapping: Boolean,
189
-
190
- /**
191
- * The earliest date that can be selected. All earlier dates will be disabled.
192
- */
193
- minDate: Date,
194
-
195
- /**
196
- * The latest date that can be selected. All later dates will be disabled.
197
- */
198
- maxDate: Date,
199
-
200
- /**
201
- * Input label
202
- */
203
- label: String,
204
-
205
- _cancelButton: {
206
- type: Object,
207
- },
208
-
209
- _todayButton: {
210
- type: Object,
211
- },
212
-
213
- calendars: {
214
- type: Array,
215
- value: () => [],
216
- },
217
-
218
- years: {
219
- type: Array,
220
- value: () => [],
221
- },
222
- };
223
- }
224
-
225
- static get observers() {
226
- return [
227
- '__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme)',
228
- '__updateCancelButton(_cancelButton, i18n)',
229
- '__updateTodayButton(_todayButton, i18n, minDate, maxDate)',
230
- '__updateYears(years, selectedDate, _theme)',
231
- ];
232
- }
233
-
234
- /**
235
- * Whether to scroll to a sub-month position when scrolling to a date.
236
- * This is active if the month scroller is not large enough to fit a
237
- * full month. In that case we want to scroll to a position between
238
- * two months in order to have the focused date in the visible area.
239
- * @returns {boolean} whether to use sub-month scrolling
240
- * @private
241
- */
242
- get __useSubMonthScrolling() {
243
- return this._monthScroller.clientHeight < this._monthScroller.itemHeight + this._monthScroller.bufferOffset;
244
- }
245
-
246
- get focusableDateElement() {
247
- return this.calendars.map((calendar) => calendar.focusableDateElement).find(Boolean);
248
- }
249
-
57
+ /** @protected */
250
58
  ready() {
251
59
  super.ready();
252
60
 
253
61
  this.setAttribute('role', 'dialog');
254
62
 
255
- addListener(this.$.scrollers, 'track', this._track.bind(this));
256
- addListener(this.shadowRoot.querySelector('[part="clear-button"]'), 'tap', this._clear.bind(this));
257
- addListener(this.shadowRoot.querySelector('[part="toggle-button"]'), 'tap', this._cancel.bind(this));
258
- addListener(
259
- this.shadowRoot.querySelector('[part="years-toggle-button"]'),
260
- 'tap',
261
- this._toggleYearScroller.bind(this),
262
- );
263
-
264
- this.addController(
265
- new MediaQueryController(this._desktopMediaQuery, (matches) => {
266
- this._desktopMode = matches;
267
- }),
268
- );
269
-
270
- this.addController(
271
- new SlotController(this, 'today-button', 'vaadin-button', {
272
- observe: false,
273
- initializer: (btn) => {
274
- btn.setAttribute('theme', 'tertiary');
275
- btn.addEventListener('keydown', (e) => this.__onTodayButtonKeyDown(e));
276
- addListener(btn, 'tap', this._onTodayTap.bind(this));
277
- this._todayButton = btn;
278
- },
279
- }),
280
- );
281
-
282
- this.addController(
283
- new SlotController(this, 'cancel-button', 'vaadin-button', {
284
- observe: false,
285
- initializer: (btn) => {
286
- btn.setAttribute('theme', 'tertiary');
287
- btn.addEventListener('keydown', (e) => this.__onCancelButtonKeyDown(e));
288
- addListener(btn, 'tap', this._cancel.bind(this));
289
- this._cancelButton = btn;
290
- },
291
- }),
292
- );
293
-
294
- this.__initMonthScroller();
295
- this.__initYearScroller();
296
- }
297
-
298
- /**
299
- * Fired when the scroller reaches the target scrolling position.
300
- * @event scroll-animation-finished
301
- * @param {Number} detail.position new position
302
- * @param {Number} detail.oldPosition old position
303
- */
304
-
305
- connectedCallback() {
306
- super.connectedCallback();
307
- this._closeYearScroller();
308
- this._toggleAnimateClass(true);
309
- setTouchAction(this.$.scrollers, 'pan-y');
310
- }
311
-
312
- /**
313
- * Focuses the cancel button
314
- */
315
- focusCancel() {
316
- this._cancelButton.focus();
317
- }
318
-
319
- /**
320
- * Scrolls the list to the given Date.
321
- */
322
- scrollToDate(date, animate) {
323
- const offset = this.__useSubMonthScrolling ? this._calculateWeekScrollOffset(date) : 0;
324
- this._scrollToPosition(this._differenceInMonths(date, this._originDate) + offset, animate);
325
- this._monthScroller.forceUpdate();
326
- }
327
-
328
- __initMonthScroller() {
329
- this.addController(
330
- new SlotController(this, 'months', 'vaadin-date-picker-month-scroller', {
331
- observe: false,
332
- initializer: (scroller) => {
333
- scroller.addEventListener('custom-scroll', () => {
334
- this._onMonthScroll();
335
- });
336
-
337
- scroller.addEventListener('touchstart', () => {
338
- this._onMonthScrollTouchStart();
339
- });
340
-
341
- scroller.addEventListener('keydown', (e) => {
342
- this.__onMonthCalendarKeyDown(e);
343
- });
344
-
345
- scroller.addEventListener('init-done', () => {
346
- const calendars = [...this.querySelectorAll('vaadin-month-calendar')];
347
-
348
- // Two-way binding for selectedDate property
349
- calendars.forEach((calendar) => {
350
- calendar.addEventListener('selected-date-changed', (e) => {
351
- this.selectedDate = e.detail.value;
352
- });
353
- });
354
-
355
- this.calendars = calendars;
356
- });
357
-
358
- this._monthScroller = scroller;
359
- },
360
- }),
361
- );
362
- }
363
-
364
- __initYearScroller() {
365
- this.addController(
366
- new SlotController(this, 'years', 'vaadin-date-picker-year-scroller', {
367
- observe: false,
368
- initializer: (scroller) => {
369
- scroller.setAttribute('aria-hidden', 'true');
370
-
371
- addListener(scroller, 'tap', (e) => {
372
- this._onYearTap(e);
373
- });
374
-
375
- scroller.addEventListener('custom-scroll', () => {
376
- this._onYearScroll();
377
- });
378
-
379
- scroller.addEventListener('touchstart', () => {
380
- this._onYearScrollTouchStart();
381
- });
382
-
383
- scroller.addEventListener('init-done', () => {
384
- this.years = [...this.querySelectorAll('vaadin-date-picker-year')];
385
- });
386
-
387
- this._yearScroller = scroller;
388
- },
389
- }),
390
- );
391
- }
392
-
393
- __updateCancelButton(cancelButton, i18n) {
394
- if (cancelButton) {
395
- cancelButton.textContent = i18n && i18n.cancel;
396
- }
397
- }
398
-
399
- __updateTodayButton(todayButton, i18n, minDate, maxDate) {
400
- if (todayButton) {
401
- todayButton.textContent = i18n && i18n.today;
402
- todayButton.disabled = !this._isTodayAllowed(minDate, maxDate);
403
- }
404
- }
405
-
406
- // eslint-disable-next-line max-params
407
- __updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, ignoreTaps, theme) {
408
- if (calendars && calendars.length) {
409
- calendars.forEach((calendar) => {
410
- calendar.setProperties({
411
- i18n,
412
- minDate,
413
- maxDate,
414
- focusedDate,
415
- selectedDate,
416
- showWeekNumbers,
417
- ignoreTaps,
418
- });
419
-
420
- if (theme) {
421
- calendar.setAttribute('theme', theme);
422
- } else {
423
- calendar.removeAttribute('theme');
424
- }
425
- });
426
- }
427
- }
428
-
429
- __updateYears(years, selectedDate, theme) {
430
- if (years && years.length) {
431
- years.forEach((year) => {
432
- year.selectedDate = selectedDate;
433
-
434
- if (theme) {
435
- year.setAttribute('theme', theme);
436
- } else {
437
- year.removeAttribute('theme');
438
- }
439
- });
440
- }
441
- }
442
-
443
- /**
444
- * Select a date and fire event indicating user interaction.
445
- * @protected
446
- */
447
- _selectDate(dateToSelect) {
448
- this.selectedDate = dateToSelect;
449
- this.dispatchEvent(
450
- new CustomEvent('date-selected', { detail: { date: dateToSelect }, bubbles: true, composed: true }),
451
- );
452
- }
453
-
454
- _desktopModeChanged(desktopMode) {
455
- this.toggleAttribute('desktop', desktopMode);
456
- }
457
-
458
- _focusedDateChanged(focusedDate) {
459
- this.revealDate(focusedDate);
460
- }
461
-
462
- /**
463
- * Scrolls the month and year scrollers enough to reveal the given date.
464
- */
465
- revealDate(date, animate = true) {
466
- if (!date) {
467
- return;
468
- }
469
- const diff = this._differenceInMonths(date, this._originDate);
470
- // If scroll area does not fit the full month, then always scroll with an offset to
471
- // approximately display the week of the date
472
- if (this.__useSubMonthScrolling) {
473
- const offset = this._calculateWeekScrollOffset(date);
474
- this._scrollToPosition(diff + offset, animate);
475
- return;
476
- }
477
-
478
- // Otherwise determine if we need to scroll to make the month of the date visible
479
- const scrolledAboveViewport = this._monthScroller.position > diff;
480
-
481
- const visibleArea = Math.max(
482
- this._monthScroller.itemHeight,
483
- this._monthScroller.clientHeight - this._monthScroller.bufferOffset * 2,
484
- );
485
- const visibleItems = visibleArea / this._monthScroller.itemHeight;
486
- const scrolledBelowViewport = this._monthScroller.position + visibleItems - 1 < diff;
487
-
488
- if (scrolledAboveViewport) {
489
- this._scrollToPosition(diff, animate);
490
- } else if (scrolledBelowViewport) {
491
- this._scrollToPosition(diff - visibleItems + 1, animate);
492
- }
493
- }
494
-
495
- /**
496
- * Calculates an offset to be added to the month scroll position
497
- * when using sub-month scrolling, in order ensure that the week
498
- * that the date is in is visible even for small scroll areas.
499
- * As the month scroller uses a month as minimal scroll unit
500
- * (a value of `1` equals one month), we can not exactly identify
501
- * the position of a specific week. This is a best effort
502
- * implementation based on manual testing.
503
- * @param date the date for which to calculate the offset
504
- * @returns {number} the offset
505
- * @private
506
- */
507
- _calculateWeekScrollOffset(date) {
508
- // Get first day of month
509
- const temp = new Date(0, 0);
510
- temp.setFullYear(date.getFullYear());
511
- temp.setMonth(date.getMonth());
512
- temp.setDate(1);
513
- // Determine week (=row index) of date within the month
514
- let week = 0;
515
- while (temp.getDate() < date.getDate()) {
516
- temp.setDate(temp.getDate() + 1);
517
- if (temp.getDay() === this.i18n.firstDayOfWeek) {
518
- week += 1;
519
- }
520
- }
521
- // Calculate magic number that approximately keeps the week visible
522
- return week / 6;
523
- }
524
-
525
- _initialPositionChanged(initialPosition) {
526
- if (this._monthScroller && this._yearScroller) {
527
- this._monthScroller.active = true;
528
- this._yearScroller.active = true;
529
- }
530
-
531
- this.scrollToDate(initialPosition);
532
- }
533
-
534
- _repositionYearScroller() {
535
- const monthPosition = this._monthScroller.position;
536
- this._visibleMonthIndex = Math.floor(monthPosition);
537
- this._yearScroller.position = (monthPosition + this._originDate.getMonth()) / 12;
538
- }
539
-
540
- _repositionMonthScroller() {
541
- this._monthScroller.position = this._yearScroller.position * 12 - this._originDate.getMonth();
542
- this._visibleMonthIndex = Math.floor(this._monthScroller.position);
543
- }
544
-
545
- _onMonthScroll() {
546
- this._repositionYearScroller();
547
- this._doIgnoreTaps();
548
- }
549
-
550
- _onYearScroll() {
551
- this._repositionMonthScroller();
552
- this._doIgnoreTaps();
553
- }
554
-
555
- _onYearScrollTouchStart() {
556
- this._notTapping = false;
557
- setTimeout(() => {
558
- this._notTapping = true;
559
- }, 300);
560
-
561
- this._repositionMonthScroller();
562
- }
563
-
564
- _onMonthScrollTouchStart() {
565
- this._repositionYearScroller();
566
- }
567
-
568
- _doIgnoreTaps() {
569
- this._ignoreTaps = true;
570
- this._debouncer = Debouncer.debounce(this._debouncer, timeOut.after(300), () => {
571
- this._ignoreTaps = false;
572
- });
573
- }
574
-
575
- _formatDisplayed(date, formatDate, label) {
576
- if (date) {
577
- return formatDate(extractDateParts(date));
578
- }
579
- return label;
580
- }
581
-
582
- _onTodayTap() {
583
- const today = new Date();
584
-
585
- if (Math.abs(this._monthScroller.position - this._differenceInMonths(today, this._originDate)) < 0.001) {
586
- // Select today only if the month scroller is positioned approximately
587
- // at the beginning of the current month
588
- this._selectDate(today);
589
- this._close();
590
- } else {
591
- this._scrollToCurrentMonth();
592
- }
593
- }
594
-
595
- _scrollToCurrentMonth() {
596
- if (this.focusedDate) {
597
- this.focusedDate = new Date();
598
- }
599
-
600
- this.scrollToDate(new Date(), true);
601
- }
602
-
603
- _onYearTap(e) {
604
- if (!this._ignoreTaps && !this._notTapping) {
605
- const scrollDelta =
606
- e.detail.y - (this._yearScroller.getBoundingClientRect().top + this._yearScroller.clientHeight / 2);
607
- const yearDelta = scrollDelta / this._yearScroller.itemHeight;
608
- this._scrollToPosition(this._monthScroller.position + yearDelta * 12, true);
609
- }
610
- }
611
-
612
- _scrollToPosition(targetPosition, animate) {
613
- if (this._targetPosition !== undefined) {
614
- this._targetPosition = targetPosition;
615
- return;
616
- }
617
-
618
- if (!animate) {
619
- this._monthScroller.position = targetPosition;
620
- this._targetPosition = undefined;
621
- this._repositionYearScroller();
622
- this.__tryFocusDate();
623
- return;
624
- }
625
-
626
- this._targetPosition = targetPosition;
627
-
628
- let revealResolve;
629
- this._revealPromise = new Promise((resolve) => {
630
- revealResolve = resolve;
631
- });
632
-
633
- // http://gizma.com/easing/
634
- const easingFunction = (t, b, c, d) => {
635
- t /= d / 2;
636
- if (t < 1) {
637
- return (c / 2) * t * t + b;
638
- }
639
- t -= 1;
640
- return (-c / 2) * (t * (t - 2) - 1) + b;
641
- };
642
-
643
- let start = 0;
644
- const initialPosition = this._monthScroller.position;
645
-
646
- const smoothScroll = (timestamp) => {
647
- if (!start) {
648
- start = timestamp;
649
- }
650
-
651
- const currentTime = timestamp - start;
652
-
653
- if (currentTime < this.scrollDuration) {
654
- const currentPos = easingFunction(
655
- currentTime,
656
- initialPosition,
657
- this._targetPosition - initialPosition,
658
- this.scrollDuration,
659
- );
660
- this._monthScroller.position = currentPos;
661
- window.requestAnimationFrame(smoothScroll);
662
- } else {
663
- this.dispatchEvent(
664
- new CustomEvent('scroll-animation-finished', {
665
- bubbles: true,
666
- composed: true,
667
- detail: {
668
- position: this._targetPosition,
669
- oldPosition: initialPosition,
670
- },
671
- }),
672
- );
673
-
674
- this._monthScroller.position = this._targetPosition;
675
- this._targetPosition = undefined;
676
-
677
- revealResolve();
678
- this._revealPromise = undefined;
679
- }
680
-
681
- setTimeout(this._repositionYearScroller.bind(this), 1);
682
- };
683
-
684
- // Start the animation.
685
- window.requestAnimationFrame(smoothScroll);
686
- }
687
-
688
- _limit(value, range) {
689
- return Math.min(range.max, Math.max(range.min, value));
690
- }
691
-
692
- _handleTrack(e) {
693
- // Check if horizontal movement threshold (dx) not exceeded or
694
- // scrolling fast vertically (ddy).
695
- if (Math.abs(e.detail.dx) < 10 || Math.abs(e.detail.ddy) > 10) {
696
- return;
697
- }
698
-
699
- // If we're flinging quickly -> start animating already.
700
- if (Math.abs(e.detail.ddx) > this._yearScrollerWidth / 3) {
701
- this._toggleAnimateClass(true);
702
- }
703
-
704
- const newTranslateX = this._translateX + e.detail.ddx;
705
- this._translateX = this._limit(newTranslateX, {
706
- min: 0,
707
- max: this._yearScrollerWidth,
708
- });
709
- }
710
-
711
- _track(e) {
712
- if (this._desktopMode) {
713
- // No need to track for swipe gestures on desktop.
714
- return;
715
- }
716
-
717
- switch (e.detail.state) {
718
- case 'start':
719
- this._toggleAnimateClass(false);
720
- break;
721
- case 'track':
722
- this._handleTrack(e);
723
- break;
724
- case 'end':
725
- this._toggleAnimateClass(true);
726
- if (this._translateX >= this._yearScrollerWidth / 2) {
727
- this._closeYearScroller();
728
- } else {
729
- this._openYearScroller();
730
- }
731
- break;
732
- default:
733
- break;
734
- }
735
- }
736
-
737
- _toggleAnimateClass(enable) {
738
- if (enable) {
739
- this.classList.add('animate');
740
- } else {
741
- this.classList.remove('animate');
742
- }
743
- }
744
-
745
- _toggleYearScroller() {
746
- if (this._isYearScrollerVisible()) {
747
- this._closeYearScroller();
748
- } else {
749
- this._openYearScroller();
750
- }
751
- }
752
-
753
- _openYearScroller() {
754
- this._translateX = 0;
755
- this.setAttribute('years-visible', '');
756
- }
757
-
758
- _closeYearScroller() {
759
- this.removeAttribute('years-visible');
760
- this._translateX = this._yearScrollerWidth;
761
- }
762
-
763
- _isYearScrollerVisible() {
764
- return this._translateX < this._yearScrollerWidth / 2;
765
- }
766
-
767
- _translateXChanged(x) {
768
- if (!this._desktopMode) {
769
- this._monthScroller.style.transform = `translateX(${x - this._yearScrollerWidth}px)`;
770
- this._yearScroller.style.transform = `translateX(${x}px)`;
771
- }
772
- }
773
-
774
- _yearAfterXMonths(months) {
775
- return dateAfterXMonths(months).getFullYear();
776
- }
777
-
778
- _differenceInMonths(date1, date2) {
779
- const months = (date1.getFullYear() - date2.getFullYear()) * 12;
780
- return months - date2.getMonth() + date1.getMonth();
781
- }
782
-
783
- _clear() {
784
- this._selectDate('');
785
- }
786
-
787
- _close() {
788
- this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
789
- }
790
-
791
- _cancel() {
792
- this.focusedDate = this.selectedDate;
793
- this._close();
794
- }
795
-
796
- _preventDefault(e) {
797
- e.preventDefault();
798
- }
799
-
800
- __toggleDate(date) {
801
- if (dateEquals(date, this.selectedDate)) {
802
- this._clear();
803
- this.focusedDate = date;
804
- } else {
805
- this._selectDate(date);
806
- }
807
- }
808
-
809
- __onMonthCalendarKeyDown(event) {
810
- let handled = false;
811
-
812
- switch (event.key) {
813
- case 'ArrowDown':
814
- this._moveFocusByDays(7);
815
- handled = true;
816
- break;
817
- case 'ArrowUp':
818
- this._moveFocusByDays(-7);
819
- handled = true;
820
- break;
821
- case 'ArrowRight':
822
- this._moveFocusByDays(this.__isRTL ? -1 : 1);
823
- handled = true;
824
- break;
825
- case 'ArrowLeft':
826
- this._moveFocusByDays(this.__isRTL ? 1 : -1);
827
- handled = true;
828
- break;
829
- case 'Enter':
830
- this._selectDate(this.focusedDate);
831
- this._close();
832
- handled = true;
833
- break;
834
- case ' ':
835
- this.__toggleDate(this.focusedDate);
836
- handled = true;
837
- break;
838
- case 'Home':
839
- this._moveFocusInsideMonth(this.focusedDate, 'minDate');
840
- handled = true;
841
- break;
842
- case 'End':
843
- this._moveFocusInsideMonth(this.focusedDate, 'maxDate');
844
- handled = true;
845
- break;
846
- case 'PageDown':
847
- this._moveFocusByMonths(event.shiftKey ? 12 : 1);
848
- handled = true;
849
- break;
850
- case 'PageUp':
851
- this._moveFocusByMonths(event.shiftKey ? -12 : -1);
852
- handled = true;
853
- break;
854
- case 'Tab':
855
- this._onTabKeyDown(event, 'calendar');
856
- break;
857
- default:
858
- break;
859
- }
860
-
861
- if (handled) {
862
- event.preventDefault();
863
- event.stopPropagation();
864
- }
865
- }
866
-
867
- _onTabKeyDown(event, section) {
868
- // Stop propagation to avoid focus-trap
869
- // listener when used in a modal dialog.
870
- event.stopPropagation();
871
-
872
- switch (section) {
873
- case 'calendar':
874
- if (event.shiftKey) {
875
- event.preventDefault();
876
-
877
- if (this.hasAttribute('fullscreen')) {
878
- // Trap focus in the overlay
879
- this.focusCancel();
880
- } else {
881
- this.__focusInput();
882
- }
883
- }
884
- break;
885
- case 'today':
886
- if (event.shiftKey) {
887
- event.preventDefault();
888
- this.focusDateElement();
889
- }
890
- break;
891
- case 'cancel':
892
- if (!event.shiftKey) {
893
- event.preventDefault();
894
-
895
- if (this.hasAttribute('fullscreen')) {
896
- // Trap focus in the overlay
897
- this.focusDateElement();
898
- } else {
899
- this.__focusInput();
900
- }
901
- }
902
- break;
903
- default:
904
- break;
905
- }
906
- }
907
-
908
- __onTodayButtonKeyDown(event) {
909
- if (event.key === 'Tab') {
910
- this._onTabKeyDown(event, 'today');
911
- }
912
- }
913
-
914
- __onCancelButtonKeyDown(event) {
915
- if (event.key === 'Tab') {
916
- this._onTabKeyDown(event, 'cancel');
917
- }
918
- }
919
-
920
- __focusInput() {
921
- this.dispatchEvent(new CustomEvent('focus-input', { bubbles: true, composed: true }));
922
- }
923
-
924
- __tryFocusDate() {
925
- const dateToFocus = this.__pendingDateFocus;
926
- if (dateToFocus) {
927
- // Check the date element with tabindex="0"
928
- const dateElement = this.focusableDateElement;
929
-
930
- if (dateElement && dateEquals(dateElement.date, this.__pendingDateFocus)) {
931
- delete this.__pendingDateFocus;
932
- dateElement.focus();
933
- }
934
- }
935
- }
936
-
937
- async focusDate(date, keepMonth) {
938
- const dateToFocus = date || this.selectedDate || this.initialPosition || new Date();
939
- this.focusedDate = dateToFocus;
940
- if (!keepMonth) {
941
- this._focusedMonthDate = dateToFocus.getDate();
942
- }
943
- await this.focusDateElement(false);
944
- }
945
-
946
- async focusDateElement(reveal = true) {
947
- this.__pendingDateFocus = this.focusedDate;
948
-
949
- // Wait for `vaadin-month-calendar` elements to be rendered
950
- if (!this.calendars.length) {
951
- await new Promise((resolve) => {
952
- afterNextRender(this, () => {
953
- // Force dom-repeat elements to render
954
- flush();
955
- resolve();
956
- });
957
- });
958
- }
959
-
960
- // Reveal focused date unless it has been just set,
961
- // which triggers `revealDate()` in the observer.
962
- if (reveal) {
963
- this.revealDate(this.focusedDate);
964
- }
965
-
966
- if (this._revealPromise) {
967
- // Wait for focused date to be scrolled into view.
968
- await this._revealPromise;
969
- }
970
-
971
- this.__tryFocusDate();
972
- }
973
-
974
- _focusClosestDate(focus) {
975
- this.focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
976
- }
977
-
978
- _focusAllowedDate(dateToFocus, diff, keepMonth) {
979
- if (this._dateAllowed(dateToFocus)) {
980
- this.focusDate(dateToFocus, keepMonth);
981
- } else if (this._dateAllowed(this.focusedDate)) {
982
- // Move to min or max date
983
- if (diff > 0) {
984
- // Down, Right or Page Down key
985
- this.focusDate(this.maxDate);
986
- } else {
987
- // Up, Left or Page Up key
988
- this.focusDate(this.minDate);
989
- }
990
- } else {
991
- // Move to closest allowed date
992
- this._focusClosestDate(this.focusedDate);
993
- }
994
- }
995
-
996
- _getDateDiff(months, days) {
997
- const date = new Date(0, 0);
998
- date.setFullYear(this.focusedDate.getFullYear());
999
- date.setMonth(this.focusedDate.getMonth() + months);
1000
- if (days) {
1001
- date.setDate(this.focusedDate.getDate() + days);
1002
- }
1003
- return date;
1004
- }
1005
-
1006
- _moveFocusByDays(days) {
1007
- const dateToFocus = this._getDateDiff(0, days);
1008
-
1009
- this._focusAllowedDate(dateToFocus, days, false);
1010
- }
1011
-
1012
- _moveFocusByMonths(months) {
1013
- const dateToFocus = this._getDateDiff(months);
1014
- const targetMonth = dateToFocus.getMonth();
1015
-
1016
- if (!this._focusedMonthDate) {
1017
- this._focusedMonthDate = this.focusedDate.getDate();
1018
- }
1019
-
1020
- dateToFocus.setDate(this._focusedMonthDate);
1021
-
1022
- if (dateToFocus.getMonth() !== targetMonth) {
1023
- dateToFocus.setDate(0);
1024
- }
1025
-
1026
- this._focusAllowedDate(dateToFocus, months, true);
1027
- }
1028
-
1029
- _moveFocusInsideMonth(focusedDate, property) {
1030
- const dateToFocus = new Date(0, 0);
1031
- dateToFocus.setFullYear(focusedDate.getFullYear());
1032
-
1033
- if (property === 'minDate') {
1034
- dateToFocus.setMonth(focusedDate.getMonth());
1035
- dateToFocus.setDate(1);
1036
- } else {
1037
- dateToFocus.setMonth(focusedDate.getMonth() + 1);
1038
- dateToFocus.setDate(0);
1039
- }
1040
-
1041
- if (this._dateAllowed(dateToFocus)) {
1042
- this.focusDate(dateToFocus);
1043
- } else if (this._dateAllowed(focusedDate)) {
1044
- // Move to minDate or maxDate
1045
- this.focusDate(this[property]);
1046
- } else {
1047
- // Move to closest allowed date
1048
- this._focusClosestDate(focusedDate);
1049
- }
1050
- }
1051
-
1052
- _dateAllowed(date, min = this.minDate, max = this.maxDate) {
1053
- return (!min || date >= min) && (!max || date <= max);
1054
- }
1055
-
1056
- _isTodayAllowed(min, max) {
1057
- const today = new Date();
1058
- const todayMidnight = new Date(0, 0);
1059
- todayMidnight.setFullYear(today.getFullYear());
1060
- todayMidnight.setMonth(today.getMonth());
1061
- todayMidnight.setDate(today.getDate());
1062
- return this._dateAllowed(todayMidnight, min, max);
63
+ this._addListeners();
64
+ this._initControllers();
1063
65
  }
1064
66
  }
1065
67