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

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-beta2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,21 +35,21 @@
35
35
  "@open-wc/dedupe-mixin": "^1.3.0",
36
36
  "@polymer/iron-media-query": "^3.0.0",
37
37
  "@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"
38
+ "@vaadin/button": "23.0.0-beta2",
39
+ "@vaadin/component-base": "23.0.0-beta2",
40
+ "@vaadin/field-base": "23.0.0-beta2",
41
+ "@vaadin/input-container": "23.0.0-beta2",
42
+ "@vaadin/vaadin-lumo-styles": "23.0.0-beta2",
43
+ "@vaadin/vaadin-material-styles": "23.0.0-beta2",
44
+ "@vaadin/vaadin-overlay": "23.0.0-beta2",
45
+ "@vaadin/vaadin-themable-mixin": "23.0.0-beta2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@esm-bundle/chai": "^4.3.4",
49
- "@vaadin/dialog": "23.0.0-beta1",
50
- "@vaadin/polymer-legacy-adapter": "23.0.0-beta1",
49
+ "@vaadin/dialog": "23.0.0-beta2",
50
+ "@vaadin/polymer-legacy-adapter": "23.0.0-beta2",
51
51
  "@vaadin/testing-helpers": "^0.3.2",
52
52
  "sinon": "^9.2.0"
53
53
  },
54
- "gitHead": "467244b76021176c109df675799b07029b293e58"
54
+ "gitHead": "a276f7a0fd00e5459b87267468e0dd0d4fb6f7f3"
55
55
  }
@@ -319,11 +319,6 @@ export const DatePickerMixin = (subclass) =>
319
319
  value: document.createElement('div').style.webkitOverflowScrolling === ''
320
320
  },
321
321
 
322
- /** @private */
323
- _ignoreAnnounce: {
324
- value: true
325
- },
326
-
327
322
  /** @private */
328
323
  _focusOverlayOnOpen: Boolean,
329
324
 
@@ -335,8 +330,7 @@ export const DatePickerMixin = (subclass) =>
335
330
  static get observers() {
336
331
  return [
337
332
  '_selectedDateChanged(_selectedDate, i18n.formatDate)',
338
- '_focusedDateChanged(_focusedDate, i18n.formatDate)',
339
- '_announceFocusedDate(_focusedDate, opened, _ignoreAnnounce)'
333
+ '_focusedDateChanged(_focusedDate, i18n.formatDate)'
340
334
  ];
341
335
  }
342
336
 
@@ -584,6 +578,7 @@ export const DatePickerMixin = (subclass) =>
584
578
  if (input) {
585
579
  input.autocomplete = 'off';
586
580
  input.setAttribute('role', 'combobox');
581
+ input.setAttribute('aria-haspopup', 'dialog');
587
582
  input.setAttribute('aria-expanded', !!this.opened);
588
583
  this._applyInputValue(this._selectedDate);
589
584
  }
@@ -710,7 +705,7 @@ export const DatePickerMixin = (subclass) =>
710
705
  }
711
706
 
712
707
  if (this._focusOverlayOnOpen) {
713
- this._overlayContent.focus();
708
+ this._overlayContent.focusDateElement();
714
709
  this._focusOverlayOnOpen = false;
715
710
  } else {
716
711
  this._focus();
@@ -719,8 +714,6 @@ export const DatePickerMixin = (subclass) =>
719
714
  if (this._noInput && this.focusElement) {
720
715
  this.focusElement.blur();
721
716
  }
722
-
723
- this._ignoreAnnounce = false;
724
717
  }
725
718
 
726
719
  // A hack needed for iOS to prevent dropdown from being clipped in an
@@ -765,8 +758,6 @@ export const DatePickerMixin = (subclass) =>
765
758
 
766
759
  /** @protected */
