@refinitiv-ui/elements 5.8.1 → 5.9.1

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/lib/calendar/constants.d.ts +4 -0
  3. package/lib/calendar/constants.js +5 -0
  4. package/lib/calendar/index.d.ts +106 -17
  5. package/lib/calendar/index.js +486 -120
  6. package/lib/calendar/locales.js +2 -1
  7. package/lib/calendar/themes/halo/dark/index.js +1 -1
  8. package/lib/calendar/themes/halo/light/index.js +1 -1
  9. package/lib/calendar/themes/solar/charcoal/index.js +1 -1
  10. package/lib/calendar/themes/solar/pearl/index.js +1 -1
  11. package/lib/calendar/types.d.ts +11 -8
  12. package/lib/calendar/utils.js +2 -1
  13. package/lib/combo-box/custom-elements.json +23 -1
  14. package/lib/combo-box/custom-elements.md +8 -1
  15. package/lib/combo-box/helpers/renderer.d.ts +8 -0
  16. package/lib/combo-box/helpers/renderer.js +24 -0
  17. package/lib/combo-box/index.d.ts +36 -16
  18. package/lib/combo-box/index.js +60 -32
  19. package/lib/combo-box/themes/halo/dark/index.js +1 -2
  20. package/lib/combo-box/themes/halo/light/index.js +1 -2
  21. package/lib/combo-box/themes/solar/charcoal/index.js +1 -2
  22. package/lib/combo-box/themes/solar/pearl/index.js +1 -2
  23. package/lib/time-picker/custom-elements.json +2 -4
  24. package/lib/time-picker/custom-elements.md +3 -3
  25. package/lib/time-picker/index.d.ts +92 -62
  26. package/lib/time-picker/index.js +278 -158
  27. package/lib/tree-select/themes/halo/dark/index.js +1 -2
  28. package/lib/tree-select/themes/halo/light/index.js +1 -2
  29. package/lib/tree-select/themes/solar/charcoal/index.js +1 -2
  30. package/lib/tree-select/themes/solar/pearl/index.js +1 -2
  31. package/lib/version.js +1 -1
  32. package/package.json +291 -10
@@ -2,15 +2,21 @@ import { __decorate } from "tslib";
2
2
  import { ControlElement, html, css, WarningNotice } from '@refinitiv-ui/core';
3
3
  import { customElement } from '@refinitiv-ui/core/lib/decorators/custom-element.js';
4
4
  import { property } from '@refinitiv-ui/core/lib/decorators/property.js';
5
+ import { state } from '@refinitiv-ui/core/lib/decorators/state.js';
5
6
  import { ifDefined } from '@refinitiv-ui/core/lib/directives/if-defined.js';
7
+ import { cache } from '@refinitiv-ui/core/lib/directives/cache.js';
8
+ import { guard } from '@refinitiv-ui/core/lib/directives/guard.js';
9
+ import { ref, createRef } from '@refinitiv-ui/core/lib/directives/ref.js';
6
10
  import { VERSION } from '../version.js';
7
11
  import { isIE } from '@refinitiv-ui/utils/lib/browser.js';
8
- import { DateFormat, format, utcFormat, utcParse, isValidDate, isWeekend, isAfter, isBefore, addMonths, subMonths, isToday, isThisMonth, isThisYear, isSameDay, isSameMonth, isSameYear, toDateSegment } from '@refinitiv-ui/utils/lib/date.js';
12
+ import { DateFormat, format, utcFormat, utcParse, isValidDate, isWeekend, isAfter, isBefore, addMonths, subMonths, isToday, isThisMonth, isThisYear, isSameDay, isSameMonth, isSameYear, toDateSegment, parse } from '@refinitiv-ui/utils/lib/date.js';
13
+ import { left, right, up, down, first, last } from '@refinitiv-ui/utils/lib/navigation.js';
9
14
  import { monthInfo, weekdaysNames, monthsNames, formatLocaleDate, ViewFormatTranslateParams } from './utils.js';
10
15
  import { translate, getLocale, TranslatePropertyKey } from '@refinitiv-ui/translate';
11
- import { RenderView, FIRST_DAY_OF_WEEK, YEARS_PER_YEAR_VIEW, DAY_VIEW, YEAR_VIEW, MONTH_VIEW } from './constants.js';
16
+ import { RenderView, CalendarLocaleScope, FIRST_DAY_OF_WEEK, YEARS_PER_YEAR_VIEW, DAY_VIEW, YEAR_VIEW, MONTH_VIEW } from './constants.js';
12
17
  import './locales.js';
13
18
  import '../button/index.js';
19
+ import '@refinitiv-ui/phrasebook/lib/locale/en/calendar.js';
14
20
  /**
15
21
  * Standard calendar element
16
22
  *
@@ -28,6 +34,11 @@ import '../button/index.js';
28
34
  let Calendar = class Calendar extends ControlElement {
29
35
  constructor() {
30
36
  super(...arguments);
37
+ this.defaultRole = 'group';
38
+ /**
39
+ * Reference to the view button
40
+ */
41
+ this.viewBtnRef = createRef();
31
42
  this._min = '';
32
43
  this._max = '';
33
44
  /**
@@ -62,8 +73,20 @@ let Calendar = class Calendar extends ControlElement {
62
73
  /**
63
74
  * Used for internal navigation between render views
64
75
  */
