@vaadin/date-picker 23.0.0-beta1 → 23.0.0-beta5

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": "23.0.0-beta1",
3
+ "version": "23.0.0-beta5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,23 +33,22 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@open-wc/dedupe-mixin": "^1.3.0",
36
- "@polymer/iron-media-query": "^3.0.0",
37
36
  "@polymer/polymer": "^3.2.0",
38
- "@vaadin/button": "23.0.0-beta1",
39
- "@vaadin/component-base": "23.0.0-beta1",
40
- "@vaadin/field-base": "23.0.0-beta1",
41
- "@vaadin/input-container": "23.0.0-beta1",
42
- "@vaadin/vaadin-lumo-styles": "23.0.0-beta1",
43
- "@vaadin/vaadin-material-styles": "23.0.0-beta1",
44
- "@vaadin/vaadin-overlay": "23.0.0-beta1",
45
- "@vaadin/vaadin-themable-mixin": "23.0.0-beta1"
37
+ "@vaadin/button": "23.0.0-beta5",
38
+ "@vaadin/component-base": "23.0.0-beta5",
39
+ "@vaadin/field-base": "23.0.0-beta5",
40
+ "@vaadin/input-container": "23.0.0-beta5",
41
+ "@vaadin/vaadin-lumo-styles": "23.0.0-beta5",
42
+ "@vaadin/vaadin-material-styles": "23.0.0-beta5",
43
+ "@vaadin/vaadin-overlay": "23.0.0-beta5",
44
+ "@vaadin/vaadin-themable-mixin": "23.0.0-beta5"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@esm-bundle/chai": "^4.3.4",
49
- "@vaadin/dialog": "23.0.0-beta1",
50
- "@vaadin/polymer-legacy-adapter": "23.0.0-beta1",
48
+ "@vaadin/dialog": "23.0.0-beta5",
49
+ "@vaadin/polymer-legacy-adapter": "23.0.0-beta5",
51
50
  "@vaadin/testing-helpers": "^0.3.2",
52
51
  "sinon": "^9.2.0"
53
52
  },
54
- "gitHead": "467244b76021176c109df675799b07029b293e58"
53
+ "gitHead": "4c388aeec6623869c70896c909799804fc95d0f9"
55
54
  }
@@ -3,7 +3,6 @@
3
3
  * Copyright (c) 2016 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import '@polymer/iron-media-query/iron-media-query.js';
7
6
  import './vaadin-date-picker-overlay.js';
8
7
  import './vaadin-date-picker-overlay-content.js';
9
8
  import { dashToCamelCase } from '@polymer/polymer/lib/utils/case-map.js';