767
760
  _onOverlayClosed() {
768
- this._ignoreAnnounce = true;
769
-
770
761
  window.removeEventListener('scroll', this._boundOnScroll, true);
771
762
 
772
763
  if (this._touchPrevented) {
@@ -891,15 +882,14 @@ export const DatePickerMixin = (subclass) =>
891
882
  case 'ArrowUp':
892
883
  // prevent scrolling the page with arrows
893
884
  e.preventDefault();
894
-
895
885
  if (this.opened) {
896
- this._overlayContent.focus();
897
- this._overlayContent._onKeydown(e);
886
+ // The overlay can be opened with ctrl + option + shift in VoiceOver
887
+ // and without this logic, it won't be possible to focus the dialog opened this way.
888
+ this._overlayContent.focusDateElement();
898
889
  } else {
899
890
  this._focusOverlayOnOpen = true;
900
891
  this.open();
901
892
  }
902
-
903
893
  break;
904
894
  case 'Enter': {
905
895
  const parsedDate = this._getParsedDate();
@@ -945,8 +935,7 @@ export const DatePickerMixin = (subclass) =>
945
935
  if (e.shiftKey) {
946
936
  this._overlayContent.focusCancel();
947
937
  } else {
948
- this._overlayContent.focus();
949
- this._overlayContent.revealDate(this._focusedDate);
938
+ this._overlayContent.focusDate(this._focusedDate);
950
939
  }
951
940
  }
952
941
  break;
@@ -994,13 +983,6 @@ export const DatePickerMixin = (subclass) =>
994
983
  }
995
984
  }
996
985
 
997
- /** @private */
998
- _announceFocusedDate(_focusedDate, opened, _ignoreAnnounce) {
999
- if (opened && !_ignoreAnnounce) {
1000
- this._overlayContent.announceFocusedDate();
1001
- }
1002
- }
1003
-
1004
986
  /** @private */
1005
987
  get _overlayContent() {
1006
988
  return this.$.overlay.content.querySelector('#overlay-content');
@@ -8,13 +8,12 @@ import '@vaadin/button/src/vaadin-button.js';
8
8
  import './vaadin-month-calendar.js';
9
9
  import './vaadin-infinite-scroller.js';
10
10
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
11
- import { announce } from '@vaadin/component-base/src/a11y-announcer.js';
12
11
  import { timeOut } from '@vaadin/component-base/src/async.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';
16
15
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
17
- import { dateEquals, extractDateParts, getClosestDate, getISOWeekNumber } from './vaadin-date-picker-helper.js';
16
+ import { dateEquals, extractDateParts, getClosestDate } from './vaadin-date-picker-helper.js';
18
17
 
19
18
  /**
20
19
  * @extends HTMLElement
@@ -150,17 +149,8 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
150
149
  z-index: 1;
151
150
  padding: 8px;
152
151
  }
153
-
154
- #announcer {
155
- display: inline-block;
156
- position: fixed;
157
- clip: rect(0, 0, 0, 0);
158
- clip-path: inset(100%);
159
- }
160
152
  </style>
161
153
 
162
- <div id="announcer" role="alert" aria-live="polite">[[i18n.calendar]]</div>
163
-
164
154
  <div part="overlay-header" on-touchend="_preventDefault" desktop$="[[_desktopMode]]" aria-hidden="true">
165
155
  <div part="label">[[_formatDisplayed(selectedDate, i18n.formatDate, label)]]</div>
166
156
  <div part="clear-button" showclear$="[[_showClear(selectedDate)]]"></div>
@@ -190,9 +180,9 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
190
180
  show-week-numbers="[[showWeekNumbers]]"
191
181
  min-date="[[minDate]]"
192
182
  max-date="[[maxDate]]"
193
- focused$="[[_focused]]"
194
183
  part="month"
195
184
  theme$="[[theme]]"
185
+ on-keydown="__onMonthCalendarKeyDown"
196
186
  >
197
187
  </vaadin-month-calendar>
198
188
  </template>
@@ -209,7 +199,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
209
199
  <template>
210
200
  <div
211
201
  part="year-number"
212
- role="button"
213
202
  current$="[[_isCurrentYear(index)]]"
214
203
  selected$="[[_isSelectedYear(index, selectedDate)]]"
215
204
  >
@@ -226,10 +215,13 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
226
215
  part="today-button"
227
216
  theme="tertiary"
228
217
  disabled="[[!_isTodayAllowed(minDate, maxDate)]]"
218
+ on-keydown="__onTodayButtonKeyDown"
229
219
  >
230
220
  [[i18n.today]]
231
221
  </vaadin-button>
232
- <vaadin-button id="cancelButton" part="cancel-button" theme="tertiary"> [[i18n.cancel]] </vaadin-button>
222
+ <vaadin-button id="cancelButton" part="cancel-button" theme="tertiary" on-keydown="__onCancelButtonKeyDown">
223
+ [[i18n.cancel]]
224
+ </vaadin-button>
233
225
  </div>
234
226
  <iron-media-query query="(min-width: 375px)" query-matches="{{_desktopMode}}"></iron-media-query>
235
227
  `;
@@ -306,8 +298,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
306
298
  */
307
299
  maxDate: Date,
308
300
 
309
- _focused: Boolean,
310
-
311
301
  /**
312
302
  * Input label
313
303
  */
@@ -319,13 +309,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
319
309
  return this.getAttribute('dir') === 'rtl';
320
310
  }
