-
+
+
+
-
- keyboard_arrow_left
-
-
- keyboard_arrow_right
-
+
+
-
-
-
-
-
-
-
+
+ {{ resourceStrings.igx_calendar_singular_single_selection}}
+
-
-
-
-
-
+
+
+
diff --git a/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.spec.ts b/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.spec.ts
index 527027e860c..02c63d766f7 100644
--- a/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.spec.ts
+++ b/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.spec.ts
@@ -1,12 +1,12 @@
import { Component, ViewChild } from '@angular/core';
-import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing';
+import { TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { UIInteractions } from '../../test-utils/ui-interactions.spec';
import { configureTestSuite } from '../../test-utils/configure-suite';
import { IgxMonthPickerComponent } from './month-picker.component';
-import { IFormattingOptions } from '../calendar';
+import { IFormattingOptions, IgxCalendarView } from '../calendar';
describe('IgxMonthPicker', () => {
configureTestSuite();
@@ -50,7 +50,8 @@ describe('IgxMonthPicker', () => {
expect(months.length).toEqual(12);
expect(current.nativeElement.textContent.trim()).toMatch('Feb');
- dom.queryAll(By.css('.igx-calendar-picker__date'))[0].nativeElement.click();
+ const yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
+ UIInteractions.simulateMouseDownEvent(yearBtn.nativeElement);
fixture.detectChanges();
const years = dom.queryAll(By.css('.igx-years-view__year'));
@@ -65,21 +66,16 @@ describe('IgxMonthPicker', () => {
fixture.detectChanges();
const dom = fixture.debugElement;
- const monthPicker = fixture.componentInstance.monthPicker;
-
- const yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
const prev = dom.query(By.css('.igx-calendar-picker__prev'));
const next = dom.query(By.css('.igx-calendar-picker__next'));
- expect(prev.nativeElement.getAttribute('aria-label')).toEqual('Previous Year ' + monthPicker.getPreviousYear());
+ expect(prev.nativeElement.getAttribute('aria-label')).toEqual('Previous Year');
expect(prev.nativeElement.getAttribute('role')).toEqual('button');
expect(prev.nativeElement.getAttribute('data-action')).toEqual('prev');
- expect(next.nativeElement.getAttribute('aria-label')).toEqual('Next Year ' + monthPicker.getNextYear());
+ expect(next.nativeElement.getAttribute('aria-label')).toEqual('Next Year');
expect(next.nativeElement.getAttribute('role')).toEqual('button');
expect(next.nativeElement.getAttribute('data-action')).toEqual('next');
-
- expect(yearBtn.nativeElement.getAttribute('aria-live')).toEqual('polite');
});
it('should properly set @Input properties and setters', () => {
@@ -150,7 +146,7 @@ describe('IgxMonthPicker', () => {
expect(yearBtn.nativeElement.textContent.trim()).toMatch('19');
expect(march.nativeElement.textContent.trim()).toMatch('March');
- yearBtn.nativeElement.click();
+ UIInteractions.simulateMouseDownEvent(yearBtn.nativeElement);
fixture.detectChanges();
const year = dom.queryAll(By.css('.igx-years-view__year'))[0];
@@ -231,15 +227,16 @@ describe('IgxMonthPicker', () => {
expect(yearBtn.nativeElement.textContent.trim()).toMatch('2019');
- prev.nativeElement.click();
+ UIInteractions.simulateMouseDownEvent(prev.nativeElement);
fixture.detectChanges();
expect(monthPicker.viewDate.getFullYear()).toEqual(2018);
expect(yearBtn.nativeElement.textContent.trim()).toMatch('2018');
- next.nativeElement.click();
- next.nativeElement.click();
- next.nativeElement.click();
+ for (let i = 0; i < 3; i++) {
+ UIInteractions.simulateMouseDownEvent(next.nativeElement);
+ }
+
fixture.detectChanges();
expect(monthPicker.viewDate.getFullYear()).toEqual(2021);
@@ -253,17 +250,17 @@ describe('IgxMonthPicker', () => {
const dom = fixture.debugElement;
const monthPicker = fixture.componentInstance.monthPicker;
const yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
- yearBtn.nativeElement.click();
+ UIInteractions.simulateMouseDownEvent(yearBtn.nativeElement);
fixture.detectChanges();
const prev = dom.query(By.css('.igx-calendar-picker__prev'));
const next = dom.query(By.css('.igx-calendar-picker__next'));
- next.nativeElement.click();
+ UIInteractions.simulateMouseDownEvent(next.nativeElement);
fixture.detectChanges();
expect(monthPicker.viewDate.getFullYear()).toEqual(2034);
- prev.nativeElement.click();
+ UIInteractions.simulateMouseDownEvent(prev.nativeElement);
fixture.detectChanges();
expect(monthPicker.viewDate.getFullYear()).toEqual(2019);
});
@@ -304,33 +301,29 @@ describe('IgxMonthPicker', () => {
expect(yearBtn.nativeElement.textContent.trim()).toMatch('2021');
});
- it('should navigate to the previous/next year via arrowLeft and arrowRight', fakeAsync(() => {
+ it('should navigate to the previous/next year via arrowLeft and arrowRight', () => {
const fixture = TestBed.createComponent(IgxMonthPickerSampleComponent);
fixture.detectChanges();
- tick();
- flush();
const monthPicker = fixture.componentInstance.monthPicker;
- const yearBtn = fixture.debugElement.query(By.css('.igx-calendar-picker__date'));
+ monthPicker.activeView = IgxCalendarView.Decade;
+ fixture.detectChanges();
- expect(yearBtn.nativeElement.textContent.trim()).toMatch('2019');
- yearBtn.nativeElement.focus();
+ const wrapper = fixture.debugElement.query(By.css('.igx-calendar__wrapper'));
+ wrapper.nativeElement.focus();
- UIInteractions.triggerKeyDownEvtUponElem('ArrowLeft', yearBtn.nativeElement);
+ UIInteractions.triggerKeyDownEvtUponElem('ArrowLeft', wrapper.nativeElement);
+ UIInteractions.triggerKeyDownEvtUponElem('Enter', wrapper.nativeElement);
fixture.detectChanges();
- tick(50);
- flush();
expect(monthPicker.viewDate.getFullYear()).toEqual(2018);
- expect(yearBtn.nativeElement.textContent.trim()).toMatch('2018');
- UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', yearBtn.nativeElement);
+ UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', wrapper.nativeElement);
+ UIInteractions.triggerKeyDownEvtUponElem('Enter', wrapper.nativeElement);
fixture.detectChanges();
- flush();
- expect(monthPicker.viewDate.getFullYear()).toEqual(2019);
- expect(yearBtn.nativeElement.textContent.trim()).toMatch('2019');
- }));
+ expect(monthPicker.viewDate.getFullYear()).toEqual(2018);
+ });
it('should not emit selected when navigating to the next year', () => {
const fixture = TestBed.createComponent(IgxMonthPickerSampleComponent);
@@ -344,8 +337,9 @@ describe('IgxMonthPicker', () => {
let yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
expect(yearBtn.nativeElement.textContent.trim()).toMatch('2019');
- UIInteractions.simulateClickEvent(next.nativeElement);
+ UIInteractions.simulateMouseDownEvent(next.nativeElement);
fixture.detectChanges();
+
UIInteractions.triggerKeyDownEvtUponElem('Enter', next.nativeElement);
fixture.detectChanges();
@@ -368,7 +362,8 @@ describe('IgxMonthPicker', () => {
UIInteractions.triggerKeyDownEvtUponElem('Enter', prev.nativeElement);
fixture.detectChanges();
- UIInteractions.simulateClickEvent(prev.nativeElement);
+
+ UIInteractions.simulateMouseDownEvent(prev.nativeElement);
fixture.detectChanges();
expect(monthPicker.selected.emit).toHaveBeenCalledTimes(0);
@@ -387,11 +382,12 @@ describe('IgxMonthPicker', () => {
let yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
expect(yearBtn.nativeElement.textContent.trim()).toMatch('2019');
- UIInteractions.simulateClickEvent(yearBtn.nativeElement);
+ UIInteractions.simulateMouseDownEvent(yearBtn.nativeElement);
fixture.detectChanges();
- const year = dom.nativeElement.querySelector('.igx-years-view__year');
- UIInteractions.simulateMouseDownEvent(year.firstChild);
+ const year = dom.query(By.css('.igx-years-view__year'));
+
+ UIInteractions.simulateMouseDownEvent(year.nativeElement);
fixture.detectChanges();
expect(monthPicker.selected.emit).toHaveBeenCalledTimes(0);
@@ -405,42 +401,31 @@ describe('IgxMonthPicker', () => {
const dom = fixture.debugElement;
const monthPicker = fixture.componentInstance.monthPicker;
-
- let yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
- yearBtn.nativeElement.focus();
-
- expect(yearBtn.nativeElement).toBe(document.activeElement);
-
- UIInteractions.triggerKeyDownEvtUponElem('Enter' , document.activeElement);
+ monthPicker.activeView = IgxCalendarView.Decade;
+ const wrapper = dom.query(By.css('.igx-calendar__wrapper'));
+ wrapper.nativeElement.focus();
fixture.detectChanges();
- let currentYear = dom.query(By.css('.igx-years-view__year--selected'));
-
- const yearsView = dom.query(By.css('igx-years-view'));
- yearsView.nativeElement.focus();
- expect(yearsView.nativeElement).toBe(document.activeElement);
- expect(currentYear.nativeElement.textContent.trim()).toMatch('2019');
+ let selectedYear = dom.query(By.css('.igx-years-view__year--selected'));
+ expect(selectedYear.nativeElement.textContent.trim()).toMatch('2019');
UIInteractions.triggerKeyDownEvtUponElem('ArrowDown' , document.activeElement);
fixture.detectChanges();
- currentYear = dom.query(By.css('.igx-years-view__year--selected'));
- expect(currentYear.nativeElement.textContent.trim()).toMatch('2022');
+ selectedYear = dom.query(By.css('.igx-years-view__year--selected'));
+ expect(selectedYear.nativeElement.textContent.trim()).toMatch('2022');
UIInteractions.triggerKeyDownEvtUponElem('ArrowUp' , document.activeElement);
UIInteractions.triggerKeyDownEvtUponElem('ArrowUp' , document.activeElement);
fixture.detectChanges();
- currentYear = dom.query(By.css('.igx-years-view__year--selected'));
- expect(currentYear.nativeElement.textContent.trim()).toMatch('2016');
+ selectedYear = dom.query(By.css('.igx-years-view__year--selected'));
+ expect(selectedYear.nativeElement.textContent.trim()).toMatch('2016');
UIInteractions.triggerKeyDownEvtUponElem('Enter' , document.activeElement);
fixture.detectChanges();
- yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
-
expect(monthPicker.viewDate.getFullYear()).toEqual(2016);
- expect(yearBtn.nativeElement.textContent.trim()).toMatch('2016');
});
it('should navigate through and select a month via KB.', () => {
@@ -527,7 +512,7 @@ describe('IgxMonthPicker', () => {
const dom = fixture.debugElement;
const yearBtn = dom.query(By.css('.igx-calendar-picker__date'));
- UIInteractions.simulateClickEvent(yearBtn.nativeElement);
+ UIInteractions.simulateMouseDownEvent(yearBtn.nativeElement);
fixture.detectChanges();
expect(monthPicker.activeViewChanged.emit).toHaveBeenCalled();
@@ -538,7 +523,7 @@ describe('IgxMonthPicker', () => {
fixture.detectChanges();
expect(monthPicker.activeViewChanged.emit).toHaveBeenCalled();
- expect(monthPicker.activeView).toEqual('month');
+ expect(monthPicker.activeView).toEqual('year');
});
});
diff --git a/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.ts b/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.ts
index fb3e5b4cde4..fdc870b9ee4 100644
--- a/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.ts
+++ b/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.ts
@@ -6,8 +6,10 @@ import {
Input,
ElementRef,
AfterViewInit,
+ OnDestroy,
+ OnInit,
} from "@angular/core";
-import { NgIf, NgStyle, NgTemplateOutlet } from "@angular/common";
+import { NgIf, NgStyle, NgTemplateOutlet, DatePipe } from "@angular/common";
import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { IgxMonthsViewComponent } from "../months-view/months-view.component";
@@ -18,6 +20,7 @@ import { IgxCalendarView } from "../calendar";
import { CalendarDay } from "../common/model";
import { IgxCalendarBaseDirective } from "../calendar-base";
import { KeyboardNavigationService } from "../calendar.services";
+import { formatToParts } from "../common/helpers";
let NEXT_ID = 0;
@Component({
@@ -39,12 +42,13 @@ let NEXT_ID = 0;
NgIf,
NgStyle,
NgTemplateOutlet,
+ DatePipe,
IgxIconComponent,
IgxMonthsViewComponent,
IgxYearsViewComponent,
],
})
-export class IgxMonthPickerComponent extends IgxCalendarBaseDirective implements AfterViewInit {
+export class IgxMonthPickerComponent extends IgxCalendarBaseDirective implements OnInit, AfterViewInit, OnDestroy {
/**
* Sets/gets the `id` of the month picker.
* If not set, the `id` will have value `"igx-month-picker-0"`.
@@ -53,6 +57,19 @@ export class IgxMonthPickerComponent extends IgxCalendarBaseDirective implements
@Input()
public id = `igx-month-picker-${NEXT_ID++}`;
+ /**
+ * @hidden
+ * @internal
+ */
+ private _activeDescendant: number;
+
+ /**
+ * @hidden
+ * @internal
+ */
+ @ViewChild("wrapper")
+ public wrapper: ElementRef;
+
/**
* The default css class applied to the component.
*
@@ -119,6 +136,30 @@ export class IgxMonthPickerComponent extends IgxCalendarBaseDirective implements
}
}
+ /**
+ * @hidden
+ * @internal
+ */
+ public onActiveViewDecadeKB(date: Date, event: KeyboardEvent, activeViewIdx: number) {
+ super.activeViewDecadeKB(event, activeViewIdx);
+
+ if (this.platform.isActivationKey(event)) {
+ this.viewDate = date;
+ this.wrapper.nativeElement.focus();
+ }
+ }
+
+ /**
+ * @hidden
+ * @internal
+ */
+ public onActiveViewDecade(event: MouseEvent, date: Date, activeViewIdx: number): void {
+ event.preventDefault();
+
+ super.activeViewDecade(activeViewIdx);
+ this.viewDate = date;
+ }
+
/**
* @hidden
*/
@@ -172,7 +213,8 @@ export class IgxMonthPickerComponent extends IgxCalendarBaseDirective implements
event.getDate(),
);
- this.activeView = IgxCalendarView.Month;
+ this.activeView = IgxCalendarView.Year;
+ this.wrapper.nativeElement.focus();
}
/**
@@ -227,9 +269,240 @@ export class IgxMonthPickerComponent extends IgxCalendarBaseDirective implements
}
}
+ @HostListener('mousedown', ['$event'])
+ protected onMouseDown(event: MouseEvent) {
+ event.stopPropagation();
+ this.wrapper.nativeElement.focus();
+ }
+
+ private _showActiveDay: boolean;
+
+ /**
+ * @hidden
+ * @internal
+ */
+ protected set showActiveDay(value: boolean) {
+ this._showActiveDay = value;
+ this.cdr.detectChanges();
+ }
+
+ protected get showActiveDay() {
+ return this._showActiveDay;
+ }
+
+ protected get activeDescendant(): number {
+ if (this.activeView === 'month') {
+ return (this.value as Date)?.getTime();
+ }
+
+ return this._activeDescendant ?? this.viewDate.getTime();
+ }
+
+ protected set activeDescendant(date: Date) {
+ this._activeDescendant = date.getTime();
+ }
+
+ public override get isDefaultView(): boolean {
+ return this.activeView === IgxCalendarView.Year;
+ }
+
+ public ngOnInit() {
+ this.activeView = IgxCalendarView.Year;
+ }
+
public ngAfterViewInit() {
+ this.keyboardNavigation
+ .attachKeyboardHandlers(this.wrapper, this)
+ .set("ArrowUp", this.onArrowUp)
+ .set("ArrowDown", this.onArrowDown)
+ .set("ArrowLeft", this.onArrowLeft)
+ .set("ArrowRight", this.onArrowRight)
+ .set("Enter", this.onEnter)
+ .set(" ", this.onEnter)
+ .set("Home", this.onHome)
+ .set("End", this.onEnd)
+ .set("PageUp", this.handlePageUp)
+ .set("PageDown", this.handlePageDown);
+
+ this.wrapper.nativeElement.addEventListener('focus', (event: FocusEvent) => this.onWrapperFocus(event));
+ this.wrapper.nativeElement.addEventListener('blur', (event: FocusEvent) => this.onWrapperBlur(event));
+
this.activeView$.subscribe((view) => {
this.activeViewChanged.emit(view);
+
+ this.viewDateChanged.emit({
+ previousValue: this.previousViewDate,
+ currentValue: this.viewDate
+ });
});
}
+
+ private onWrapperFocus(event: FocusEvent) {
+ event.stopPropagation();
+ this.showActiveDay = true;
+ }
+
+ private onWrapperBlur(event: FocusEvent) {
+ event.stopPropagation();
+
+ this.showActiveDay = false;
+ this._onTouchedCallback();
+ }
+
+ private handlePageUpDown(event: KeyboardEvent, delta: number) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.isDefaultView && event.shiftKey) {
+ this.viewDate = CalendarDay.from(this.viewDate).add('year', delta).native;
+ this.cdr.detectChanges();
+ } else {
+ delta > 0 ? this.nextPage() : this.previousPage();
+ }
+ }
+
+ private handlePageUp(event: KeyboardEvent) {
+ this.handlePageUpDown(event, -1);
+ }
+
+ private handlePageDown(event: KeyboardEvent) {
+ this.handlePageUpDown(event, 1);
+ }
+
+ private onArrowUp(event: KeyboardEvent) {
+ if (this.isDefaultView) {
+ this.monthsView.onKeydownArrowUp(event);
+ }
+
+ if (this.isDecadeView) {
+ this.dacadeView.onKeydownArrowUp(event);
+ }
+ }
+
+ private onArrowDown(event: KeyboardEvent) {
+ if (this.isDefaultView) {
+ this.monthsView.onKeydownArrowDown(event);
+ }
+
+ if (this.isDecadeView) {
+ this.dacadeView.onKeydownArrowDown(event);
+ }
+ }
+
+ private onArrowLeft(event: KeyboardEvent) {
+ if (this.isDefaultView) {
+ this.monthsView.onKeydownArrowLeft(event);
+ }
+
+ if (this.isDecadeView) {
+ this.dacadeView.onKeydownArrowLeft(event);
+ }
+ }
+
+ private onArrowRight(event: KeyboardEvent) {
+ if (this.isDefaultView) {
+ this.monthsView.onKeydownArrowRight(event);
+ }
+
+ if (this.isDecadeView) {
+ this.dacadeView.onKeydownArrowRight(event);
+ }
+ }
+
+ private onEnter(event: KeyboardEvent) {
+ event.stopPropagation();
+
+ if (this.isDefaultView) {
+ this.monthsView.onKeydownEnter(event);
+ }
+
+ if (this.isDecadeView) {
+ this.dacadeView.onKeydownEnter(event);
+ }
+ }
+
+ private onHome(event: KeyboardEvent) {
+ event.stopPropagation();
+ if (this.isDefaultView) {
+ this.monthsView.onKeydownHome(event);
+ }
+
+ if (this.isDecadeView) {
+ this.dacadeView.onKeydownHome(event);
+ }
+ }
+
+ private onEnd(event: KeyboardEvent) {
+ event.stopPropagation();
+ if (this.isDefaultView) {
+ this.monthsView.onKeydownEnd(event);
+ }
+
+ if (this.isDecadeView) {
+ this.dacadeView.onKeydownEnd(event);
+ }
+ }
+
+ /**
+ * @hidden
+ * @internal
+ */
+ public ngOnDestroy(): void {
+ this.keyboardNavigation.detachKeyboardHandlers();
+ this.wrapper?.nativeElement.removeEventListener('focus', this.onWrapperFocus);
+ this.wrapper?.nativeElement.removeEventListener('blur', this.onWrapperBlur);
+ }
+
+ /**
+ * @hidden
+ * @internal
+ */
+ public getPrevYearDate(date: Date): Date {
+ return CalendarDay.from(date).add('year', -1).native;
+ }
+
+ /**
+ * @hidden
+ * @internal
+ */
+ public getNextYearDate(date: Date): Date {
+ return CalendarDay.from(date).add('year', 1).native;
+ }
+
+ /**
+ * Getter for the context object inside the calendar templates.
+ *
+ * @hidden
+ * @internal
+ */
+ public getContext(i: number) {
+ const date = CalendarDay.from(this.viewDate).add('month', i).native;
+ return this.generateContext(date, i);
+ }
+
+ /**
+ * Helper method building and returning the context object inside the calendar templates.
+ *
+ * @hidden
+ * @internal
+ */
+ private generateContext(value: Date | Date[], i?: number) {
+ const construct = (date: Date, index: number) => ({
+ index: index,
+ date,
+ ...formatToParts(date, this.locale, this.formatOptions, [
+ "era",
+ "year",
+ "month",
+ "day",
+ "weekday",
+ ]),
+ });
+
+ const formatObject = Array.isArray(value)
+ ? value.map((date, index) => construct(date, index))
+ : construct(value, i);
+
+ return { $implicit: formatObject };
+ }
}