Skip to content

Commit f28a294

Browse files
jysoommalerba
authored andcommitted
feat(datepicker): align multi-year-view based on minDate and maxDate (#16018)
* feat(datepicker): align multi-year-view based on minDate and maxDate If minDate is set (and maxDate not set), let first page begin with minYear. If maxDate is set, let last page end with maxYear, and disable year(s) < minYear (if any) in first page. Fixes #10646 * add tests for feat(datepicker): align multi-year-view based on minDate and maxDate * refactor feat(datepicker): align multi-year-view based on minDate and maxDate * omit helper func from public api * fixup, resolve conflict refactor with private methods feat(datepicker): align multi-year-view based on minDate and maxDate * share func feat(datepicker): align multi-year-view based on minDate and maxDate * fix formatting nit
1 parent 27a08cc commit f28a294

File tree

5 files changed

+357
-26
lines changed

5 files changed

+357
-26
lines changed

src/material/datepicker/calendar-header.spec.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('MatCalendarHeader', () => {
1919
declarations: [
2020
// Test components.
2121
StandardCalendar,
22+
CalendarWithMinMaxDate,
2223
],
2324
providers: [
2425
MatDatepickerIntl,
@@ -148,7 +149,174 @@ describe('MatCalendarHeader', () => {
148149
expect(calendarInstance.activeDate).toEqual(new Date(2016, DEC, 31));
149150
expect(testComponent.selected).toBeFalsy('no date should be selected yet');
150151
});
152+
});
153+
154+
describe('calendar with minDate only', () => {
155+
let fixture: ComponentFixture<CalendarWithMinMaxDate>;
156+
let testComponent: CalendarWithMinMaxDate;
157+
let calendarElement: HTMLElement;
158+
let periodButton: HTMLButtonElement;
159+
let prevButton: HTMLButtonElement;
160+
let nextButton: HTMLButtonElement;
161+
let calendarInstance: MatCalendar<Date>;
162+
163+
beforeEach(() => {
164+
fixture = TestBed.createComponent(CalendarWithMinMaxDate);
165+
fixture.detectChanges();
166+
167+
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
168+
calendarElement = calendarDebugElement.nativeElement;
169+
periodButton =
170+
calendarElement.querySelector('.mat-calendar-period-button') as HTMLButtonElement;
171+
prevButton =
172+
calendarElement.querySelector('.mat-calendar-previous-button') as HTMLButtonElement;
173+
nextButton =
174+
calendarElement.querySelector('.mat-calendar-next-button') as HTMLButtonElement;
175+
calendarInstance = calendarDebugElement.componentInstance;
176+
testComponent = fixture.componentInstance;
177+
});
178+
179+
it('should start the first page with minDate', () => {
180+
testComponent.minDate = new Date(2010, JAN, 1);
181+
periodButton.click();
182+
fixture.detectChanges();
183+
184+
expect(calendarInstance.currentView).toBe('multi-year');
185+
expect(periodButton.innerText.trim()).toEqual('2010 \u2013 2033');
186+
});
187+
188+
189+
it('should disable the page before the one showing minDate', () => {
190+
testComponent.minDate = new Date(2010, JAN, 1);
191+
periodButton.click();
192+
fixture.detectChanges();
193+
194+
expect(calendarInstance.currentView).toBe('multi-year');
195+
expect(prevButton.disabled).toBe(true);
196+
});
151197

198+
it('should enable the page after the one showing minDate', () => {
199+
testComponent.minDate = new Date(2010, JAN, 1);
200+
periodButton.click();
201+
fixture.detectChanges();
202+
203+
expect(calendarInstance.currentView).toBe('multi-year');
204+
expect(nextButton.disabled).toBe(false);
205+
});
206+
});
207+
208+
describe('calendar with maxDate only', () => {
209+
let fixture: ComponentFixture<CalendarWithMinMaxDate>;
210+
let testComponent: CalendarWithMinMaxDate;
211+
let calendarElement: HTMLElement;
212+
let periodButton: HTMLButtonElement;
213+
let prevButton: HTMLButtonElement;
214+
let nextButton: HTMLButtonElement;
215+
let calendarInstance: MatCalendar<Date>;
216+
217+
beforeEach(() => {
218+
fixture = TestBed.createComponent(CalendarWithMinMaxDate);
219+
fixture.detectChanges();
220+
221+
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
222+
calendarElement = calendarDebugElement.nativeElement;
223+
periodButton =
224+
calendarElement.querySelector('.mat-calendar-period-button') as HTMLButtonElement;
225+
prevButton =
226+
calendarElement.querySelector('.mat-calendar-previous-button') as HTMLButtonElement;
227+
nextButton =
228+
calendarElement.querySelector('.mat-calendar-next-button') as HTMLButtonElement;
229+
calendarInstance = calendarDebugElement.componentInstance;
230+
testComponent = fixture.componentInstance;
231+
});
232+
233+
it('should end the last page with maxDate', () => {
234+
testComponent.maxDate = new Date(2020, JAN, 1);
235+
periodButton.click();
236+
fixture.detectChanges();
237+
238+
expect(calendarInstance.currentView).toBe('multi-year');
239+
expect(periodButton.innerText.trim()).toEqual('1997 \u2013 2020');
240+
});
241+
242+
it('should disable the page after the one showing maxDate', () => {
243+
testComponent.maxDate = new Date(2020, JAN, 1);
244+
periodButton.click();
245+
fixture.detectChanges();
246+
247+
expect(calendarInstance.currentView).toBe('multi-year');
248+
expect(nextButton.disabled).toBe(true);
249+
});
250+
251+
it('should enable the page before the one showing maxDate', () => {
252+
testComponent.maxDate = new Date(2020, JAN, 1);
253+
periodButton.click();
254+
fixture.detectChanges();
255+
256+
expect(calendarInstance.currentView).toBe('multi-year');
257+
expect(prevButton.disabled).toBe(false);
258+
});
259+
});
260+
261+
describe('calendar with minDate and maxDate', () => {
262+
let fixture: ComponentFixture<CalendarWithMinMaxDate>;
263+
let testComponent: CalendarWithMinMaxDate;
264+
let calendarElement: HTMLElement;
265+
let periodButton: HTMLButtonElement;
266+
let prevButton: HTMLButtonElement;
267+
let nextButton: HTMLButtonElement;
268+
let calendarInstance: MatCalendar<Date>;
269+
270+
beforeEach(() => {
271+
fixture = TestBed.createComponent(CalendarWithMinMaxDate);
272+
fixture.detectChanges();
273+
274+
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
275+
calendarElement = calendarDebugElement.nativeElement;
276+
periodButton =
277+
calendarElement.querySelector('.mat-calendar-period-button') as HTMLButtonElement;
278+
prevButton =
279+
calendarElement.querySelector('.mat-calendar-previous-button') as HTMLButtonElement;
280+
nextButton =
281+
calendarElement.querySelector('.mat-calendar-next-button') as HTMLButtonElement;
282+
calendarInstance = calendarDebugElement.componentInstance;
283+
testComponent = fixture.componentInstance;
284+
});
285+
286+
it('should end the last page with maxDate', () => {
287+
testComponent.minDate = new Date(1993, JAN, 1);
288+
testComponent.maxDate = new Date(2020, JAN, 1);
289+
periodButton.click();
290+
fixture.detectChanges();
291+
292+
expect(calendarInstance.currentView).toBe('multi-year');
293+
expect(periodButton.innerText.trim()).toEqual('1997 \u2013 2020');
294+
});
295+
296+
it('should disable the page after the one showing maxDate', () => {
297+
testComponent.minDate = new Date(1993, JAN, 1);
298+
testComponent.maxDate = new Date(2020, JAN, 1);
299+
periodButton.click();
300+
fixture.detectChanges();
301+
302+
expect(calendarInstance.currentView).toBe('multi-year');
303+
expect(nextButton.disabled).toBe(true);
304+
});
305+
306+
it('should disable the page before the one showing minDate', () => {
307+
testComponent.minDate = new Date(1993, JAN, 1);
308+
testComponent.maxDate = new Date(2020, JAN, 1);
309+
periodButton.click();
310+
fixture.detectChanges();
311+
312+
expect(calendarInstance.currentView).toBe('multi-year');
313+
314+
prevButton.click();
315+
fixture.detectChanges();
316+
317+
expect(calendarInstance.activeDate).toEqual(new Date(2018 - yearsPerPage, JAN, 1));
318+
expect(prevButton.disabled).toBe(true);
319+
});
152320
});
153321
});
154322

