@vaadin/date-picker 23.0.0-alpha3 → 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-alpha3",
3
+ "version": "23.0.0-beta2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "main": "vaadin-date-picker.js",
20
20
  "module": "vaadin-date-picker.js",
21
+ "type": "module",
21
22
  "files": [
22
23
  "src",
23
24
  "theme",
@@ -34,21 +35,21 @@
34
35
  "@open-wc/dedupe-mixin": "^1.3.0",
35
36
  "@polymer/iron-media-query": "^3.0.0",
36
37
  "@polymer/polymer": "^3.2.0",
37
- "@vaadin/button": "23.0.0-alpha3",
38
- "@vaadin/component-base": "23.0.0-alpha3",
39
- "@vaadin/field-base": "23.0.0-alpha3",
40
- "@vaadin/input-container": "23.0.0-alpha3",
41
- "@vaadin/vaadin-lumo-styles": "23.0.0-alpha3",
42
- "@vaadin/vaadin-material-styles": "23.0.0-alpha3",
43
- "@vaadin/vaadin-overlay": "23.0.0-alpha3",
44
- "@vaadin/vaadin-themable-mixin": "23.0.0-alpha3"
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"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@esm-bundle/chai": "^4.3.4",
48
- "@vaadin/dialog": "23.0.0-alpha3",
49
- "@vaadin/polymer-legacy-adapter": "23.0.0-alpha3",
49
+ "@vaadin/dialog": "23.0.0-beta2",
50
+ "@vaadin/polymer-legacy-adapter": "23.0.0-beta2",
50
51
  "@vaadin/testing-helpers": "^0.3.2",
51
52
  "sinon": "^9.2.0"
52
53
  },
53
- "gitHead": "490037919a9e054cc002c1b3be0c94a1603e1a44"
54
+ "gitHead": "a276f7a0fd00e5459b87267468e0dd0d4fb6f7f3"
54
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
 
@@ -582,7 +576,9 @@ export const DatePickerMixin = (subclass) =>
582
576
  _inputElementChanged(input) {
583
577
  super._inputElementChanged(input);
584
578
  if (input) {
579
+ input.autocomplete = 'off';
585
580
  input.setAttribute('role', 'combobox');
581
+ input.setAttribute('aria-haspopup', 'dialog');
586
582
  input.setAttribute('aria-expanded', !!this.opened);
587
583
  this._applyInputValue(this._selectedDate);
588
584
  }
@@ -709,7 +705,7 @@ export const DatePickerMixin = (subclass) =>
709
705
  }
710
706
 
711
707
  if (this._focusOverlayOnOpen) {
712
- this._overlayContent.focus();
708
+ this._overlayContent.focusDateElement();
713
709
  this._focusOverlayOnOpen = false;
714
710
  } else {
715
711
  this._focus();
@@ -718,8 +714,6 @@ export const DatePickerMixin = (subclass) =>
718
714
  if (this._noInput && this.focusElement) {
719
715
  this.focusElement.blur();
720
716
  }
721
-
722
- this._ignoreAnnounce = false;
723
717
  }
724
718
 
725
719
  // A hack needed for iOS to prevent dropdown from being clipped in an
@@ -764,8 +758,6 @@ export const DatePickerMixin = (subclass) =>
764
758
 
765
759
  /** @protected */
766
760
  _onOverlayClosed() {
767
- this._ignoreAnnounce = true;
768
-
769
761
  window.removeEventListener('scroll', this._boundOnScroll, true);
770
762
 
771
763
  if (this._touchPrevented) {
@@ -890,15 +882,14 @@ export const DatePickerMixin = (subclass) =>
890
882
  case 'ArrowUp':
891
883
  // prevent scrolling the page with arrows
892
884
  e.preventDefault();
893
-
894
885
  if (this.opened) {
895
- this._overlayContent.focus();
896
- 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();
897
889
  } else {
898
890
  this._focusOverlayOnOpen = true;
899
891
  this.open();
900
892
  }
901
-
902
893
  break;
903
894
  case 'Enter': {
904
895
  const parsedDate = this._getParsedDate();
@@ -944,8 +935,7 @@ export const DatePickerMixin = (subclass) =>
944
935
  if (e.shiftKey) {
945
936
  this._overlayContent.focusCancel();
946
937
  } else {
947
- this._overlayContent.focus();
948
- this._overlayContent.revealDate(this._focusedDate);
938
+ this._overlayContent.focusDate(this._focusedDate);
949
939
  }
950
940
  }
951
941
  break;
@@ -993,13 +983,6 @@ export const DatePickerMixin = (subclass) =>
993
983
  }
994
984
  }