321
311
 
312
+ get focusableDateElement() {
313
+ return [...this.shadowRoot.querySelectorAll('vaadin-month-calendar')]
314
+ .map((calendar) => calendar.focusableDateElement)
315
+ .find(Boolean);
316
+ }
317
+
322
318
  ready() {
323
319
  super.ready();
324
- this.setAttribute('tabindex', 0);
325
- this.addEventListener('keydown', this._onKeydown.bind(this));
326
320
  addListener(this, 'tap', this._stopPropagation);
327
- this.addEventListener('focus', this._onOverlayFocus.bind(this));
328
- this.addEventListener('blur', this._onOverlayBlur.bind(this));
329
321
  addListener(this.$.scrollers, 'track', this._track.bind(this));
330
322
  addListener(this.shadowRoot.querySelector('[part="clear-button"]'), 'tap', this._clear.bind(this));
331
323
  addListener(this.shadowRoot.querySelector('[part="today-button"]'), 'tap', this._onTodayTap.bind(this));
@@ -353,25 +345,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
353
345
  setTouchAction(this.$.scrollers, 'pan-y');
354
346
  }
355
347
 
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
348
  /**
376
349
  * Focuses the cancel button
377
350
  */
@@ -423,14 +396,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
423
396
  }
424
397
  }
425
398
 
426
- _onOverlayFocus() {
427
- this._focused = true;
428
- }
429
-
430
- _onOverlayBlur() {
431
- this._focused = false;
432
- }
433
-
434
399
  _initialPositionChanged(initialPosition) {
435
400
  this.scrollToDate(initialPosition);
436
401
  }
@@ -522,6 +487,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
522
487
  this.$.monthScroller.position = targetPosition;
523
488
  this._targetPosition = undefined;
524
489
  this._repositionYearScroller();
490
+ this.__tryFocusDate();
525
491
  return;
526
492
  }
527
493
 
@@ -563,6 +529,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
563
529
 
564
530
  this.$.monthScroller.position = this._targetPosition;
565
531
  this._targetPosition = undefined;
532
+ this.__tryFocusDate();
566
533
  }
567
534
 
568
535
  setTimeout(this._repositionYearScroller.bind(this), 1);
@@ -703,150 +670,168 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
703
670
  e.preventDefault();
704
671
  }
705
672
 
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
- }
673
+ __toggleDate(date) {
674
+ if (dateEquals(date, this.selectedDate)) {
675
+ this.selectedDate = '';
676
+ this.focusedDate = date;
677
+ } else {
678
+ this.selectedDate = date;
817
679
  }
818
680
  }
819
681
 