@@ -167,3 +335,18 @@ class StandardCalendar {
167335
selectedMonth: Date;
168336
startDate = new Date(2017, JAN, 31);
169337
}
338+
339+
@Component({
340+
template: `
341+
<mat-calendar
342+
[startAt]="startAt"
343+
[minDate]="minDate"
344+
[maxDate]="maxDate">
345+
</mat-calendar>
346+
`
347+
})
348+
class CalendarWithMinMaxDate {
349+
startAt = new Date(2018, JAN, 1);
350+
minDate: Date | null;
351+
maxDate: Date | null;
352+
}

src/material/datepicker/calendar.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,17 @@ import {
2727
} from '@angular/core';
2828
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
2929
import {Subject, Subscription} from 'rxjs';
30+
import {MatCalendarCellCssClasses} from './calendar-body';
3031
import {createMissingDateImplError} from './datepicker-errors';
3132
import {MatDatepickerIntl} from './datepicker-intl';
3233
import {MatMonthView} from './month-view';
33-
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
34+
import {
35+
getActiveOffset,
36+
isSameMultiYearView,
37+
MatMultiYearView,
38+
yearsPerPage
39+
} from './multi-year-view';
3440
import {MatYearView} from './year-view';
35-
import {MatCalendarCellCssClasses} from './calendar-body';
3641