@@ -98,8 +97,6 @@ class DatePickerLight extends ThemableMixin(DatePickerMixin(PolymerElement)) {
98
97
  </vaadin-date-picker-overlay-content>
99
98
  </template>
100
99
  </vaadin-date-picker-overlay>
101
-
102
- <iron-media-query query="[[_fullscreenMediaQuery]]" query-matches="{{_fullscreen}}"> </iron-media-query>
103
100
  `;
104
101
  }
105
102
 
@@ -6,6 +6,7 @@
6
6
  import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
7
7
  import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
8
8
  import { KeyboardMixin } from '@vaadin/component-base/src/keyboard-mixin.js';
9
+ import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
9
10
  import { DelegateFocusMixin } from '@vaadin/field-base/src/delegate-focus-mixin.js';
10
11
  import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
11
12
  import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
@@ -319,11 +320,6 @@ export const DatePickerMixin = (subclass) =>
319
320
  value: document.createElement('div').style.webkitOverflowScrolling === ''
320
321
  },
321
322
 
322
- /** @private */
323
- _ignoreAnnounce: {
324
- value: true
325
- },
326
-
327
323
  /** @private */
328
324
  _focusOverlayOnOpen: Boolean,
329
325
 
@@ -335,8 +331,7 @@ export const DatePickerMixin = (subclass) =>
335
331
  static get observers() {
336
332
  return [
337
333
  '_selectedDateChanged(_selectedDate, i18n.formatDate)',
338
- '_focusedDateChanged(_focusedDate, i18n.formatDate)',
339
- '_announceFocusedDate(_focusedDate, opened, _ignoreAnnounce)'
334
+ '_focusedDateChanged(_focusedDate, i18n.formatDate)'
340
335
  ];
341
336
  }
342
337
 
@@ -423,6 +418,12 @@ export const DatePickerMixin = (subclass) =>
423
418
  }
424
419
  });
425
420
 
421
+ this.addController(
422
+ new MediaQueryController(this._fullscreenMediaQuery, (matches) => {
423
+ this._fullscreen = matches;
424
+ })
425
+ );
426
+
426
427
  this.addController(new VirtualKeyboardController(this));
427
428
  }
428
429
 
@@ -584,6 +585,7 @@ export const DatePickerMixin = (subclass) =>
584
585
  if (input) {
585
586
  input.autocomplete = 'off';
586
587
  input.setAttribute('role', 'combobox');
588
+ input.setAttribute('aria-haspopup', 'dialog');
587
589
  input.setAttribute('aria-expanded', !!this.opened);
588
590
  this._applyInputValue(this._selectedDate);
589
591
  }
@@ -710,7 +712,7 @@ export const DatePickerMixin = (subclass) =>
710
712
  }
711
713
 
712
714
  if (this._focusOverlayOnOpen) {
713
- this._overlayContent.focus();
715
+ this._overlayContent.focusDateElement();
714
716
  this._focusOverlayOnOpen = false;
715
717
  } else {
716
718
  this._focus();
@@ -719,8 +721,6 @@ export const DatePickerMixin = (subclass) =>
719
721
  if (this._noInput && this.focusElement) {
720
722
  this.focusElement.blur();
721
723
  }
722
-
723
- this._ignoreAnnounce = false;
724
724
  }
725
725
 
726
726
  // A hack needed for iOS to prevent dropdown from being clipped in an
@@ -765,8 +765,6 @@ export const DatePickerMixin = (subclass) =>
765
765
 
766
766
  /** @protected */
767
767
  _onOverlayClosed() {
768
- this._ignoreAnnounce = true;
769
-
770
768
  window.removeEventListener('scroll', this._boundOnScroll, true);
771
769
 
772
770
  if (this._touchPrevented) {
@@ -891,15 +889,14 @@ export const DatePickerMixin = (subclass) =>
891
889
  case 'ArrowUp':
892
890
  // prevent scrolling the page with arrows
893
891
  e.preventDefault();
894
-
895
892
  if (this.opened) {
896
- this._overlayContent.focus();
897
- this._overlayContent._onKeydown(e);
893
+ // The overlay can be opened with ctrl + option + shift in VoiceOver
894
+ // and without this logic, it won't be possible to focus the dialog opened this way.
895
+ this._overlayContent.focusDateElement();
898
896
  } else {
899
897
  this._focusOverlayOnOpen = true;
900
898
  this.open();
901
899
  }
902
-
903
900
  break;
904
901
  case 'Enter': {
905
902
  const parsedDate = this._getParsedDate();
@@ -945,8 +942,7 @@ export const DatePickerMixin = (subclass) =>
945
942
  if (e.shiftKey) {
946
943
  this._overlayContent.focusCancel();
947
944
  } else {
948
- this._overlayContent.focus();
949
- this._overlayContent.revealDate(this._focusedDate);
945
+ this._overlayContent.focusDate(this._focusedDate);
950
946
  }
951
947
  }
952
948
  break;
@@ -994,13 +990,6 @@ export const DatePickerMixin = (subclass) =>
994
990
  }
995
991
  }
996
992
 
997
- /** @private */
998
- _announceFocusedDate(_focusedDate, opened, _ignoreAnnounce) {
999
- if (opened && !_ignoreAnnounce) {
1000
- this._overlayContent.announceFocusedDate();
1001
- }
1002
- }
1003
-
1004
993
  /** @private */
1005
994
  get _overlayContent() {
1006
995
  return this.$.overlay.content.querySelector('#overlay-content');
@@ -3,24 +3,24 @@
3
3
  * Copyright (c) 2016 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import '@polymer/iron-media-query/iron-media-query.js';
7
6
  import '@vaadin/button/src/vaadin-button.js';
8
7
  import './vaadin-month-calendar.js';
9
8
  import './vaadin-infinite-scroller.js';
10
9
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
11
- import { announce } from '@vaadin/component-base/src/a11y-announcer.js';
12
10
  import { timeOut } from '@vaadin/component-base/src/async.js';
11
+ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
13
12
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
14
13
  import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
15
14
  import { addListener, setTouchAction } from '@vaadin/component-base/src/gestures.js';
15
+ import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
16
16
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
17
- import { dateEquals, extractDateParts, getClosestDate, getISOWeekNumber } from './vaadin-date-picker-helper.js';
17
+ import { dateEquals, extractDateParts, getClosestDate } from './vaadin-date-picker-helper.js';
18
18
 
19
19
  /**
20
20
  * @extends HTMLElement
21
21
  * @private
22
22
  */
23
- class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
23
+ class DatePickerOverlayContent extends ControllerMixin(ThemableMixin(DirMixin(PolymerElement))) {
24
24
  static get template() {
25
25
  return html`
26
26
  <style>
@@ -150,17 +150,8 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
150
150
  z-index: 1;
151
151
  padding: 8px;
152
152
  }
153
-
154
- #announcer {
155
- display: inline-block;
156
- position: fixed;
157
- clip: rect(0, 0, 0, 0);
158
- clip-path: inset(100%);
159
- }
160
153
  </style>
161
154
 
162
- <div id="announcer" role="alert" aria-live="polite">[[i18n.calendar]]</div>
163
-
164
155
  <div part="overlay-header" on-touchend="_preventDefault" desktop$="[[_desktopMode]]" aria-hidden="true">
165
156
  <div part="label">[[_formatDisplayed(selectedDate, i18n.formatDate, label)]]</div>
166
157
  <div part="clear-button" showclear$="[[_showClear(selectedDate)]]"></div>
@@ -190,9 +181,9 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
190
181
  show-week-numbers="[[showWeekNumbers]]"
191
182
  min-date="[[minDate]]"
192
183
  max-date="[[maxDate]]"
193
- focused$="[[_focused]]"
194
184
  part="month"
195
185
  theme$="[[theme]]"
186
+ on-keydown="__onMonthCalendarKeyDown"
196
187
  >
197
188
  </vaadin-month-calendar>
198
189
  </template>
@@ -209,7 +200,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
209
200
  <template>
210
201
  <div
211
202
  part="year-number"
212
- role="button"
213
203
  current$="[[_isCurrentYear(index)]]"
214
204
  selected$="[[_isSelectedYear(index, selectedDate)]]"
215
205
  >
@@ -226,12 +216,14 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
226
216
  part="today-button"