65
- this.renderView = RenderView.DAY;
66
- this.isDateAvailable = null; /* a constructed filter based on multiple local filters */
76
+ this._renderView = RenderView.DAY;
77
+ /**
78
+ * Used for keyboard navigation when trying
79
+ * to restore focus on re-render and control navigation
80
+ */
81
+ this._activeCellIndex = null;
82
+ // Used to store current navigation map
83
+ this.navigationGrid = [];
84
+ /**
85
+ * Connected to role. If false, the values are not announced in the screen reader
86
+ */
87
+ this.announceValues = true;
88
+ // Cashed filter, which is constructed based on multiple local filters
89
+ this.isDateAvailable = null;
67
90
  }
68
91
  /**
69
92
  * Element version number
@@ -83,14 +106,14 @@ let Calendar = class Calendar extends ControlElement {
83
106
  :host {
84
107
  display: inline-block;
85
108
  }
86
- [part=navigation], [part=navigation] section {
109
+ [part~=navigation], [part~=navigation] section {
87
110
  display: flex;
88
111
  flex-flow: row nowrap;
89
112
  }
90
- [part=navigation] {
113
+ [part~=navigation] {
91
114
  justify-content: space-between;
92
115
  }
93
- [part=navigation] > div {
116
+ [part~=navigation] > div {
94
117
  display: flex;
95
118
  flex: 1;
96
119
  justify-content: center;
@@ -101,7 +124,6 @@ let Calendar = class Calendar extends ControlElement {
101
124
  bottom: 0;
102
125
  left: 0;
103
126
  right: 0;
104
- pointer-events: none;
105
127
  display: flex;
106
128
  align-items: center;
107
129
  justify-content: center;
@@ -131,7 +153,10 @@ let Calendar = class Calendar extends ControlElement {
131
153
  width: calc(100% / ${DAY_VIEW.columnCount});
132
154
  padding-top: calc(100% / ${DAY_VIEW.columnCount});
133
155
  }
134
- [part~=cell][tabindex] {
156
+ [part~=cell-content]:not([tabindex]) {
157
+ pointer-events: none;
158
+ }
159
+ [part~=selection] {
135
160
  cursor: pointer;
136
161
  }
137
162
  `;
@@ -187,6 +212,7 @@ let Calendar = class Calendar extends ControlElement {
187
212
  const oldView = this._view;
188
213
  if (oldView !== view) {
189
214
  this._view = view;
215
+ this.resetActiveCellIndex();
190
216
  this.requestUpdate('view', oldView);
191
217
  }
192
218
  }
@@ -240,27 +266,82 @@ let Calendar = class Calendar extends ControlElement {
240
266
  get values() {
241
267
  return this._values.concat();
242
268
  }
269
+ get renderView() {
270
+ return this._renderView;
271
+ }
272
+ set renderView(renderView) {
273
+ const oldRenderView = this._renderView;
274
+ if (oldRenderView !== renderView) {
275
+ this._renderView = renderView;
276
+ // always reset active cell to not focus on potentially invalid cell
277
+ this.resetActiveCellIndex();
278
+ this.requestUpdate('renderView', oldRenderView);
279
+ }
280
+ }
281
+ get activeCellIndex() {
282
+ return this._activeCellIndex;
283
+ }
284
+ set activeCellIndex(activeCellIndex) {
285
+ const oldCellIndex = this._activeCellIndex;
286
+ if (String(activeCellIndex) !== String(oldCellIndex)) {
287
+ this._activeCellIndex = activeCellIndex;
288
+ this.requestUpdate('activeCellIndex', oldCellIndex);
289
+ }
290
+ }
243
291
  /**
244
- * Get weekday numbers.
245
- * Sort the list based on first day of the week
292
+ * Silently reset cell index without calling request update
293
+ * @returns {void}
246
294
  */