3742
/**
3843
* Possible views for the calendar.
@@ -69,12 +74,15 @@ export class MatCalendarHeader<D> {
6974
if (this.calendar.currentView == 'year') {
7075
return this._dateAdapter.getYearName(this.calendar.activeDate);
7176
}
77+
78+
// The offset from the active year to the "slot" for the starting year is the
79+
// *actual* first rendered year in the multi-year view, and the last year is
80+
// just yearsPerPage - 1 away.
7281
const activeYear = this._dateAdapter.getYear(this.calendar.activeDate);
73-
const firstYearInView = this._dateAdapter.getYearName(
74-
this._dateAdapter.createDate(activeYear - activeYear % 24, 0, 1));
75-
const lastYearInView = this._dateAdapter.getYearName(
76-
this._dateAdapter.createDate(activeYear + yearsPerPage - 1 - activeYear % 24, 0, 1));
77-
return `${firstYearInView} \u2013 ${lastYearInView}`;
82+
const minYearOfPage = activeYear - getActiveOffset(
83+
this._dateAdapter, this.calendar.activeDate, this.calendar.minDate, this.calendar.maxDate);
84+
const maxYearOfPage = minYearOfPage + yearsPerPage - 1;
85+
return `${minYearOfPage} \u2013 ${maxYearOfPage}`;
7886
}
7987

8088
get periodButtonLabel(): string {
@@ -149,8 +157,8 @@ export class MatCalendarHeader<D> {
149157
return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
150158
}
151159
// Otherwise we are in 'multi-year' view.
152-
return Math.floor(this._dateAdapter.getYear(date1) / yearsPerPage) ==
153-
Math.floor(this._dateAdapter.getYear(date2) / yearsPerPage);
160+
return isSameMultiYearView(
161+
this._dateAdapter, date1, date2, this.calendar.minDate, this.calendar.maxDate);
154162
}
155163
}
156164

src/material/datepicker/multi-year-view.spec.ts

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('MatMultiYearView', () => {
3333
// Test components.
3434
StandardMultiYearView,
3535
MultiYearViewWithDateFilter,
36+
MultiYearViewWithMinMaxDate,
3637
],
3738
providers: [
3839
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
@@ -247,8 +248,87 @@ describe('MatMultiYearView', () => {
247248
expect(cells[1].classList).toContain('mat-calendar-body-disabled');
248249
});
249250
});
250-
});
251251

252+
describe('multi year view with minDate only', () => {
253+
let fixture: ComponentFixture<MultiYearViewWithMinMaxDate>;
254+
let testComponent: MultiYearViewWithMinMaxDate;
255+
let multiYearViewNativeElement: Element;
256+
257+
beforeEach(() => {
258+
fixture = TestBed.createComponent(MultiYearViewWithMinMaxDate);
259+
260+
const multiYearViewDebugElement = fixture.debugElement.query(By.directive(MatMultiYearView));
261+
multiYearViewNativeElement = multiYearViewDebugElement.nativeElement;
262+
testComponent = fixture.componentInstance;
263+
});
264+
265+
it('should begin first page with minDate', () => {
266+
testComponent.minDate = new Date(2014, JAN, 1);
267+
testComponent.maxDate = null;
268+
fixture.detectChanges();
269+
270+
const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
271+
expect((cells[0] as HTMLElement).innerText.trim()).toBe('2014');
272+
});
273+
});
274+
275+
describe('multi year view with maxDate only', () => {
276+
let fixture: ComponentFixture<MultiYearViewWithMinMaxDate>;
277+
let testComponent: MultiYearViewWithMinMaxDate;
278+
let multiYearViewNativeElement: Element;
279+
280+
beforeEach(() => {
281+
fixture = TestBed.createComponent(MultiYearViewWithMinMaxDate);
282+
283+
const multiYearViewDebugElement = fixture.debugElement.query(By.directive(MatMultiYearView));
284+
multiYearViewNativeElement = multiYearViewDebugElement.nativeElement;
285+
testComponent = fixture.componentInstance;
286+
});
287+
288+
it('should end last page with maxDate', () => {
289+
testComponent.minDate = null;
290+
testComponent.maxDate = new Date(2020, JAN, 1);
291+
fixture.detectChanges();
292+
293+
const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
294+
expect((cells[cells.length - 1] as HTMLElement).innerText.trim()).toBe('2020');
295+
});
296+
});
297+
298+
describe('multi year view with minDate and maxDate', () => {
299+
let fixture: ComponentFixture<MultiYearViewWithMinMaxDate>;
300+
let testComponent: MultiYearViewWithMinMaxDate;
301+
let multiYearViewNativeElement: Element;
302+
303+
beforeEach(() => {
304+
fixture = TestBed.createComponent(MultiYearViewWithMinMaxDate);
305+
306+
const multiYearViewDebugElement = fixture.debugElement.query(By.directive(MatMultiYearView));
307+
multiYearViewNativeElement = multiYearViewDebugElement.nativeElement;
308+
testComponent = fixture.componentInstance;
309+
});
310+
311+
it('should end last page with maxDate', () => {
312+
testComponent.minDate = new Date(2006, JAN, 1);
313+
testComponent.maxDate = new Date (2020, JAN, 1);
314+
fixture.detectChanges();
315+
316+
const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
317+
expect((cells[cells.length - 1] as HTMLElement).innerText.trim()).toBe('2020');
318+
});
319+
320+
it('should disable dates before minDate', () => {
321+
testComponent.minDate = new Date(2006, JAN, 1);
322+
testComponent.maxDate = new Date (2020, JAN, 1);
323+
fixture.detectChanges();
324+
325+
const cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
326+
expect(cells[0].classList).toContain('mat-calendar-body-disabled');
327+
expect(cells[8].classList).toContain('mat-calendar-body-disabled');
328+
expect(cells[9].classList).not.toContain('mat-calendar-body-disabled');
329+
});
330+
});
331+
});
252332

253333
@Component({
254334
template: `
@@ -265,7 +345,8 @@ class StandardMultiYearView {
265345

266346
@Component({
267347
template: `
268-
<mat-multi-year-view [activeDate]="activeDate" [dateFilter]="dateFilter"></mat-multi-year-view>
348+
<mat-multi-year-view [(activeDate)]="activeDate" [dateFilter]="dateFilter">
349+
</mat-multi-year-view>
269350
`
270351
})
271352
class MultiYearViewWithDateFilter {
@@ -274,3 +355,15 @@ class MultiYearViewWithDateFilter {
274355
return date.getFullYear() !== 2017;
275356
}
276357
}
358+
359+
@Component({
360+
template: `
361+
<mat-multi-year-view [(activeDate)]="activeDate" [minDate]="minDate" [maxDate]="maxDate">
362+
</mat-multi-year-view>
363+
`
364+
})
365+
class MultiYearViewWithMinMaxDate {
366+
activeDate = new Date(2019, JAN, 1);
367+
minDate: Date | null;
368+
maxDate: Date | null;
369+
}

0 commit comments

Comments
 (0)