227
217
  theme="tertiary"
228
218
  disabled="[[!_isTodayAllowed(minDate, maxDate)]]"
219
+ on-keydown="__onTodayButtonKeyDown"
229
220
  >
230
221
  [[i18n.today]]
231
222
  </vaadin-button>
232
- <vaadin-button id="cancelButton" part="cancel-button" theme="tertiary"> [[i18n.cancel]] </vaadin-button>
223
+ <vaadin-button id="cancelButton" part="cancel-button" theme="tertiary" on-keydown="__onCancelButtonKeyDown">
224
+ [[i18n.cancel]]
225
+ </vaadin-button>
233
226
  </div>
234
- <iron-media-query query="(min-width: 375px)" query-matches="{{_desktopMode}}"></iron-media-query>
235
227
  `;
236
228
  }
237
229
 
@@ -276,6 +268,11 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
276
268
 
277
269
  _desktopMode: Boolean,
278
270
 
271
+ _desktopMediaQuery: {
272
+ type: String,
273
+ value: '(min-width: 375px)'
274
+ },
275
+
279
276
  _translateX: {
280
277
  observer: '_translateXChanged'
281
278
  },
@@ -306,8 +303,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
306
303
  */
307
304
  maxDate: Date,
308
305
 
309
- _focused: Boolean,
310
-
311
306
  /**
312
307
  * Input label
313
308
  */
@@ -319,13 +314,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
319
314
  return this.getAttribute('dir') === 'rtl';
320
315
  }
321
316
 
317
+ get focusableDateElement() {
318
+ return [...this.shadowRoot.querySelectorAll('vaadin-month-calendar')]
319
+ .map((calendar) => calendar.focusableDateElement)
320
+ .find(Boolean);
321
+ }
322
+
322
323
  ready() {
323
324
  super.ready();
324
- this.setAttribute('tabindex', 0);
325
- this.addEventListener('keydown', this._onKeydown.bind(this));
326
325
  addListener(this, 'tap', this._stopPropagation);
327
- this.addEventListener('focus', this._onOverlayFocus.bind(this));
328
- this.addEventListener('blur', this._onOverlayBlur.bind(this));
329
326
  addListener(this.$.scrollers, 'track', this._track.bind(this));
330
327
  addListener(this.shadowRoot.querySelector('[part="clear-button"]'), 'tap', this._clear.bind(this));
331
328
  addListener(this.shadowRoot.querySelector('[part="today-button"]'), 'tap', this._onTodayTap.bind(this));
@@ -337,6 +334,12 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
337
334
  'tap',
338
335
  this._toggleYearScroller.bind(this)
339
336
  );
337
+
338
+ this.addController(
339
+ new MediaQueryController(this._desktopMediaQuery, (matches) => {
340
+ this._desktopMode = matches;
341
+ })
342
+ );
340
343
  }
341
344
 
342
345
  /**
@@ -353,25 +356,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
353
356
  setTouchAction(this.$.scrollers, 'pan-y');
354
357
  }
355
358
 
356
- announceFocusedDate() {
357
- const focusedDate = this._currentlyFocusedDate();
358
- let messages = [];
359
- if (dateEquals(focusedDate, new Date())) {
360
- messages.push(this.i18n.today);
361
- }
362
- messages = messages.concat([
363
- this.i18n.weekdays[focusedDate.getDay()],
364
- focusedDate.getDate(),
365
- this.i18n.monthNames[focusedDate.getMonth()],
366
- focusedDate.getFullYear()
367
- ]);
368
- if (this.showWeekNumbers && this.i18n.firstDayOfWeek === 1) {
369
- messages.push(this.i18n.week);
370
- messages.push(getISOWeekNumber(focusedDate));
371
- }
372
- announce(messages.join(' '));
373
- }
374
-
375
359
  /**
376
360
  * Focuses the cancel button
377
361
  */
@@ -423,14 +407,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
423
407
  }
424
408
  }
425
409
 
426
- _onOverlayFocus() {
427
- this._focused = true;
428
- }
429
-
430
- _onOverlayBlur() {
431
- this._focused = false;
432
- }
433
-
434
410
  _initialPositionChanged(initialPosition) {
435
411
  this.scrollToDate(initialPosition);
436
412
  }
@@ -522,6 +498,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
522
498
  this.$.monthScroller.position = targetPosition;
523
499
  this._targetPosition = undefined;
524
500
  this._repositionYearScroller();
501
+ this.__tryFocusDate();
525
502
  return;
526
503
  }
527
504
 
@@ -563,6 +540,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
563
540
 
564
541
  this.$.monthScroller.position = this._targetPosition;
565
542
  this._targetPosition = undefined;
543
+ this.__tryFocusDate();
566
544
  }
567
545
 
568
546
  setTimeout(this._repositionYearScroller.bind(this), 1);
@@ -703,150 +681,168 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
703
681
  e.preventDefault();
704
682
  }
705
683
 
706
- _onKeydown(e) {
707
- var focus = this._currentlyFocusedDate();
708
-
709
- // Cannot use (today/cancel).focused flag because vaadin-text-field removes it
710
- // previously in the keydown event.
711
- const isToday = e.composedPath().indexOf(this.$.todayButton) >= 0;
712
- const isCancel = e.composedPath().indexOf(this.$.cancelButton) >= 0;
713
- const isScroller = !isToday && !isCancel;
714
-
715
- // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
716
- const navigationKeys = [
717
- ' ',
718
- 'ArrowDown',
719
- 'ArrowUp',
720
- 'ArrowRight',
721
- 'ArrowLeft',
722
- 'Enter',
723
- 'End',
724
- 'Escape',
725
- 'Home',
726
- 'PageUp',
727
- 'PageDown',
728
- 'Tab'
729
- ];
730
-
731
- const eventKey = e.key;
732
- if (eventKey === 'Tab') {
733
- // We handle tabs here and don't want to bubble up.
734
- e.stopPropagation();
735
-
736
- const isFullscreen = this.hasAttribute('fullscreen');
737
- const isShift = e.shiftKey;
738
-
739
- if (isFullscreen) {
740
- e.preventDefault();
741
- } else if ((isShift && isScroller) || (!isShift && isCancel)) {
742
- // Return focus back to the input field
743
- e.preventDefault();
744
- this.dispatchEvent(new CustomEvent('focus-input', { bubbles: true, composed: true }));
745
- } else if (isShift && isToday) {
746
- // Browser returns focus back to the scrollable area. We need to set
747
- // the focused flag, and move the scroll to focused date.
748
- this._focused = true;
749
- setTimeout(() => this.revealDate(this.focusedDate), 1);
750
- } else {
751
- // Browser moves the focus out of the scroller, hence focused flag must
752
- // set to false.
753
- this._focused = false;
754
- }
755
- } else if (navigationKeys.includes(eventKey)) {
756
- e.preventDefault();
757
- e.stopPropagation();
758
- switch (eventKey) {
759
- case 'ArrowDown':
760
- this._moveFocusByDays(7);
761
- this.focus();
762
- break;
763
- case 'ArrowUp':
764
- this._moveFocusByDays(-7);
765
- this.focus();
766
- break;
767
- case 'ArrowRight':
768
- if (isScroller) {
769
- this._moveFocusByDays(this.__isRTL ? -1 : 1);
770
- }
771
- break;
772
- case 'ArrowLeft':
773
- if (isScroller) {
774
- this._moveFocusByDays(this.__isRTL ? 1 : -1);
775
- }
776
- break;
777
- case 'Enter':
778
- if (isScroller || isCancel) {
779
- this._close();
780
- } else if (isToday) {
781
- this._onTodayTap();
782
- }
783
- break;
784
- case ' ':
785
- if (isCancel) {
786
- this._close();
787
- } else if (isToday) {
788
- this._onTodayTap();
789
- } else {
790
- var focusedDate = this.focusedDate;
791
- if (dateEquals(focusedDate, this.selectedDate)) {
792
- this.selectedDate = '';
793
- this.focusedDate = focusedDate;
794
- } else {
795
- this.selectedDate = focusedDate;
796
- }
797
- }
798
- break;
799
- case 'Home':
800
- this._moveFocusInsideMonth(focus, 'minDate');
801
- break;
802
- case 'End':
803
- this._moveFocusInsideMonth(focus, 'maxDate');
804
- break;
805
- case 'PageDown':
806
- this._moveFocusByMonths(e.shiftKey ? 12 : 1);
807
- break;
808
- case 'PageUp':
809
- this._moveFocusByMonths(e.shiftKey ? -12 : -1);
810
- break;
811
- case 'Escape':
812
- this._cancel();
813
- break;
814
- default:
815
- break;
816
- }
684
+ __toggleDate(date) {
685
+ if (dateEquals(date, this.selectedDate)) {
686
+ this.selectedDate = '';
687
+ this.focusedDate = date;
688
+ } else {
689
+ this.selectedDate = date;
817
690
  }
818
691
  }
819
692
 
820
- _currentlyFocusedDate() {
821
- return this.focusedDate || this.selectedDate || this.initialPosition || new Date();
693
+ __onMonthCalendarKeyDown(event) {
694
+ let handled = false;
695
+
696
+ switch (event.key) {
697
+ case 'ArrowDown':
698
+ this._moveFocusByDays(7);
699
+ handled = true;
700
+ break;
701
+ case 'ArrowUp':
702
+ this._moveFocusByDays(-7);
703
+ handled = true;
704
+ break;
705
+ case 'ArrowRight':
706
+ this._moveFocusByDays(this.__isRTL ? -1 : 1);
707
+ handled = true;
708
+ break;
709
+ case 'ArrowLeft':
710
+ this._moveFocusByDays(this.__isRTL ? 1 : -1);
711
+ handled = true;
712
+ break;
713
+ case 'Enter':
714
+ this.selectedDate = this.focusedDate;
715
+ this._close();
716
+ handled = true;
717
+ break;
718
+ case ' ':
719
+ this.__toggleDate(this.focusedDate);
720
+ handled = true;
721
+ break;
722
+ case 'Home':
723
+ this._moveFocusInsideMonth(this.focusedDate, 'minDate');
724
+ handled = true;
725
+ break;
726
+ case 'End':
727
+ this._moveFocusInsideMonth(this.focusedDate, 'maxDate');
728
+ handled = true;
729
+ break;
730
+ case 'PageDown':
731
+ this._moveFocusByMonths(event.shiftKey ? 12 : 1);
732
+ handled = true;
733
+ break;
734
+ case 'PageUp':
735
+ this._moveFocusByMonths(event.shiftKey ? -12 : -1);
736
+ handled = true;
737
+ break;
738
+ case 'Escape':
739
+ this._cancel();
740
+ handled = true;
741
+ break;
742
+ default:
743
+ break;
744
+ }
745
+
746
+ if (handled) {
747
+ event.preventDefault();
748
+ event.stopPropagation();
749
+ }
750
+ }
751
+
752
+ __onTodayButtonKeyDown(event) {
753
+ if (this.hasAttribute('fullscreen')) {
754
+ event.stopPropagation();
755
+ return;
756
+ }
757
+
758
+ if (event.key === 'Tab' && event.shiftKey) {
759
+ event.stopPropagation();
760
+
761
+ // Browser returns focus back to the calendar.
762
+ // We need to move the scroll to focused date.
763
+ setTimeout(() => this.revealDate(this.focusedDate), 1);
764
+ }
765
+
766
+ if (event.key === 'Escape') {
767
+ this._cancel();
768
+ event.preventDefault();
769
+ event.stopPropagation();
770
+ }
771
+ }
772
+
773
+ __onCancelButtonKeyDown(event) {
774
+ if (this.hasAttribute('fullscreen')) {
775
+ event.stopPropagation();
776
+ return;
777
+ }
778
+
779
+ if (event.key === 'Tab' && !event.shiftKey) {
780
+ // Return focus back to the input field
781
+ event.preventDefault();
782
+ event.stopPropagation();
783
+ this.dispatchEvent(new CustomEvent('focus-input', { bubbles: true, composed: true }));
784
+ }
785
+
786
+ if (event.key === 'Escape') {
787
+ this._cancel();
788
+ event.preventDefault();
789
+ event.stopPropagation();
790
+ }
822
791
  }
823
792
 
824
- _focusDate(dateToFocus) {
793
+ __tryFocusDate() {
794
+ const dateToFocus = this.__pendingDateFocus;
795
+ if (dateToFocus) {
796
+ // Check the date element with tabindex="0"
797
+ const dateElement = this.focusableDateElement;
798
+
799
+ if (dateElement && dateEquals(dateElement.date, this.__pendingDateFocus)) {
800
+ delete this.__pendingDateFocus;
801
+ dateElement.focus();
802
+ }
803
+ }
804
+ }
805
+
806
+ async focusDate(date, keepMonth) {
807
+ const dateToFocus = date || this.selectedDate || this.initialPosition || new Date();
825
808
  this.focusedDate = dateToFocus;
826
- this._focusedMonthDate = dateToFocus.getDate();
809
+ if (!keepMonth) {
810
+ this._focusedMonthDate = dateToFocus.getDate();
811
+ }
812
+ await this.focusDateElement();
813
+ }
814
+
815
+ async focusDateElement() {
816
+ this.__pendingDateFocus = this.focusedDate;
817
+
818
+ await new Promise((resolve) => {
819
+ requestAnimationFrame(resolve);
820
+ });
821
+
822
+ this.__tryFocusDate();
827
823
  }
828
824
 
829
825
  _focusClosestDate(focus) {
830
- this._focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
826
+ this.focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
831
827
  }
832
828
 
833
829
  _moveFocusByDays(days) {
834
- var focus = this._currentlyFocusedDate();
830
+ var focus = this.focusedDate;
835
831
  var dateToFocus = new Date(0, 0);
836
832
  dateToFocus.setFullYear(focus.getFullYear());
837
833
  dateToFocus.setMonth(focus.getMonth());
838
834
  dateToFocus.setDate(focus.getDate() + days);
839
835
 
840
836
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
841
- this._focusDate(dateToFocus);
837
+ this.focusDate(dateToFocus);
842
838
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
843
839
  // Move to min or max date
844
840
  if (days > 0) {
845
841
  // down or right
846
- this._focusDate(this.maxDate);
842
+ this.focusDate(this.maxDate);
847
843
  } else {
848
844
  // up or left
849
- this._focusDate(this.minDate);
845
+ this.focusDate(this.minDate);
850
846
  }
851
847
  } else {
852
848
  // Move to closest allowed date
@@ -855,7 +851,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
855
851
  }
856
852
 
857
853
  _moveFocusByMonths(months) {
858
- var focus = this._currentlyFocusedDate();
854
+ var focus = this.focusedDate;
859
855
  var dateToFocus = new Date(0, 0);
860
856
  dateToFocus.setFullYear(focus.getFullYear());
861
857
  dateToFocus.setMonth(focus.getMonth() + months);
@@ -868,15 +864,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
868
864
  }
869
865
 
870
866
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
871
- this.focusedDate = dateToFocus;
867
+ this.focusDate(dateToFocus, true);
872
868
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
873
869
  // Move to min or max date
874
870
  if (months > 0) {
875
871
  // pagedown
876
- this._focusDate(this.maxDate);
872
+ this.focusDate(this.maxDate);
877
873
  } else {
878
874
  // pageup
879
- this._focusDate(this.minDate);
875
+ this.focusDate(this.minDate);
880
876
  }
881
877
  } else {
882
878
  // Move to closest allowed date
@@ -897,10 +893,10 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
897
893
  }
898
894
 
899
895
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
900
- this._focusDate(dateToFocus);
896
+ this.focusDate(dateToFocus);
901
897
  } else if (this._dateAllowed(focusedDate, this.minDate, this.maxDate)) {
902
898
  // Move to minDate or maxDate
903
- this._focusDate(this[property]);
899
+ this.focusDate(this[property]);
904
900
  } else {
905
901
  // Move to closest allowed date
906
902
  this._focusClosestDate(focusedDate);
@@ -13,6 +13,8 @@ registerStyles('vaadin-date-picker-overlay', datePickerOverlayStyles, {
13
13
  moduleId: 'vaadin-date-picker-overlay-styles'
14
14
  });
15
15
 
16
+ let memoizedTemplate;
17
+
16
18
  /**
17
19
  * An element used internally by `<vaadin-date-picker>`. Not intended to be used separately.
18
20
  *
@@ -23,6 +25,15 @@ class DatePickerOverlay extends DisableUpgradeMixin(PositionMixin(OverlayElement
23
25
  static get is() {
24
26
  return 'vaadin-date-picker-overlay';
25
27
  }
28
+
29
+ static get template() {
30
+ if (!memoizedTemplate) {
31
+ memoizedTemplate = super.template.cloneNode(true);
32
+ memoizedTemplate.content.querySelector('[part~="overlay"]').removeAttribute('tabindex');
33
+ }
34
+
35
+ return memoizedTemplate;
36
+ }
26
37
  }
27
38
 
28
39
  customElements.define(DatePickerOverlay.is, DatePickerOverlay);
@@ -3,13 +3,11 @@
3
3
  * Copyright (c) 2016 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import '@polymer/iron-media-query/iron-media-query.js';
7
6
  import '@vaadin/input-container/src/vaadin-input-container.js';
8
7
  import './vaadin-date-picker-overlay.js';
9
8
  import './vaadin-date-picker-overlay-content.js';
10
9
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
11
10
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
12
- import { addListener } from '@vaadin/component-base/src/gestures.js';
13
11
  import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
14
12
  import { InputController } from '@vaadin/field-base/src/input-controller.js';
15
13
  import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
@@ -144,8 +142,8 @@ class DatePicker extends DatePickerMixin(InputControlMixin(ThemableMixin(Element
144
142
  >
145
143
  <slot name="prefix" slot="prefix"></slot>
146
144
  <slot name="input"></slot>
147
- <div id="clearButton" part="clear-button" slot="suffix"></div>
148
- <div part="toggle-button" slot="suffix" role="button"></div>
145
+ <div id="clearButton" part="clear-button" slot="suffix" aria-hidden="true"></div>
146
+ <div part="toggle-button" slot="suffix" aria-hidden="true" on-click="_toggle"></div>
149
147
  </vaadin-input-container>
150
148
 
151
149
  <div part="helper-text">
@@ -186,8 +184,6 @@ class DatePicker extends DatePickerMixin(InputControlMixin(ThemableMixin(Element
186
184
  ></vaadin-date-picker-overlay-content>
187
185
  </template>
188
186
  </vaadin-date-picker-overlay>
189
-
190
- <iron-media-query query="[[_fullscreenMediaQuery]]" query-matches="{{_fullscreen}}"> </iron-media-query>
191
187
  `;
