@vaadin/date-picker 23.0.0-alpha5 → 23.0.0-beta4

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-alpha5",
3
+ "version": "23.0.0-beta4",
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-alpha5",
39
- "@vaadin/component-base": "23.0.0-alpha5",
40
- "@vaadin/field-base": "23.0.0-alpha5",
41
- "@vaadin/input-container": "23.0.0-alpha5",
42
- "@vaadin/vaadin-lumo-styles": "23.0.0-alpha5",
43
- "@vaadin/vaadin-material-styles": "23.0.0-alpha5",
44
- "@vaadin/vaadin-overlay": "23.0.0-alpha5",
45
- "@vaadin/vaadin-themable-mixin": "23.0.0-alpha5"
37
+ "@vaadin/button": "23.0.0-beta4",
38
+ "@vaadin/component-base": "23.0.0-beta4",
39
+ "@vaadin/field-base": "23.0.0-beta4",
40
+ "@vaadin/input-container": "23.0.0-beta4",
41
+ "@vaadin/vaadin-lumo-styles": "23.0.0-beta4",
42
+ "@vaadin/vaadin-material-styles": "23.0.0-beta4",
43
+ "@vaadin/vaadin-overlay": "23.0.0-beta4",
44
+ "@vaadin/vaadin-themable-mixin": "23.0.0-beta4"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@esm-bundle/chai": "^4.3.4",
49
- "@vaadin/dialog": "23.0.0-alpha5",
50
- "@vaadin/polymer-legacy-adapter": "23.0.0-alpha5",
48
+ "@vaadin/dialog": "23.0.0-beta4",
49
+ "@vaadin/polymer-legacy-adapter": "23.0.0-beta4",
51
50
  "@vaadin/testing-helpers": "^0.3.2",
52
51
  "sinon": "^9.2.0"
53
52
  },
54
- "gitHead": "74f9294964eb8552d96578c14af6ad214f5257bc"
53
+ "gitHead": "d0b447f1c31ca4256a5e26f2dcd27784447ff79b"
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>
@@ -204,11 +195,11 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
204
195
  buffer-size="12"
205
196
  active="[[initialPosition]]"
206
197
  part="years"
198
+ aria-hidden="true"
207
199
  >
208
200
  <template>
209
201
  <div
210
202
  part="year-number"
211
- role="button"
212
203
  current$="[[_isCurrentYear(index)]]"
213
204
  selected$="[[_isSelectedYear(index, selectedDate)]]"
214
205
  >
@@ -225,12 +216,14 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
225
216
  part="today-button"
226
217
  theme="tertiary"
227
218
  disabled="[[!_isTodayAllowed(minDate, maxDate)]]"
219
+ on-keydown="__onTodayButtonKeyDown"
228
220
  >
229
221
  [[i18n.today]]
230
222
  </vaadin-button>
231
- <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>
232
226
  </div>