247
- get weekdaysNames() {
248
- const firstDayOfWeek = this.firstDayOfWeek;
249
- const localWeekdaysNames = this.localWeekdaysNames;
250
- return localWeekdaysNames.slice(firstDayOfWeek).concat(localWeekdaysNames.slice(0, firstDayOfWeek));
295
+ resetActiveCellIndex() {
296
+ this._activeCellIndex = null;
297
+ }
298
+ /**
299
+ * Get an active element
300
+ */
301
+ get activeElement() {
302
+ return this.shadowRoot.activeElement;
303
+ }
304
+ /**
305
+ * Get selectable date button element by index
306
+ * @param index Cell index
307
+ * @returns button HTML date button element or null
308
+ */
309
+ getDateButtonByIndex(index) {
310
+ const elements = Array.from(this.renderRoot.querySelectorAll('[part~=cell] > [part~=selection]'));
311
+ return elements.find((element) => this.isDateButton(element) && String(element.index) === String(index));
251
312
  }
252
313
  /**
253
- * Get localised month names from January to December
314
+ * Get active date button element
315
+ * @returns button HTML date button element or null
254
316
  */
255
- get monthsNames() {
256
- return this.localMonthsNames;
317
+ get activeDateButton() {
318
+ return this.renderRoot.querySelector('[part~=cell][active] > [part~=selection]');
319
+ }
320
+ /**
321
+ * Return true if passed target is HTML
322
+ * date button element that can be selected
323
+ * @param target Target to check
324
+ * @returns isDateButtonElement
325
+ */
326
+ isDateButton(target) {
327
+ return target.index !== undefined;
328
+ }
329
+ static get observedAttributes() {
330
+ const observed = super.observedAttributes;
331
+ return ['role'].concat(observed);
332
+ }
333
+ attributeChangedCallback(name, oldValue, newValue) {
334
+ super.attributeChangedCallback(name, oldValue, newValue);
335
+ if (name === 'role') {
336
+ this.announceValues = !(!newValue || newValue === 'none' || newValue === 'presentation');
337
+ }
257
338
  }
258
339
  /**
259
340
  * Perform asynchronous update
260
341
  * @returns promise
261
342
  */
262
343
  async performUpdate() {
263
- const localFirstDayOfWeek = Number(await this.tPromise('FIRST_DAY_OF_WEEK'));
344
+ const localFirstDayOfWeek = Number(await this.dateTPromise('FIRST_DAY_OF_WEEK'));
264
345
  this.localFirstDayOfWeek = isNaN(localFirstDayOfWeek) ? FIRST_DAY_OF_WEEK : (localFirstDayOfWeek % 7);
265
346
  void super.performUpdate();
266
347
  }
@@ -271,14 +352,39 @@ let Calendar = class Calendar extends ControlElement {
271
352
  */
272
353
  update(changedProperties) {
273
354
  if (!this.localMonthsNames || changedProperties.has(TranslatePropertyKey)) {
274
- this.localMonthsNames = monthsNames(getLocale(this));
355
+ const locale = getLocale(this);
356
+ this.localMonthsNames = monthsNames(locale);
275
357
  }
276
358
  if (!this.localWeekdaysNames || changedProperties.has(TranslatePropertyKey)) {
277
- this.localWeekdaysNames = weekdaysNames(getLocale(this));
359
+ const locale = getLocale(this);
360
+ const longWeekdaysNames = weekdaysNames(locale, 'long');
361
+ this.localWeekdaysNames = weekdaysNames(locale).map((narrow, index) => {
362
+ return { narrow, long: longWeekdaysNames[index] };
363
+ });
278
364
  }
279
365
  this.shouldConstructFilters(changedProperties) && this.constructFilters();
280
366
  super.update(changedProperties);
281
367
  }
368
+ /**
369
+ * Called after render life-cycle finished
370
+ * @param changedProperties Properties which have changed
371
+ * @return {void}
372
+ */
373
+ updated(changedProperties) {
374
+ super.updated(changedProperties);
375
+ // This code is here to ensure that focus is not lost
376
+ // while navigating through the render views using keyboard
377
+ if (this.focused && changedProperties.has('renderView') && this.viewBtnRef.value && this.activeElement !== this.viewBtnRef.value) {
378
+ this.viewBtnRef.value.focus();
379
+ }
380
+ const cellIndex = this.activeCellIndex;
381
+ if (cellIndex && changedProperties.has('activeCellIndex')) {
382
+ const dateButtonEl = this.getDateButtonByIndex(cellIndex);
383
+ if (dateButtonEl && this.activeElement !== dateButtonEl) {
384
+ dateButtonEl.focus();
385
+ }
386
+ }
387
+ }
282
388
  /**
283
389
  * Run when an element has been first updated
284
390
  * @param changedProperties properties that was changed on first update
@@ -286,7 +392,7 @@ let Calendar = class Calendar extends ControlElement {
286
392
  */
287
393
  firstUpdated(changedProperties) {
288
394
  super.firstUpdated(changedProperties);
289
- this.renderRoot.addEventListener('keydown', event => this.onTableKeyDown(event));
395
+ this.renderRoot.addEventListener('keydown', event => this.onKeyDown(event));
290
396
  }
291
397
  /**
292
398
  * Show invalid view message
@@ -418,78 +524,88 @@ let Calendar = class Calendar extends ControlElement {
418
524
  rangeTo
419
525
  };
420
526
  }
527
+ /**
528
+ * Set navigation map based on rows
529
+ * @param rows A collection of rows with cells
530
+ * @returns {void}
531
+ */
532
+ setNavigationMap(rows) {
533
+ this.navigationGrid = rows.map(row => row.map(cell => cell.value && !cell.disabled ? 1 : 0));
534
+ }
421
535
  /**
422
536
  * Run when next button is tapped.
423
537
  * Change current view to next view
538
+ * @param event Next view tap event
424
539
  * @returns {void}
425
540
  */
426
- onNextTap() {
427
- let viewSegment = toDateSegment(this.view);
428
- switch (this.renderView) {
429
- case RenderView.DAY:
430
- viewSegment = toDateSegment(addMonths(this.view, 1));
431
- break;
432
- case RenderView.MONTH:
433
- viewSegment.year += 1;
434
- break;
435
- case RenderView.YEAR:
436
- viewSegment.year += YEARS_PER_YEAR_VIEW;
437
- break;
438
- // no default
541
+ onNextTap(event) {
542
+ if (!event.defaultPrevented) {
543
+ this.toNextView();
439
544
  }
440
- this.notifyViewChange(viewSegment);
441
545
  }
442
546
  /**
443
547
  * Run when previous button is tapped.
444
548
  * Change current view to previous view
549
+ * @param event Previous view tap event
445
550
  * @returns {void}
446
551
  */
447
- onPreviousTap() {
448
- let viewSegment = toDateSegment(this.view);
449
- switch (this.renderView) {
450
- case RenderView.DAY:
451
- viewSegment = toDateSegment(subMonths(this.view, 1));
452
- break;
453
- case RenderView.MONTH:
454
- viewSegment.year -= 1;
455
- break;
456
- case RenderView.YEAR:
457
- viewSegment.year -= YEARS_PER_YEAR_VIEW;
458
- break;
459
- // no default
552
+ onPreviousTap(event) {
553
+ if (!event.defaultPrevented) {
554
+ this.toPreviousView();
460
555
  }
461
- this.notifyViewChange(viewSegment);
462
556
  }
463
557
  /**
464
558
  * Run when change view button is tapped.
465
559
  * Switch between views
560
+ * @param event Render view tap event
466
561
  * @returns {void}
467
562
  */
468
- onRenderViewTap() {
469
- this.renderView = this.renderView === RenderView.DAY ? RenderView.YEAR : RenderView.DAY;
563
+ onRenderViewTap(event) {
564
+ if (!event.defaultPrevented) {
565
+ this.renderView = this.renderView === RenderView.DAY ? RenderView.YEAR : RenderView.DAY;
566
+ }
470
567
  }
471
568
  /**
472
569
  * Run when key down event happens on calendar
473
570
  * @param event Keyboard event
474
571
  * @returns {void}
475
572
  */
476
- onTableKeyDown(event) {
573
+ onKeyDown(event) {
574
+ if (event.defaultPrevented) {
575
+ return;
576
+ }
477
577
  switch (event.key) {
478
- case ' ':
479
- case 'Enter':
480
- case 'Spacebar':
481
- event.preventDefault();
482
- this.onTableTap(event);
483
- break;
484
578
  case 'Esc':
485
579
  case 'Escape':
486
580
  if (this.renderView === RenderView.YEAR || this.renderView === RenderView.MONTH) {
487
- event.preventDefault();
488
581
  this.renderView = RenderView.DAY;
582
+ break;
489
583
  }
584
+ return;
585
+ case 'Up': // IE11
586
+ case 'ArrowUp':
587
+ void this.onNavigation('ArrowUp');
490
588
  break;
491
- // no default
589
+ case 'Down':
590
+ case 'ArrowDown':
591
+ void this.onNavigation('ArrowDown');
592
+ break;
593
+ case 'Left':
594
+ case 'ArrowLeft':
595
+ void this.onNavigation('ArrowLeft');
596
+ break;
597
+ case 'Right':
598
+ case 'ArrowRight':
599
+ void this.onNavigation('ArrowRight');
600
+ break;
601
+ case 'Home':
602
+ case 'End':
603
+ void this.onNavigation(event.key);
604
+ break;
605
+ default:
606
+ return;
492
607
  }
608
+ event.preventDefault();
493
609
  }
494
610
  /**
495
611
  * Run when tap event happens ot table.
@@ -498,10 +614,14 @@ let Calendar = class Calendar extends ControlElement {
498
614
  * @returns {void}
499
615
  */
500
616
  onTableTap(event) {
501
- const cell = event.target; /* here we just emulate interface */
502
- if (!cell || !cell.value) {
617
+ if (event.defaultPrevented) {
618
+ return;
619
+ }
620
+ const cell = event.target;
621
+ if (!cell || !this.isDateButton(cell) || !cell.value) {
503
622
  return;
504
623
  }
624
+ this.activeCellIndex = cell.index;
505
625
  const cellSegment = toDateSegment(cell.value);
506
626
  const viewSegment = toDateSegment(this.view);
507
627
  if (this.renderView === RenderView.YEAR) { /* YEAR -> MONTH */
@@ -521,6 +641,117 @@ let Calendar = class Calendar extends ControlElement {
521
641
  }
522
642
  this.onTapSelectValue(cell.value);
523
643
  }
644
+ /**
645
+ * Navigate over the grid
646
+ * @param key Navigation direction
647
+ * @returns navigation promise
648
+ */
649
+ async onNavigation(key) {
650
+ const grid = this.navigationGrid;
651
+ switch (key) {
652
+ case 'Home':
653
+ this.activeCellIndex = first(grid);
654
+ return;
655
+ case 'End':
656
+ this.activeCellIndex = last(grid);
657
+ return;
658
+ // no default
659
+ }
660
+ // no previously selected cell, but there is cell which is a candidate for navigation
661
+ const activeDateEl = this.activeDateButton;
662
+ if (!this.activeCellIndex && activeDateEl) {
663
+ this.activeCellIndex = activeDateEl.index;
664
+ // current cell is already in focus (e.g. via Tab key, continue navigation from that point)
665
+ if (!(this.activeElement === activeDateEl)) {
666
+ return;
667
+ }
668
+ }
669
+ const activeCellIndex = this.activeCellIndex;
670
+ // All cells are disabled
671
+ if (!activeCellIndex) {
672
+ return;
673
+ }
674
+ // active cell is selected
675
+ if (activeCellIndex) {
676
+ let newActiveCell;
677
+ switch (key) {
678
+ case 'ArrowUp':
679
+ newActiveCell = up(grid, activeCellIndex);
680
+ break;
681
+ case 'ArrowDown':
682
+ newActiveCell = down(grid, activeCellIndex);
683
+ break;
684
+ case 'ArrowLeft':
685
+ newActiveCell = left(grid, activeCellIndex);
686
+ break;
687
+ case 'ArrowRight':
688
+ newActiveCell = right(grid, activeCellIndex);
689
+ break;
690
+ // no default
691
+ }
692
+ // Standard navigation withing the same view
693
+ if (newActiveCell) {
694
+ this.activeCellIndex = newActiveCell;
695
+ return;
696
+ }
697
+ }
698
+ // Jump to the next view
699
+ switch (key) {
700
+ // case 'ArrowUp': // it feels better not having Up/Down in these case
701
+ case 'ArrowLeft':
702
+ this.toPreviousView();
703
+ await this.updateComplete; // must wait until the render cycle has finished
704
+ await this.onNavigation('End');
705
+ break;
706
+ // case 'ArrowDown':
707
+ case 'ArrowRight':
708
+ this.toNextView();
709
+ await this.updateComplete;
710
+ await this.onNavigation('Home');
711
+ break;
712
+ // no default
713
+ }
714
+ }
715
+ /**
716
+ * Navigate to the next view
717
+ * @returns {void}
718
+ */
719
+ toNextView() {
720
+ let viewSegment = toDateSegment(this.view);
721
+ switch (this.renderView) {
722
+ case RenderView.DAY:
723
+ viewSegment = toDateSegment(addMonths(this.view, 1));
724
+ break;
725
+ case RenderView.MONTH:
726
+ viewSegment.year += 1;
727
+ break;
728
+ case RenderView.YEAR:
729
+ viewSegment.year += YEARS_PER_YEAR_VIEW;
730
+ break;
731
+ // no default
732
+ }
733
+ this.notifyViewChange(viewSegment);
734
+ }
735
+ /**
736
+ * Navigate to the previous view
737
+ * @returns {void}
738
+ */
739
+ toPreviousView() {
740
+ let viewSegment = toDateSegment(this.view);
741
+ switch (this.renderView) {
742
+ case RenderView.DAY:
743
+ viewSegment = toDateSegment(subMonths(this.view, 1));
744
+ break;
745
+ case RenderView.MONTH:
746
+ viewSegment.year -= 1;
747
+ break;
748
+ case RenderView.YEAR:
749
+ viewSegment.year -= YEARS_PER_YEAR_VIEW;
750
+ break;
751
+ // no default
752
+ }
753
+ this.notifyViewChange(viewSegment);
754
+ }
524
755
  /**
525
756
  * Run when tap event happened on DAY view and the cell has the values
526
757
  * Try to select/deselect cell value
@@ -533,12 +764,14 @@ let Calendar = class Calendar extends ControlElement {
533
764
  }
534
765
  let values;
535
766
  if (this.multiple) {
536
- values = this.values.filter(oldValue => {
537
- return oldValue !== value;
538
- });
539
- if (this.values.length === values.length) {
767
+ values = this.values.concat([]);
768
+ const valueIdx = this.values.indexOf(value);
769
+ if (valueIdx === -1) {
540
770
  values.push(value);
541
771
  }
772
+ else {
773
+ values.splice(valueIdx, 1);
774
+ }
542
775
  }
543
776
  else if (this.range) {
544
777
  if (!this.values.length) {
@@ -554,12 +787,16 @@ let Calendar = class Calendar extends ControlElement {
554
787
  values = [value];
555
788
  }
556
789
  }
557
- else {
790
+ else if (this.values.indexOf(value) === -1) {
558
791
  values = [value];
559
792
  }
793
+ else {
794
+ // remove range if start/end index match
795
+ values = [];
796
+ }
560
797
  }
561
798
  else {
562
- values = [value];
799
+ values = this.value === value ? [] : [value];
563
800
  }
564
801
  this.notifyValuesChange(values);
565
802
  }
@@ -603,11 +840,7 @@ let Calendar = class Calendar extends ControlElement {
603
840
  if (isIE && isBC) {
604
841
  return html `${formatLocaleDate(date, getLocale(this), includeMonth, includeEra)}`;
605
842
  }
606
- return html `${this.t('VIEW_FORMAT', {
607
- date,
608
- includeMonth,
609
- includeEra
610
- }, ViewFormatTranslateParams)}`;
843
+ return html `${this.dateT('VIEW_FORMAT', { date, includeMonth, includeEra }, ViewFormatTranslateParams)}`;
611
844
  }
612
845
  /**
613
846
  * Get a string representation of current view
@@ -630,14 +863,49 @@ let Calendar = class Calendar extends ControlElement {
630
863
  }
631
864
  }
632
865
  /**
633
- * Render cell content template.
634
- * If the cell is selectable (aka has value) add selection part
635
- * @param text Text to render
636
- * @param selectable True if cell may be selected
637
- * @returns template result
866
+ * Set an active state of the cell based
867
+ * @param rows A collection of rows with cells
868
+ * @returns {void}
638
869
  */
639
- renderContentBox(text = '', selectable = false) {
640
- return html `<div part="cell-content${selectable ? ' selection' : ''}">${text}</div>`;
870
+ setActiveCell(rows) {
871
+ const setActive = (cell) => {
872
+ if (cell) {
873
+ cell.active = true;
874
+ }
875
+ };
876
+ const columnIdx = this.activeCellIndex ? this.activeCellIndex[0] : NaN;
877
+ const rowIdx = this.activeCellIndex ? this.activeCellIndex[1] : NaN;
878
+ // Selected cell is active or today cell or first available cell
879
+ let activeCell;
880
+ let nowCell;
881
+ let firstCell;
882
+ let selectedCell;
883
+ for (let i = 0; i < rows.length; i += 1) {
884
+ const row = rows[i];
885
+ for (let e = 0; e < row.length; e += 1) {
886
+ const cell = row[e];
887
+ if (cell.disabled || !cell.value) {
888
+ continue;
889
+ }
890
+ if (i === rowIdx && e === columnIdx) {
891
+ activeCell = cell;
892
+ }
893
+ if (!selectedCell && cell.selected) {
894
+ selectedCell = cell;
895
+ }
896
+ if (cell.now) {
897
+ nowCell = cell;
898
+ }
899
+ if (!firstCell) {
900
+ firstCell = cell;
901
+ }
902
+ }
903
+ }
904
+ // If a cell that was active before last render is not available now, remove index
905
+ if (!activeCell && this.activeCellIndex) {
906
+ this.resetActiveCellIndex(); // set on private to not cause another re-render
907
+ }
908
+ setActive(activeCell || selectedCell || nowCell || firstCell);
641
909
  }
642
910
  /**
643
911
  * Get year view template
@@ -653,18 +921,18 @@ let Calendar = class Calendar extends ControlElement {
653
921
  for (let i = 0; i < YEAR_VIEW.totalCount; i += 1) {
654
922
  if (i % YEAR_VIEW.columnCount === 0) {
655
923
  cells = [];
656
- rows.push({
657
- cells
658
- });
924
+ rows.push(cells);
659
925
  }
660
926
  const year = startIdx + i;
661
927
  const value = utcFormat({ year, month: 0, day: 1 }, DateFormat.yyyyMMdd);
662
- cell = Object.assign({ view, text: year > 0 ? `${year}` : year === 0 ? '1' : `${Math.abs(year - 1)}`, value: `${year}`, now: isThisYear(value) }, this.getCellSelection(value, isSameYear));
928
+ cell = Object.assign({ view, text: year > 0 ? `${year}` : year === 0 ? '1' : `${Math.abs(year - 1)}`, value: `${year}`, now: isThisYear(value), index: [cells.length, rows.length - 1] }, this.getCellSelection(value, isSameYear));
663
929
  cells.push(cell);
664
930
  years.push(cell);
665
931
  }
666
932
  years[0].firstDate = true;
667
933
  years[years.length - 1].lastDate = true;
934
+ this.setActiveCell(rows);
935
+ this.setNavigationMap(rows);
668
936
  return html `${this.renderRows(rows)}`;
669
937
  }
670
938
  /**
@@ -676,7 +944,7 @@ let Calendar = class Calendar extends ControlElement {
676
944
  const columnCount = MONTH_VIEW.columnCount;
677
945
  const monthCount = 12;
678
946
  const totalCount = MONTH_VIEW.totalCount;
679
- const monthsNames = this.monthsNames;
947
+ const monthsNames = this.localMonthsNames;
680
948
  const before = (totalCount - monthCount) / 2;
681
949
  const startIdx = monthCount - before % monthCount;
682
950
  const after = before + monthCount;
@@ -687,21 +955,21 @@ let Calendar = class Calendar extends ControlElement {
687
955
  for (let i = 0; i < totalCount; i += 1) {
688
956
  if (i % columnCount === 0) {
689
957
  cells = [];
690
- rows.push({
691
- cells
692
- });
958
+ rows.push(cells);
693
959
  }
694
960
  const month = (startIdx + i) % monthCount; /* 0 for Jan, 11 for Dev */
695
961
  const year = currentYear + Math.floor((i - before) / monthCount);
696
962
  const segment = { year, month, day: 1 };
697
963
  const value = utcFormat(segment, DateFormat.yyyyMMdd);
698
964
  const idle = i < before || i >= after;
699
- cell = Object.assign({ view, text: monthsNames[month], value: utcFormat(segment, DateFormat.yyyyMM), idle, now: isThisMonth(value) }, this.getCellSelection(value, isSameMonth));
965
+ cell = Object.assign({ view, text: monthsNames[month], value: utcFormat(segment, DateFormat.yyyyMM), idle, now: isThisMonth(value), index: [cells.length, rows.length - 1] }, this.getCellSelection(value, isSameMonth));
700
966
  cells.push(cell);
701
967
  months.push(cell);
702
968
  }
703
969
  months[0].firstDate = true;
704
970
  months[months.length - 1].lastDate = true;
971
+ this.setActiveCell(rows);
972
+ this.setNavigationMap(rows);
705
973
  return html `${this.renderRows(rows)}`;
706
974
  }
707
975
  /**
@@ -724,15 +992,14 @@ let Calendar = class Calendar extends ControlElement {
724
992
  for (let i = 0; i < DAY_VIEW.totalCount; i += 1) {
725
993
  if (i % DAY_VIEW.columnCount === 0) {
726
994
  cells = [];
727
- rows.push({
728
- cells
729
- });
995
+ rows.push(cells);
730
996
  }
731
997
  const datePadding = i - padding + 1;
732
998
  if (datePadding <= 0) {
733
999
  if (!this.fillCells) {
734
1000
  cells.push({
735
- view
1001
+ view,
1002
+ index: [cells.length, rows.length - 1]
736
1003
  });
737
1004
  continue;
738
1005
  }
@@ -743,7 +1010,8 @@ let Calendar = class Calendar extends ControlElement {
743
1010
  else if (datePadding > viewMonth.days) {
744
1011
  if (!this.fillCells) {
745
1012
  cells.push({
746
- view
1013
+ view,
1014
+ index: [cells.length, rows.length - 1]
747
1015
  });
748
1016
  continue;
749
1017
  }
@@ -759,14 +1027,16 @@ let Calendar = class Calendar extends ControlElement {
759
1027
  const value = utcFormat({ year, month, day }, DateFormat.yyyyMMdd);
760
1028
  const disabled = this.isDateAvailable ? !this.isDateAvailable(value) : false;
761
1029
  const dayCell = Object.assign({ view, text: day.toString(), value,
762
- disabled, idle: month !== viewMonth.month || year !== viewMonth.year, now: isToday(value) }, this.getCellSelection(value, isSameDay));
1030
+ disabled, idle: month !== viewMonth.month || year !== viewMonth.year, now: isToday(value), index: [cells.length, rows.length - 1] }, this.getCellSelection(value, isSameDay));
763
1031
  cells.push(dayCell);
764
1032
  days.push(dayCell);
765
1033
  }
766
1034
  days[0].firstDate = true;
767
1035
  days[days.length - 1].lastDate = true;
1036
+ this.setActiveCell(rows);
1037
+ this.setNavigationMap(rows);
768
1038
  return html `
769
- ${this.renderWeekdayNames}
1039
+ ${guard([this.firstDayOfWeek, this.lang], () => this.renderWeekdayNames)}
770
1040
  ${this.renderRows(rows)}
771
1041
  `;
772
1042
  }
@@ -774,23 +1044,50 @@ let Calendar = class Calendar extends ControlElement {
774
1044
  * Get weekday names template
775
1045
  */
776
1046
  get renderWeekdayNames() {
1047
+ const firstDayOfWeek = this.firstDayOfWeek;
1048
+ const weekdaysNames = this.localWeekdaysNames.slice(firstDayOfWeek).concat(this.localWeekdaysNames.slice(0, firstDayOfWeek));
777
1049
  return html `
778
- <div part="row day-name-row">${this.weekdaysNames.map(day => html `<div part="cell day-name">${this.renderContentBox(day)}</div>`)}</div>
779
- `;
1050
+ <div role="row"
1051
+ part="row day-name-row">${weekdaysNames.map(day => html `
1052
+ <div scope="col" role="columnheader" part="cell day-name" abbr="${day.long}">
1053
+ <div part="cell-content">${day.narrow}</div>
1054
+ </div>
1055
+ `)}</div>`;
780
1056
  }
781
1057
  /**
782
1058
  * Render a view based on the current render view
783
1059
  */
784
1060
  get viewRender() {
1061
+ let renderView;
785
1062
  switch (this.renderView) {
786
1063
  case RenderView.MONTH:
787
- return this.monthView;
1064
+ renderView = this.monthView;
1065
+ break;
788
1066
  case RenderView.YEAR:
789
- return this.yearView;
1067
+ renderView = this.yearView;
1068
+ break;
790
1069
  case RenderView.DAY:
791
1070
  default:
792
- return this.dayView;
1071
+ renderView = this.dayView;
1072
+ }
1073
+ return html `${cache(renderView)}`;
1074
+ }
1075
+ /**
1076
+ * Get cell translate label key based on selected state
1077
+ * @param cell Cell
1078
+ * @returns key Translate label key
1079
+ */
1080
+ getCellLabelKey(cell) {
1081
+ if (cell.selected && cell.now) {
1082
+ return 'SELECTED_NOW';
1083
+ }
1084
+ if (cell.selected) {
1085
+ return 'SELECTED';
793
1086
  }
1087
+ if (cell.now) {
1088
+ return 'NOW';
1089
+ }
1090
+ return 'CELL_LABEL';
794
1091
  }
795
1092
  /**
796
1093
  * Render cell template. Cell can be a day, month or year
@@ -798,10 +1095,13 @@ let Calendar = class Calendar extends ControlElement {
798
1095
  * @returns template result
799
1096
  */
800
1097
  renderCell(cell) {
1098
+ const isSelectable = cell.value !== undefined && !cell.disabled;
801
1099
  return html `<div
1100
+ role="gridcell"
802
1101
  part="cell ${cell.view}"
1102
+ aria-selected="${ifDefined(isSelectable ? (cell.selected ? 'true' : 'false') : undefined)}"
1103
+ ?active=${cell.active}
803
1104
  ?disabled=${cell.disabled}
804
- .value=${cell.value}
805
1105
  ?idle=${cell.idle}
806
1106
  ?today=${cell.now}
807
1107
  ?first-date=${cell.firstDate}
@@ -809,9 +1109,18 @@ let Calendar = class Calendar extends ControlElement {
809
1109
  ?selected=${cell.selected}
810
1110
  ?range=${cell.range}
811
1111
  ?range-from=${cell.rangeFrom}
812
- ?range-to=${cell.rangeTo}
813
- tabindex=${ifDefined(cell.value !== undefined && !cell.disabled ? 0 : undefined)}
814
- >${this.renderContentBox(cell.text, cell.value !== undefined)}</div>`;
1112
+ ?range-to=${cell.rangeTo}>
1113
+ <div role="${ifDefined(cell.value ? 'button' : undefined)}"
1114
+ tabindex=${ifDefined(isSelectable ? (cell.active ? 0 : -1) : undefined)}
1115
+ aria-label="${ifDefined(isSelectable && !isIE ? this.t(this.getCellLabelKey(cell), {
1116
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1117
+ value: parse(cell.value),
1118
+ view: this.renderView
1119
+ }) : undefined)}"
1120
+ part="cell-content${isSelectable ? ' selection' : ''}"
1121
+ .value=${cell.value}
1122
+ .index=${cell.index}>${cell.text}</div>
1123
+ </div>`;
815
1124
  }
816
1125
  /**
817
1126
  * Render view rows
@@ -819,32 +1128,80 @@ let Calendar = class Calendar extends ControlElement {
819
1128
  * @returns template result
820
1129
  */
821
1130
  renderRows(rows) {
822
- return rows.map(row => html `<div part="row">${row.cells.map(cell => this.renderCell(cell))}</div>`);
1131
+ return rows.map(row => html `<div role="row" part="row">${row.map(cell => this.renderCell(cell))}</div>`);
823
1132
  }
824
1133
  /**
825
- * A `TemplateResult` that will be used
826
- * to render the updated internal template.
827
- * @return Render template
1134
+ * Render button navigation template
1135
+ * @returns template result
828
1136
  */
829
- render() {
830
- return html `
831
- <div part="navigation">
1137
+ get buttonNavigationTemplate() {
1138
+ let prevBtnAriaLabel = this.t('PREVIOUS_MONTH');
1139
+ let nextBtnAriaLabel = this.t('NEXT_MONTH');
1140
+ let viewBtnAriaLabel = this.t('YEAR_SELECTOR');
1141
+ switch (this.renderView) {
1142
+ case RenderView.YEAR:
1143
+ prevBtnAriaLabel = this.t('PREVIOUS_DECADE');
1144
+ nextBtnAriaLabel = this.t('NEXT_DECADE');
1145
+ viewBtnAriaLabel = this.t('DATE_SELECTOR');
1146
+ break;
1147
+ case RenderView.MONTH:
1148
+ prevBtnAriaLabel = this.t('PREVIOUS_YEAR');
1149
+ nextBtnAriaLabel = this.t('NEXT_YEAR');
1150
+ viewBtnAriaLabel = this.t('DATE_SELECTOR');
1151
+ break;
1152
+ // RenderView.DAY
1153
+ // no default
1154
+ }
1155
+ return html `<div part="navigation">
832
1156
  <ef-button
833
1157
  part="btn-prev"
1158
+ aria-label="${prevBtnAriaLabel}"
834
1159
  icon="left"
835
1160
  @tap=${this.onPreviousTap}></ef-button>
836
1161
  <ef-button
1162
+ ${ref(this.viewBtnRef)}
1163
+ aria-description="${viewBtnAriaLabel}"
837
1164
  part="btn-view"
838
1165
  textpos="before"
839
1166
  .icon="${this.renderView === RenderView.DAY ? 'down' : 'up'}"
840
1167
  @tap="${this.onRenderViewTap}">${this.formattedViewRender}</ef-button>
841
1168
  <ef-button
842
1169
  part="btn-next"
1170
+ aria-label="${nextBtnAriaLabel}"
843
1171
  icon="right"
844
1172
  @tap=${this.onNextTap}></ef-button>
845
- </div>
846
- <div part="table"
847
- @tap=${this.onTableTap}>${this.viewRender}</div>
1173
+ </div>`;
1174
+ }
1175
+ /**
1176
+ * A template used to notify currently selected value for screen readers
1177
+ * @returns template result
1178
+ */
1179
+ get selectionTemplate() {
1180
+ if (isIE || !this.announceValues) { /* IE11 has significant performance complications */
1181
+ return;
1182
+ }
1183
+ return html `<div
1184
+ part="aria-selection"
1185
+ aria-live="polite"
1186
+ aria-label="${this.value
1187
+ ? this.range
1188
+ ? this.t('SELECTED_RANGE', { from: parse(this.values[0]), to: this.values[1] ? parse(this.values[1]) : null })
1189
+ : this.t('SELECTED_DATE', { value: parse(this.value), count: this.values.length })
1190
+ : this.t('SELECTED_NONE', { multiple: this.multiple, range: this.range })}"></div>`;
1191
+ }
1192
+ /**
1193
+ * A `TemplateResult` that will be used
1194
+ * to render the updated internal template.
1195
+ * @return Render template
1196
+ */
1197
+ render() {
1198
+ return html `
1199
+ ${guard([this.values, this.lang, this.range, this.multiple, this.announceValues], () => this.selectionTemplate)}
1200
+ ${guard([this.view, this.renderView, this.lang], () => this.buttonNavigationTemplate)}
1201
+ <div role="grid"
1202
+ aria-multiselectable="${this.range || this.multiple}"
1203
+ part="table"
1204
+ @tap=${this.onTableTap}>${this.viewRender}</div>
848
1205
  <div part="footer"><slot name="footer"></slot></div>
849
1206
  `;
850
1207
  }
@@ -891,15 +1248,24 @@ __decorate([
891
1248
  __decorate([
892
1249
  property({ type: Boolean, attribute: 'fill-cells' })
893
1250
  ], Calendar.prototype, "fillCells", void 0);
1251
+ __decorate([
1252
+ translate({ mode: 'directive', scope: CalendarLocaleScope })
1253
+ ], Calendar.prototype, "dateT", void 0);
1254
+ __decorate([
1255
+ translate({ mode: 'promise', scope: CalendarLocaleScope })
1256
+ ], Calendar.prototype, "dateTPromise", void 0);
894
1257
  __decorate([
895
1258
  translate({ mode: 'directive', scope: 'ef-calendar' })
896
1259
  ], Calendar.prototype, "t", void 0);
897
1260
  __decorate([
898
- translate({ mode: 'promise', scope: 'ef-calendar' })
899
- ], Calendar.prototype, "tPromise", void 0);
1261
+ state()
1262
+ ], Calendar.prototype, "renderView", null);
900
1263
  __decorate([
901
- property({ type: String })
902
- ], Calendar.prototype, "renderView", void 0);
1264
+ state()
1265
+ ], Calendar.prototype, "activeCellIndex", null);
1266
+ __decorate([
1267
+ state()
1268
+ ], Calendar.prototype, "announceValues", void 0);
903
1269
  Calendar = __decorate([
904
1270
  customElement('ef-calendar', {
905
1271
  alias: 'coral-calendar'