192
188
  }
193
189
 
@@ -213,7 +209,9 @@ class DatePicker extends DatePickerMixin(InputControlMixin(ThemableMixin(Element
213
209
  })
214
210
  );
215
211
  this.addController(new LabelledInputController(this.inputElement, this._labelController));
216
- addListener(this.shadowRoot.querySelector('[part="toggle-button"]'), 'tap', this._toggle.bind(this));
212
+
213
+ const toggleButton = this.shadowRoot.querySelector('[part="toggle-button"]');
214
+ toggleButton.addEventListener('mousedown', (e) => e.preventDefault());
217
215
  }
218
216
 
219
217
  /** @private */
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import '@polymer/polymer/lib/elements/dom-repeat.js';
7
7
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
8
+ import { FocusMixin } from '@vaadin/component-base/src/focus-mixin.js';
8
9
  import { addListener } from '@vaadin/component-base/src/gestures.js';
9
10
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
10
11
  import { dateAllowed, dateEquals, getISOWeekNumber } from './vaadin-date-picker-helper.js';
@@ -13,7 +14,7 @@ import { dateAllowed, dateEquals, getISOWeekNumber } from './vaadin-date-picker-
13
14
  * @extends HTMLElement
14
15
  * @private
15
16
  */
16
- class MonthCalendar extends ThemableMixin(PolymerElement) {
17
+ class MonthCalendar extends FocusMixin(ThemableMixin(PolymerElement)) {
17
18
  static get template() {
18
19
  return html`
19
20
  <style>
@@ -21,15 +22,23 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
21
22
  display: block;
22
23
  }
23
24
 
24
- [part='weekdays'],
25
- #days {
25
+ #monthGrid {
26
+ display: block;
27
+ }
28
+
29
+ #monthGrid thead,
30
+ #monthGrid tbody {
31
+ display: block;
32
+ width: 100%;
33
+ }
34
+
35
+ [part='weekdays'] {
26
36
  display: flex;
27
- flex-wrap: wrap;
28
37
  flex-grow: 1;
29
38
  }
30
39
 
31
- #days-container,
32
- #weekdays-container {
40
+ #days-container tr,
41
+ #weekdays-container tr {
33
42
  display: flex;
34
43
  }
35
44
 
@@ -40,6 +49,11 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
40
49
  flex-shrink: 0;
41
50
  }
42
51
 
52
+ [part='date'] {
53
+ outline: none;
54
+ }
55
+
56
+ [part='week-number'][hidden],
43
57
  [part='week-numbers'][hidden],
44
58
  [part='weekday'][hidden] {
45
59
  display: none;
@@ -47,8 +61,11 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
47
61
 
48
62
  [part='weekday'],
49
63
  [part='date'] {
64
+ display: block;
50
65
  /* Would use calc(100% / 7) but it doesn't work nice on IE */
51
66
  width: 14.285714286%;
67
+ padding: 0;
68
+ font-weight: normal;
52
69
  }
53
70
 
54
71
  [part='weekday']:empty,
@@ -58,42 +75,59 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
58
75
  }
59
76
  </style>
60
77
 
61
- <div part="month-header" role="heading">[[_getTitle(month, i18n.monthNames)]]</div>
62
- <div id="monthGrid" on-touchend="_preventDefault" on-touchstart="_onMonthGridTouchStart">
63
- <div id="weekdays-container">
64
- <div hidden$="[[!_showWeekSeparator(showWeekNumbers, i18n.firstDayOfWeek)]]" part="weekday"></div>
65
- <div part="weekdays">
78
+ <div part="month-header" id="month-header" aria-hidden="true">[[_getTitle(month, i18n.monthNames)]]</div>
79
+ <table
80
+ id="monthGrid"
81
+ role="grid"
82
+ aria-labelledby="month-header"
83
+ on-touchend="_preventDefault"
84
+ on-touchstart="_onMonthGridTouchStart"
85
+ >
86
+ <thead id="weekdays-container">
87
+ <tr role="row" part="weekdays">
88
+ <th
89
+ part="weekday"
90
+ aria-hidden="true"
91
+ hidden$="[[!_showWeekSeparator(showWeekNumbers, i18n.firstDayOfWeek)]]"
92
+ ></th>
66
93
  <template
67
94
  is="dom-repeat"
68
95
  items="[[_getWeekDayNames(i18n.weekdays, i18n.weekdaysShort, showWeekNumbers, i18n.firstDayOfWeek)]]"
69
96
  >
70
- <div part="weekday" role="heading" aria-label$="[[item.weekDay]]">[[item.weekDayShort]]</div>
71
- </template>
72
- </div>
73
- </div>
74
- <div id="days-container">
75
- <div part="week-numbers" hidden$="[[!_showWeekSeparator(showWeekNumbers, i18n.firstDayOfWeek)]]">
76
- <template is="dom-repeat" items="[[_getWeekNumbers(_days)]]">
77
- <div part="week-number" role="heading" aria-label$="[[i18n.week]] [[item]]">[[item]]</div>
78
- </template>
79
- </div>
80
- <div id="days">
81
- <template is="dom-repeat" items="[[_days]]">
82
- <!-- prettier-ignore -->
83
- <div
84
- part="date"
85
- today$="[[_isToday(item)]]"
86
- selected$="[[_dateEquals(item, selectedDate)]]"
87
- focused$="[[_dateEquals(item, focusedDate)]]"
88
- date="[[item]]"
89
- disabled$="[[!_dateAllowed(item, minDate, maxDate)]]"
90
- role$="[[_getRole(item)]]"
91
- aria-label$="[[_getAriaLabel(item)]]"
92
- aria-disabled$="[[_getAriaDisabled(item, minDate, maxDate)]]">[[_getDate(item)]]</div>
97
+ <th role="columnheader" part="weekday" scope="col" abbr$="[[item.weekDay]]">[[item.weekDayShort]]</th>
93
98
  </template>
94
- </div>
95
- </div>
96
- </div>
99
+ </tr>
100
+ </thead>
101
+ <tbody id="days-container">
102
+ <template is="dom-repeat" items="[[_weeks]]" as="week">
103
+ <tr role="row">
104
+ <td
105
+ part="week-number"
106
+ aria-hidden="true"
107
+ hidden$="[[!_showWeekSeparator(showWeekNumbers, i18n.firstDayOfWeek)]]"
108
+ >
109
+ [[__getWeekNumber(week)]]
110
+ </td>
111
+ <template is="dom-repeat" items="[[week]]">
112
+ <td
113
+ role="gridcell"
114
+ part="date"
115
+ date="[[item]]"
116
+ today$="[[_isToday(item)]]"
117
+ focused$="[[__isDayFocused(item, focusedDate)]]"
118
+ tabindex$="[[__getDayTabindex(item, focusedDate)]]"
119
+ selected$="[[__isDaySelected(item, selectedDate)]]"
120
+ disabled$="[[__isDayDisabled(item, minDate, maxDate)]]"
121
+ aria-selected$="[[__getDayAriaSelected(item, selectedDate)]]"
122
+ aria-disabled$="[[__getDayAriaDisabled(item, minDate, maxDate)]]"
123
+ aria-label$="[[__getDayAriaLabel(item)]]"
124
+ >[[_getDate(item)]]</td
125
+ >
126
+ </template>
127
+ </tr>
128
+ </template>
129
+ </tbody>
130
+ </table>
97
131
  `;
98
132
  }
99
133
 
@@ -162,6 +196,11 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
162
196
  computed: '_getDays(month, i18n.firstDayOfWeek, minDate, maxDate)'
163
197
  },
164
198
 
199
+ _weeks: {
200
+ type: Array,
201
+ computed: '_getWeeks(_days)'
202
+ },
203
+
165
204
  disabled: {
166
205
  type: Boolean,
167
206
  reflectToAttribute: true,
@@ -171,7 +210,10 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
171
210
  }
172
211
 
173
212
  static get observers() {
174
- return ['_showWeekNumbersChanged(showWeekNumbers, i18n.firstDayOfWeek)'];
213
+ return [
214
+ '_showWeekNumbersChanged(showWeekNumbers, i18n.firstDayOfWeek)',
215
+ '__focusedDateChanged(focusedDate, _days)'
216
+ ];
175
217
  }
176
218
 
177
219
  /** @protected */
@@ -180,12 +222,10 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
180
222
  addListener(this.$.monthGrid, 'tap', this._handleTap.bind(this));
181
223
  }