820
- _currentlyFocusedDate() {
821
- return this.focusedDate || this.selectedDate || this.initialPosition || new Date();
682
+ __onMonthCalendarKeyDown(event) {
683
+ let handled = false;
684
+
685
+ switch (event.key) {
686
+ case 'ArrowDown':
687
+ this._moveFocusByDays(7);
688
+ handled = true;
689
+ break;
690
+ case 'ArrowUp':
691
+ this._moveFocusByDays(-7);
692
+ handled = true;
693
+ break;
694
+ case 'ArrowRight':
695
+ this._moveFocusByDays(this.__isRTL ? -1 : 1);
696
+ handled = true;
697
+ break;
698
+ case 'ArrowLeft':
699
+ this._moveFocusByDays(this.__isRTL ? 1 : -1);
700
+ handled = true;
701
+ break;
702
+ case 'Enter':
703
+ this.selectedDate = this.focusedDate;
704
+ this._close();
705
+ handled = true;
706
+ break;
707
+ case ' ':
708
+ this.__toggleDate(this.focusedDate);
709
+ handled = true;
710
+ break;
711
+ case 'Home':
712
+ this._moveFocusInsideMonth(this.focusedDate, 'minDate');
713
+ handled = true;
714
+ break;
715
+ case 'End':
716
+ this._moveFocusInsideMonth(this.focusedDate, 'maxDate');
717
+ handled = true;
718
+ break;
719
+ case 'PageDown':
720
+ this._moveFocusByMonths(event.shiftKey ? 12 : 1);
721
+ handled = true;
722
+ break;
723
+ case 'PageUp':
724
+ this._moveFocusByMonths(event.shiftKey ? -12 : -1);
725
+ handled = true;
726
+ break;
727
+ case 'Escape':
728
+ this._cancel();
729
+ handled = true;
730
+ break;
731
+ default:
732
+ break;
733
+ }
734
+
735
+ if (handled) {
736
+ event.preventDefault();
737
+ event.stopPropagation();
738
+ }
739
+ }
740
+
741
+ __onTodayButtonKeyDown(event) {
742
+ if (this.hasAttribute('fullscreen')) {
743
+ event.stopPropagation();
744
+ return;
745
+ }
746
+
747
+ if (event.key === 'Tab' && event.shiftKey) {
748
+ event.stopPropagation();
749
+
750
+ // Browser returns focus back to the calendar.
751
+ // We need to move the scroll to focused date.
752
+ setTimeout(() => this.revealDate(this.focusedDate), 1);
753
+ }
754
+
755
+ if (event.key === 'Escape') {
756
+ this._cancel();
757
+ event.preventDefault();
758
+ event.stopPropagation();
759
+ }
760
+ }
761
+
762
+ __onCancelButtonKeyDown(event) {
763
+ if (this.hasAttribute('fullscreen')) {
764
+ event.stopPropagation();
765
+ return;
766
+ }
767
+
768
+ if (event.key === 'Tab' && !event.shiftKey) {
769
+ // Return focus back to the input field
770
+ event.preventDefault();
771
+ event.stopPropagation();
772
+ this.dispatchEvent(new CustomEvent('focus-input', { bubbles: true, composed: true }));
773
+ }
774
+
775
+ if (event.key === 'Escape') {
776
+ this._cancel();
777
+ event.preventDefault();
778
+ event.stopPropagation();
779
+ }
780
+ }
781
+
782
+ __tryFocusDate() {
783
+ const dateToFocus = this.__pendingDateFocus;
784
+ if (dateToFocus) {
785
+ // Check the date element with tabindex="0"
786
+ const dateElement = this.focusableDateElement;
787
+
788
+ if (dateElement && dateEquals(dateElement.date, this.__pendingDateFocus)) {
789
+ delete this.__pendingDateFocus;
790
+ dateElement.focus();
791
+ }
792
+ }
822
793
  }
823
794
 
824
- _focusDate(dateToFocus) {
795
+ async focusDate(date, keepMonth) {
796
+ const dateToFocus = date || this.selectedDate || this.initialPosition || new Date();
825
797
  this.focusedDate = dateToFocus;
826
- this._focusedMonthDate = dateToFocus.getDate();
798
+ if (!keepMonth) {
799
+ this._focusedMonthDate = dateToFocus.getDate();
800
+ }
801
+ await this.focusDateElement();
802
+ }
803
+
804
+ async focusDateElement() {
805
+ this.__pendingDateFocus = this.focusedDate;
806
+
807
+ await new Promise((resolve) => {
808
+ requestAnimationFrame(resolve);
809
+ });
810
+
811
+ this.__tryFocusDate();
827
812
  }
828
813
 
829
814
  _focusClosestDate(focus) {
830
- this._focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
815
+ this.focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
831
816
  }
832
817
 
833
818
  _moveFocusByDays(days) {
834
- var focus = this._currentlyFocusedDate();
819
+ var focus = this.focusedDate;
835
820
  var dateToFocus = new Date(0, 0);
836
821
  dateToFocus.setFullYear(focus.getFullYear());
837
822
  dateToFocus.setMonth(focus.getMonth());
838
823
  dateToFocus.setDate(focus.getDate() + days);
839
824
 
840
825
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
841
- this._focusDate(dateToFocus);
826
+ this.focusDate(dateToFocus);
842
827
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
843
828
  // Move to min or max date
844
829
  if (days > 0) {
845
830
  // down or right
846
- this._focusDate(this.maxDate);
831
+ this.focusDate(this.maxDate);
847
832
  } else {
848
833
  // up or left
849
- this._focusDate(this.minDate);
834
+ this.focusDate(this.minDate);
850
835
  }
851
836
  } else {
852
837
  // Move to closest allowed date
@@ -855,7 +840,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
855
840
  }
856
841
 
857
842
  _moveFocusByMonths(months) {
858
- var focus = this._currentlyFocusedDate();
843
+ var focus = this.focusedDate;
859
844
  var dateToFocus = new Date(0, 0);
860
845
  dateToFocus.setFullYear(focus.getFullYear());
861
846
  dateToFocus.setMonth(focus.getMonth() + months);
@@ -868,15 +853,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
868
853
  }
869
854
 
870
855
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
871
- this.focusedDate = dateToFocus;
856
+ this.focusDate(dateToFocus, true);
872
857
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
873
858
  // Move to min or max date
874
859
  if (months > 0) {
875
860
  // pagedown
876
- this._focusDate(this.maxDate);
861
+ this.focusDate(this.maxDate);
877
862
  } else {
878
863
  // pageup
879
- this._focusDate(this.minDate);
864
+ this.focusDate(this.minDate);
880
865
  }
881
866
  } else {
882
867
  // Move to closest allowed date
@@ -897,10 +882,10 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
897
882
  }
898
883
 
899
884
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
900
- this._focusDate(dateToFocus);
885
+ this.focusDate(dateToFocus);
901
886
  } else if (this._dateAllowed(focusedDate, this.minDate, this.maxDate)) {
902
887
  // Move to minDate or maxDate
903
- this._focusDate(this[property]);
888
+ this.focusDate(this[property]);
904
889
  } else {
905
890
  // Move to closest allowed date
906
891
  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);