995
985
 
996
- /** @private */
997
- _announceFocusedDate(_focusedDate, opened, _ignoreAnnounce) {
998
- if (opened && !_ignoreAnnounce) {
999
- this._overlayContent.announceFocusedDate();
1000
- }
1001
- }
1002
-
1003
986
  /** @private */
1004
987
  get _overlayContent() {
1005
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>
@@ -204,11 +194,11 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
204
194
  buffer-size="12"
205
195
  active="[[initialPosition]]"
206
196
  part="years"
197
+ aria-hidden="true"
207
198
  >
208
199
  <template>
209
200
  <div
210
201
  part="year-number"
211
- role="button"
212
202
  current$="[[_isCurrentYear(index)]]"
213
203
  selected$="[[_isSelectedYear(index, selectedDate)]]"
214
204
  >
@@ -225,10 +215,13 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
225
215
  part="today-button"
226
216
  theme="tertiary"
227
217
  disabled="[[!_isTodayAllowed(minDate, maxDate)]]"
218
+ on-keydown="__onTodayButtonKeyDown"
228
219
  >
229
220
  [[i18n.today]]
230
221
  </vaadin-button>
231
- <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>
232
225
  </div>
233
226
  <iron-media-query query="(min-width: 375px)" query-matches="{{_desktopMode}}"></iron-media-query>
234
227
  `;
@@ -305,8 +298,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
305
298
  */
306
299
  maxDate: Date,
307
300
 
308
- _focused: Boolean,
309
-
310
301
  /**
311
302
  * Input label
312
303
  */
@@ -318,13 +309,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
318
309
  return this.getAttribute('dir') === 'rtl';
319
310
  }
320
311
 
312
+ get focusableDateElement() {
313
+ return [...this.shadowRoot.querySelectorAll('vaadin-month-calendar')]
314
+ .map((calendar) => calendar.focusableDateElement)
315
+ .find(Boolean);
316
+ }
317
+
321
318
  ready() {
322
319
  super.ready();
323
- this.setAttribute('tabindex', 0);
324
- this.addEventListener('keydown', this._onKeydown.bind(this));
325
320
  addListener(this, 'tap', this._stopPropagation);
326
- this.addEventListener('focus', this._onOverlayFocus.bind(this));
327
- this.addEventListener('blur', this._onOverlayBlur.bind(this));
328
321
  addListener(this.$.scrollers, 'track', this._track.bind(this));
329
322
  addListener(this.shadowRoot.querySelector('[part="clear-button"]'), 'tap', this._clear.bind(this));
330
323
  addListener(this.shadowRoot.querySelector('[part="today-button"]'), 'tap', this._onTodayTap.bind(this));
@@ -352,25 +345,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
352
345
  setTouchAction(this.$.scrollers, 'pan-y');
353
346
  }
354
347
 
355
- announceFocusedDate() {
356
- const focusedDate = this._currentlyFocusedDate();
357
- let messages = [];
358
- if (dateEquals(focusedDate, new Date())) {
359
- messages.push(this.i18n.today);
360
- }
361
- messages = messages.concat([
362
- this.i18n.weekdays[focusedDate.getDay()],
363
- focusedDate.getDate(),
364
- this.i18n.monthNames[focusedDate.getMonth()],
365
- focusedDate.getFullYear()
366
- ]);
367
- if (this.showWeekNumbers && this.i18n.firstDayOfWeek === 1) {
368
- messages.push(this.i18n.week);
369
- messages.push(getISOWeekNumber(focusedDate));
370
- }
371
- announce(messages.join(' '));
372
- }
373
-
374
348
  /**
375
349
  * Focuses the cancel button
376
350
  */
@@ -422,14 +396,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
422
396
  }
423
397
  }
424
398
 
425
- _onOverlayFocus() {
426
- this._focused = true;
427
- }
428
-
429
- _onOverlayBlur() {
430
- this._focused = false;
431
- }
432
-
433
399
  _initialPositionChanged(initialPosition) {
434
400
  this.scrollToDate(initialPosition);
435
401
  }
@@ -521,6 +487,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
521
487
  this.$.monthScroller.position = targetPosition;
522
488
  this._targetPosition = undefined;
523
489
  this._repositionYearScroller();
490
+ this.__tryFocusDate();
524
491
  return;
525
492
  }
526
493
 
@@ -562,6 +529,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
562
529
 
563
530
  this.$.monthScroller.position = this._targetPosition;
564
531
  this._targetPosition = undefined;
532
+ this.__tryFocusDate();
565
533
  }
566
534
 
567
535
  setTimeout(this._repositionYearScroller.bind(this), 1);
@@ -702,150 +670,168 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
702
670
  e.preventDefault();
703
671
  }
704
672
 
705
- _onKeydown(e) {
706
- var focus = this._currentlyFocusedDate();
707
-
708
- // Cannot use (today/cancel).focused flag because vaadin-text-field removes it
709
- // previously in the keydown event.
710
- const isToday = e.composedPath().indexOf(this.$.todayButton) >= 0;
711
- const isCancel = e.composedPath().indexOf(this.$.cancelButton) >= 0;
712
- const isScroller = !isToday && !isCancel;
713
-
714
- // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
715
- const navigationKeys = [
716
- ' ',
717
- 'ArrowDown',
718
- 'ArrowUp',
719
- 'ArrowRight',
720
- 'ArrowLeft',
721
- 'Enter',
722
- 'End',
723
- 'Escape',
724
- 'Home',
725
- 'PageUp',
726
- 'PageDown',
727
- 'Tab'
728
- ];
729
-
730
- const eventKey = e.key;
731
- if (eventKey === 'Tab') {
732
- // We handle tabs here and don't want to bubble up.
733
- e.stopPropagation();
734
-
735
- const isFullscreen = this.hasAttribute('fullscreen');
736
- const isShift = e.shiftKey;
737
-
738
- if (isFullscreen) {
739
- e.preventDefault();
740
- } else if ((isShift && isScroller) || (!isShift && isCancel)) {
741
- // Return focus back to the input field
742
- e.preventDefault();
743
- this.dispatchEvent(new CustomEvent('focus-input', { bubbles: true, composed: true }));
744
- } else if (isShift && isToday) {
745
- // Browser returns focus back to the scrollable area. We need to set
746
- // the focused flag, and move the scroll to focused date.
747
- this._focused = true;
748
- setTimeout(() => this.revealDate(this.focusedDate), 1);
749
- } else {
750
- // Browser moves the focus out of the scroller, hence focused flag must
751
- // set to false.
752
- this._focused = false;
753
- }
754
- } else if (navigationKeys.includes(eventKey)) {
755
- e.preventDefault();
756
- e.stopPropagation();
757
- switch (eventKey) {
758
- case 'ArrowDown':
759
- this._moveFocusByDays(7);
760
- this.focus();
761
- break;
762
- case 'ArrowUp':
763
- this._moveFocusByDays(-7);
764
- this.focus();
765
- break;
766
- case 'ArrowRight':
767
- if (isScroller) {
768
- this._moveFocusByDays(this.__isRTL ? -1 : 1);
769
- }
770
- break;
771
- case 'ArrowLeft':
772
- if (isScroller) {
773
- this._moveFocusByDays(this.__isRTL ? 1 : -1);
774
- }
775
- break;
776
- case 'Enter':
777
- if (isScroller || isCancel) {
778
- this._close();
779
- } else if (isToday) {
780
- this._onTodayTap();
781
- }
782
- break;
783
- case ' ':
784
- if (isCancel) {
785
- this._close();
786
- } else if (isToday) {
787
- this._onTodayTap();
788
- } else {
789
- var focusedDate = this.focusedDate;
790
- if (dateEquals(focusedDate, this.selectedDate)) {
791
- this.selectedDate = '';
792
- this.focusedDate = focusedDate;
793
- } else {
794
- this.selectedDate = focusedDate;
795
- }
796
- }
797
- break;
798
- case 'Home':
799
- this._moveFocusInsideMonth(focus, 'minDate');
800
- break;
801
- case 'End':
802
- this._moveFocusInsideMonth(focus, 'maxDate');
803
- break;
804
- case 'PageDown':
805
- this._moveFocusByMonths(e.shiftKey ? 12 : 1);
806
- break;
807
- case 'PageUp':
808
- this._moveFocusByMonths(e.shiftKey ? -12 : -1);
809
- break;
810
- case 'Escape':
811
- this._cancel();
812
- break;
813
- default:
814
- break;
815
- }
673
+ __toggleDate(date) {
674
+ if (dateEquals(date, this.selectedDate)) {
675
+ this.selectedDate = '';
676
+ this.focusedDate = date;
677
+ } else {
678
+ this.selectedDate = date;
816
679
  }
817
680
  }
818
681
 
819
- _currentlyFocusedDate() {
820
- 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
+ }
821
793
  }
822
794
 
823
- _focusDate(dateToFocus) {
795
+ async focusDate(date, keepMonth) {
796
+ const dateToFocus = date || this.selectedDate || this.initialPosition || new Date();
824
797
  this.focusedDate = dateToFocus;
825
- 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();
826
812
  }
827
813
 
828
814
  _focusClosestDate(focus) {
829
- this._focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
815
+ this.focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
830
816
  }
831
817
 
832
818
  _moveFocusByDays(days) {
833
- var focus = this._currentlyFocusedDate();
819
+ var focus = this.focusedDate;
834
820
  var dateToFocus = new Date(0, 0);
835
821
  dateToFocus.setFullYear(focus.getFullYear());
836
822
  dateToFocus.setMonth(focus.getMonth());
837
823
  dateToFocus.setDate(focus.getDate() + days);
838
824
 
839
825
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
840
- this._focusDate(dateToFocus);
826
+ this.focusDate(dateToFocus);
841
827
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
842
828
  // Move to min or max date
843
829
  if (days > 0) {
844
830
  // down or right
845
- this._focusDate(this.maxDate);
831
+ this.focusDate(this.maxDate);
846
832
  } else {
847
833
  // up or left
848
- this._focusDate(this.minDate);
834
+ this.focusDate(this.minDate);
849
835
  }
850
836
  } else {
851
837
  // Move to closest allowed date
@@ -854,7 +840,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
854
840
  }
855
841
 
856
842
  _moveFocusByMonths(months) {
857
- var focus = this._currentlyFocusedDate();
843
+ var focus = this.focusedDate;
858
844
  var dateToFocus = new Date(0, 0);
859
845
  dateToFocus.setFullYear(focus.getFullYear());
860
846
  dateToFocus.setMonth(focus.getMonth() + months);
@@ -867,15 +853,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
867
853
  }
868
854
 
869
855
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
870
- this.focusedDate = dateToFocus;
856
+ this.focusDate(dateToFocus, true);
871
857
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
872
858
  // Move to min or max date
873
859
  if (months > 0) {
874
860
  // pagedown
875
- this._focusDate(this.maxDate);
861
+ this.focusDate(this.maxDate);
876
862
  } else {
877
863
  // pageup
878
- this._focusDate(this.minDate);
864
+ this.focusDate(this.minDate);
879
865
  }
880
866
  } else {
881
867
  // Move to closest allowed date
@@ -896,10 +882,10 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
896
882
  }
897
883
 
898
884
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
899
- this._focusDate(dateToFocus);
885
+ this.focusDate(dateToFocus);
900
886
  } else if (this._dateAllowed(focusedDate, this.minDate, this.maxDate)) {
901
887
  // Move to minDate or maxDate
902
- this._focusDate(this[property]);
888
+ this.focusDate(this[property]);
903
889
  } else {
904
890
  // Move to closest allowed date
905
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