182
224
 
183
- _dateEquals(date1, date2) {
184
- return dateEquals(date1, date2);
185
- }
186
-
187
- _dateAllowed(date, min, max) {
188
- return dateAllowed(date, min, max);
225
+ get focusableDateElement() {
226
+ return [...this.shadowRoot.querySelectorAll('[part=date]')].find((datePart) => {
227
+ return dateEquals(datePart.date, this.focusedDate);
228
+ });
189
229
  }
190
230
 
191
231
  /* Returns true if all the dates in the month are out of the allowed range */
@@ -212,7 +252,7 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
212
252
  return false;
213
253
  }
214
254
 
215
- return !this._dateAllowed(firstDate, minDate, maxDate) && !this._dateAllowed(lastDate, minDate, maxDate);
255
+ return !dateAllowed(firstDate, minDate, maxDate) && !dateAllowed(lastDate, minDate, maxDate);
216
256
  }
217
257
 
218
258
  _getTitle(month, monthNames) {
@@ -260,6 +300,14 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
260
300
  return weekDayNames;
261
301
  }
262
302
 
303
+ __focusedDateChanged(focusedDate, days) {
304
+ if (days.some((date) => dateEquals(date, focusedDate))) {
305
+ this.removeAttribute('aria-hidden');
306
+ } else {
307
+ this.setAttribute('aria-hidden', 'true');
308
+ }
309
+ }
310
+
263
311
  _getDate(date) {
264
312
  return date ? date.getDate() : '';
265
313
  }
