diff --git a/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.html b/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.html index 62cc5c43f3d..215591474d4 100644 --- a/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.html +++ b/projects/igniteui-angular/src/lib/calendar/month-picker/month-picker.component.html @@ -1,128 +1,159 @@ - + + + + + + + + + + + + +
+ +
+
- - + + +
+ +
+
+ + + + {{ formattedYear(obj.date) }} - {{ formattedYear(viewDate) }} + role="button" + [attr.aria-label]="(obj.date | date: 'yyyy') + ', ' + resourceStrings.igx_calendar_select_year" + (keydown)="onActiveViewDecadeKB(obj.date, $event, obj.index)" + (mousedown)="onActiveViewDecade($event, obj.date, obj.index)" + class="igx-calendar-picker__date"> + {{ formattedYear(obj.date) }} - - - {{ getDecadeRange().start }} -  -  - {{ getDecadeRange().end }} + + + {{ getDecadeRange().start }} - {{ getDecadeRange().end }} - - - - + +
-
- +
+ +
-
- 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 }; + } }