@@ -9,7 +9,6 @@ import './vaadin-date-picker-overlay.js';
9
9
  import './vaadin-date-picker-overlay-content.js';
10
10
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
11
11
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
12
- import { addListener } from '@vaadin/component-base/src/gestures.js';
13
12
  import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
14
13
  import { InputController } from '@vaadin/field-base/src/input-controller.js';
15
14
  import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
@@ -144,8 +143,8 @@ class DatePicker extends DatePickerMixin(InputControlMixin(ThemableMixin(Element
144
143
  >
145
144
  <slot name="prefix" slot="prefix"></slot>
146
145
  <slot name="input"></slot>
147
- <div id="clearButton" part="clear-button" slot="suffix"></div>
148
- <div part="toggle-button" slot="suffix" role="button"></div>
146
+ <div id="clearButton" part="clear-button" slot="suffix" aria-hidden="true"></div>
147
+ <div part="toggle-button" slot="suffix" aria-hidden="true" on-click="_toggle"></div>
149
148
  </vaadin-input-container>
150
149
 
151
150
  <div part="helper-text">
@@ -213,7 +212,9 @@ class DatePicker extends DatePickerMixin(InputControlMixin(ThemableMixin(Element
213
212
  })
214
213
  );
215
214
  this.addController(new LabelledInputController(this.inputElement, this._labelController));
216
- addListener(this.shadowRoot.querySelector('[part="toggle-button"]'), 'tap', this._toggle.bind(this));
215
+
216
+ const toggleButton = this.shadowRoot.querySelector('[part="toggle-button"]');
217
+ toggleButton.addEventListener('mousedown', (e) => e.preventDefault());
217
218
  }
218
219
 
219
220
  /** @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