@@ -278,7 +326,7 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
278
326
  }
279
327
 
280
328
  _isToday(date) {
281
- return this._dateEquals(new Date(), date);
329
+ return dateEquals(new Date(), date);
282
330
  }
283
331
 
284
332
  _getDays(month, firstDayOfWeek) {
@@ -308,25 +356,14 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
308
356
  return days;
309
357
  }
310
358
 
311
- _getWeekNumber(date, days) {
312
- if (date === undefined || days === undefined) {
313
- return;
314
- }
315
-
316
- if (!date) {
317
- // Get the first non-null date from the days array.
318
- date = days.reduce((acc, d) => {
319
- return !acc && d ? d : acc;
320
- });
321
- }
322
-
323
- return getISOWeekNumber(date);
324
- }
325
-
326
- _getWeekNumbers(dates) {
327
- return dates
328
- .map((date) => this._getWeekNumber(date, dates))
329
- .filter((week, index, arr) => arr.indexOf(week) === index);
359
+ _getWeeks(days) {
360
+ return days.reduce((acc, day, i) => {
361
+ if (i % 7 === 0) {
362
+ acc.push([]);
363
+ }
364
+ acc[acc.length - 1].push(day);
365
+ return acc;
366
+ }, []);
330
367
  }
331
368
 
332
369
  _handleTap(e) {
@@ -340,11 +377,43 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
340
377
  e.preventDefault();
341
378
  }
342
379
 
343
- _getRole(date) {
344
- return date ? 'button' : 'presentation';
380
+ __getWeekNumber(days) {
381
+ const date = days.reduce((acc, d) => {
382
+ return !acc && d ? d : acc;
383
+ });
384
+
385
+ return getISOWeekNumber(date);
386
+ }
387
+
388
+ __isDayFocused(date, focusedDate) {
389
+ return dateEquals(date, focusedDate);
390
+ }
391
+
392
+ __isDaySelected(date, selectedDate) {
393
+ return dateEquals(date, selectedDate);
394
+ }
395
+
396
+ __getDayAriaSelected(date, selectedDate) {
397
+ if (this.__isDaySelected(date, selectedDate)) {
398
+ return 'true';
399
+ }
400
+ }
401
+
402
+ __isDayDisabled(date, minDate, maxDate) {
403
+ return !dateAllowed(date, minDate, maxDate);
404
+ }
405
+
406
+ __getDayAriaDisabled(date, min, max) {
407
+ if (date === undefined || min === undefined || max === undefined) {
408
+ return;
409
+ }
410
+
411
+ if (this.__isDayDisabled(date, min, max)) {
412
+ return 'true';
413
+ }
345
414
  }
346
415
 
347
- _getAriaLabel(date) {
416
+ __getDayAriaLabel(date) {
348
417
  if (!date) {
349
418
  return '';
350
419
  }
@@ -365,11 +434,18 @@ class MonthCalendar extends ThemableMixin(PolymerElement) {
365
434
  return ariaLabel;
366
435
  }
367
436
 
368
- _getAriaDisabled(date, min, max) {
369
- if (date === undefined || min === undefined || max === undefined) {
370
- return;
437
+ __getDayTabindex(date, focusedDate) {
438
+ if (this.__isDayFocused(date, focusedDate)) {
439
+ return '0';
371
440
  }
372
- return this._dateAllowed(date, min, max) ? 'false' : 'true';
441
+
442
+ return '-1';
443
+ }
444
+
445
+ __getWeekNumbers(dates) {
446
+ return dates
447
+ .map((date) => this.__getWeekNumber(date, dates))
448
+ .filter((week, index, arr) => arr.indexOf(week) === index);
373
449
  }
374
450
  }
375
451