233
- <iron-media-query query="(min-width: 375px)" query-matches="{{_desktopMode}}"></iron-media-query>
234
227
  `;
235
228
  }
236
229
 
@@ -275,6 +268,11 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
275
268
 
276
269
  _desktopMode: Boolean,
277
270
 
271
+ _desktopMediaQuery: {
272
+ type: String,
273
+ value: '(min-width: 375px)'
274
+ },
275
+
278
276
  _translateX: {
279
277
  observer: '_translateXChanged'
280
278
  },
@@ -305,8 +303,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
305
303
  */
306
304
  maxDate: Date,
307
305
 
308
- _focused: Boolean,
309
-
310
306
  /**
311
307
  * Input label
312
308
  */
@@ -318,13 +314,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
318
314
  return this.getAttribute('dir') === 'rtl';
319
315
  }
320
316
 
317
+ get focusableDateElement() {
318
+ return [...this.shadowRoot.querySelectorAll('vaadin-month-calendar')]
319
+ .map((calendar) => calendar.focusableDateElement)
320
+ .find(Boolean);
321
+ }
322
+
321
323
  ready() {
322
324
  super.ready();
323
- this.setAttribute('tabindex', 0);
324
- this.addEventListener('keydown', this._onKeydown.bind(this));
325
325
  addListener(this, 'tap', this._stopPropagation);
326
- this.addEventListener('focus', this._onOverlayFocus.bind(this));
327
- this.addEventListener('blur', this._onOverlayBlur.bind(this));
328
326
  addListener(this.$.scrollers, 'track', this._track.bind(this));
329
327
  addListener(this.shadowRoot.querySelector('[part="clear-button"]'), 'tap', this._clear.bind(this));
330
328
  addListener(this.shadowRoot.querySelector('[part="today-button"]'), 'tap', this._onTodayTap.bind(this));
@@ -336,6 +334,12 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
336
334
  'tap',
337
335
  this._toggleYearScroller.bind(this)
338
336
  );
337
+
338
+ this.addController(
339
+ new MediaQueryController(this._desktopMediaQuery, (matches) => {
340
+ this._desktopMode = matches;
341
+ })
342
+ );
339
343
  }
340
344
 
341
345
  /**
@@ -352,25 +356,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
352
356
  setTouchAction(this.$.scrollers, 'pan-y');
353
357
  }
354
358
 
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
359
  /**
375
360
  * Focuses the cancel button
376
361
  */
@@ -422,14 +407,6 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
422
407
  }
423
408
  }
424
409
 
425
- _onOverlayFocus() {
426
- this._focused = true;
427
- }
428
-
429
- _onOverlayBlur() {
430
- this._focused = false;
431
- }
432
-
433
410
  _initialPositionChanged(initialPosition) {
434
411
  this.scrollToDate(initialPosition);
435
412
  }
@@ -521,6 +498,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
521
498
  this.$.monthScroller.position = targetPosition;
522
499
  this._targetPosition = undefined;
523
500
  this._repositionYearScroller();
501
+ this.__tryFocusDate();
524
502
  return;
525
503
  }
526
504
 
@@ -562,6 +540,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
562
540
 
563
541
  this.$.monthScroller.position = this._targetPosition;
564
542
  this._targetPosition = undefined;
543
+ this.__tryFocusDate();
565
544
  }
566
545
 
567
546
  setTimeout(this._repositionYearScroller.bind(this), 1);
@@ -702,150 +681,168 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
702
681
  e.preventDefault();
703
682
  }
704
683
 
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
- }
684
+ __toggleDate(date) {
685
+ if (dateEquals(date, this.selectedDate)) {
686
+ this.selectedDate = '';
687
+ this.focusedDate = date;
688
+ } else {
689
+ this.selectedDate = date;
816
690
  }
817
691
  }
818
692
 
819
- _currentlyFocusedDate() {
820
- 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
+ }
821
791
  }
822
792
 
823
- _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();
824
808
  this.focusedDate = dateToFocus;
825
- 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();
826
823
  }
827
824
 
828
825
  _focusClosestDate(focus) {
829
- this._focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
826
+ this.focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
830
827
  }
831
828
 
832
829
  _moveFocusByDays(days) {
833
- var focus = this._currentlyFocusedDate();
830
+ var focus = this.focusedDate;
834
831
  var dateToFocus = new Date(0, 0);
835
832
  dateToFocus.setFullYear(focus.getFullYear());
836
833
  dateToFocus.setMonth(focus.getMonth());
837
834
  dateToFocus.setDate(focus.getDate() + days);
838
835
 
839
836
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
840
- this._focusDate(dateToFocus);
837
+ this.focusDate(dateToFocus);
841
838
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
842
839
  // Move to min or max date
843
840
  if (days > 0) {
844
841
  // down or right
845
- this._focusDate(this.maxDate);
842
+ this.focusDate(this.maxDate);
846
843
  } else {
847
844
  // up or left
848
- this._focusDate(this.minDate);
845
+ this.focusDate(this.minDate);
849
846
  }
850
847
  } else {
851
848
  // Move to closest allowed date
@@ -854,7 +851,7 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
854
851
  }
855
852
 
856
853
  _moveFocusByMonths(months) {
857
- var focus = this._currentlyFocusedDate();
854
+ var focus = this.focusedDate;
858
855
  var dateToFocus = new Date(0, 0);
859
856
  dateToFocus.setFullYear(focus.getFullYear());
860
857
  dateToFocus.setMonth(focus.getMonth() + months);
@@ -867,15 +864,15 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
867
864
  }
868
865
 
869
866
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
870
- this.focusedDate = dateToFocus;
867
+ this.focusDate(dateToFocus, true);
871
868
  } else if (this._dateAllowed(focus, this.minDate, this.maxDate)) {
872
869
  // Move to min or max date
873
870
  if (months > 0) {
874
871
  // pagedown
875
- this._focusDate(this.maxDate);
872
+ this.focusDate(this.maxDate);
876
873
  } else {
877
874
  // pageup
878
- this._focusDate(this.minDate);
875
+ this.focusDate(this.minDate);
879
876
  }
880
877
  } else {
881
878
  // Move to closest allowed date
@@ -896,10 +893,10 @@ class DatePickerOverlayContent extends ThemableMixin(DirMixin(PolymerElement)) {
896
893
  }
897
894
 
898
895
  if (this._dateAllowed(dateToFocus, this.minDate, this.maxDate)) {
899
- this._focusDate(dateToFocus);
896
+ this.focusDate(dateToFocus);
900
897
  } else if (this._dateAllowed(focusedDate, this.minDate, this.maxDate)) {
901
898
  // Move to minDate or maxDate
902
- this._focusDate(this[property]);
899
+ this.focusDate(this[property]);
903
900
  } else {
904
901
  // Move to closest allowed date
905
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