Skip to content

Commit 2683b3d

Browse files
authored
fix(datepicker): support dateClass on year and multi-year views (#20256)
We have a `dateClass` input that allows consumers to set classes to particular date cells, however currently we only invoke it inside the month view. These changes pass it along to the year and multi-year views too and provide the name of the view that invoked it so that cases like the first date of a month and a month cell can be distinguished. Fixes #20017.
1 parent e3699dc commit 2683b3d

File tree

12 files changed

+145
-28
lines changed

12 files changed

+145
-28
lines changed
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, ViewEncapsulation} from '@angular/core';
2-
import {MatCalendarCellCssClasses} from '@angular/material/datepicker';
2+
import {MatCalendarCellClassFunction} from '@angular/material/datepicker';
33

44
/** @title Datepicker with custom date classes */
55
@Component({
@@ -9,10 +9,15 @@ import {MatCalendarCellCssClasses} from '@angular/material/datepicker';
99
encapsulation: ViewEncapsulation.None,
1010
})
1111
export class DatepickerDateClassExample {
12-
dateClass = (d: Date): MatCalendarCellCssClasses => {
13-
const date = d.getDate();
12+
dateClass: MatCalendarCellClassFunction<Date> = (cellDate, view) => {
13+
// Only highligh dates inside the month view.
14+
if (view === 'month') {
15+
const date = cellDate.getDate();
1416

15-
// Highlight the 1st and 20th day of each month.
16-
return (date === 1 || date === 20) ? 'example-custom-date-class' : '';
17+
// Highlight the 1st and 20th day of each month.
18+
return (date === 1 || date === 20) ? 'example-custom-date-class' : '';
19+
}
20+
21+
return '';
1722
}
1823
}

src/material/datepicker/calendar-body.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import {
2121
} from '@angular/core';
2222
import {take} from 'rxjs/operators';
2323

24-
/**
25-
* Extra CSS classes that can be associated with a calendar cell.
26-
*/
24+
/** Extra CSS classes that can be associated with a calendar cell. */
2725
export type MatCalendarCellCssClasses = string | string[] | Set<string> | {[key: string]: any};
2826

27+
/** Function that can generate the extra classes that should be added to a calendar cell. */
28+
export type MatCalendarCellClassFunction<D> =
29+
(date: D, view: 'month' | 'year' | 'multi-year') => MatCalendarCellCssClasses;
30+
2931
/**
3032
* An internal class that represents the data corresponding to a single calendar cell.
3133
* @docs-private

src/material/datepicker/calendar.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
[dateFilter]="dateFilter"
2222
[maxDate]="maxDate"
2323
[minDate]="minDate"
24+
[dateClass]="dateClass"
2425
(monthSelected)="_monthSelectedInYearView($event)"
2526
(selectedChange)="_goToDateInView($event, 'month')">
2627
</mat-year-view>
@@ -32,6 +33,7 @@
3233
[dateFilter]="dateFilter"
3334
[maxDate]="maxDate"
3435
[minDate]="minDate"
36+
[dateClass]="dateClass"
3537
(yearSelected)="_yearSelectedInMultiYearView($event)"
3638
(selectedChange)="_goToDateInView($event, 'year')">
3739
</mat-multi-year-view>

src/material/datepicker/calendar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
MatDateFormats,
3232
} from '@angular/material/core';
3333
import {Subject, Subscription} from 'rxjs';
34-
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
34+
import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body';
3535
import {createMissingDateImplError} from './datepicker-errors';
3636
import {MatDatepickerIntl} from './datepicker-intl';
3737
import {MatMonthView} from './month-view';
@@ -245,7 +245,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
245245
@Input() dateFilter: (date: D) => boolean;
246246

247247
/** Function that can be used to add custom CSS classes to dates. */
248-
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
248+
@Input() dateClass: MatCalendarCellClassFunction<D>;
249249

250250
/** Start of the comparison range. */
251251
@Input() comparisonStart: D | null;

src/material/datepicker/datepicker-base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {filter, take} from 'rxjs/operators';
5353
import {MatCalendar} from './calendar';
5454
import {matDatepickerAnimations} from './datepicker-animations';
5555
import {createMissingDateImplError} from './datepicker-errors';
56-
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
56+
import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body';
5757
import {DateFilterFn} from './datepicker-input-base';
5858
import {
5959
ExtractDateTypeFromSelection,
@@ -322,7 +322,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
322322
@Input() panelClass: string | string[];
323323

324324
/** Function that can be used to add custom CSS classes to dates. */
325-
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
325+
@Input() dateClass: MatCalendarCellClassFunction<D>;
326326

327327
/** Emits when the datepicker has been opened. */
328328
@Output('opened') openedStream: EventEmitter<void> = new EventEmitter<void>();

src/material/datepicker/month-view.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,11 @@ describe('MatMonthView', () => {
441441
describe('month view with custom date classes', () => {
442442
let fixture: ComponentFixture<MonthViewWithDateClass>;
443443
let monthViewNativeElement: Element;
444+
let dateClassSpy: jasmine.Spy;
444445

445446
beforeEach(() => {
446447
fixture = TestBed.createComponent(MonthViewWithDateClass);
448+
dateClassSpy = spyOn(fixture.componentInstance, 'dateClass').and.callThrough();
447449
fixture.detectChanges();
448450

449451
let monthViewDebugElement = fixture.debugElement.query(By.directive(MatMonthView))!;
@@ -455,6 +457,10 @@ describe('MatMonthView', () => {
455457
expect(cells[0].classList).not.toContain('even');
456458
expect(cells[1].classList).toContain('even');
457459
});
460+
461+
it('should call dateClass with the correct view name', () => {
462+
expect(dateClassSpy).toHaveBeenCalledWith(jasmine.any(Date), 'month');
463+
});
458464
});
459465

460466
});

src/material/datepicker/month-view.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ import {Directionality} from '@angular/cdk/bidi';
4040
import {
4141
MatCalendarBody,
4242
MatCalendarCell,
43-
MatCalendarCellCssClasses,
4443
MatCalendarUserEvent,
44+
MatCalendarCellClassFunction,
4545
} from './calendar-body';
4646
import {createMissingDateImplError} from './datepicker-errors';
4747
import {Subscription} from 'rxjs';
@@ -122,7 +122,7 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
122122
@Input() dateFilter: (date: D) => boolean;
123123

124124
/** Function that can be used to add custom CSS classes to dates. */
125-
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
125+
@Input() dateClass: MatCalendarCellClassFunction<D>;
126126

127127
/** Start of the comparison range. */
128128
@Input() comparisonStart: D | null;
@@ -380,7 +380,7 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
380380
this._dateAdapter.getMonth(this.activeDate), i + 1);
381381
const enabled = this._shouldEnableDate(date);
382382
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
383-
const cellClasses = this.dateClass ? this.dateClass(date) : undefined;
383+
const cellClasses = this.dateClass ? this.dateClass(date, 'month') : undefined;
384384

385385
this._weeks[this._weeks.length - 1].push(new MatCalendarCell<D>(i + 1, dateNames[i],
386386
ariaLabel, enabled, cellClasses, this._getCellCompareValue(date)!, date));

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('MatMultiYearView', () => {
3434
StandardMultiYearView,
3535
MultiYearViewWithDateFilter,
3636
MultiYearViewWithMinMaxDate,
37+
MultiYearViewWithDateClass,
3738
],
3839
providers: [
3940
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
@@ -343,6 +344,32 @@ describe('MatMultiYearView', () => {
343344
expect(cells[9].classList).not.toContain('mat-calendar-body-disabled');
344345
});
345346
});
347+
348+
describe('multi-year view with custom date classes', () => {
349+
let fixture: ComponentFixture<MultiYearViewWithDateClass>;
350+
let multiYearViewNativeElement: Element;
351+
let dateClassSpy: jasmine.Spy;
352+
353+
beforeEach(() => {
354+
fixture = TestBed.createComponent(MultiYearViewWithDateClass);
355+
dateClassSpy = spyOn(fixture.componentInstance, 'dateClass').and.callThrough();
356+
fixture.detectChanges();
357+
358+
let multiYearViewDebugElement = fixture.debugElement.query(By.directive(MatMultiYearView))!;
359+
multiYearViewNativeElement = multiYearViewDebugElement.nativeElement;
360+
});
361+
362+
it('should be able to add a custom class to some dates', () => {
363+
let cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
364+
expect(cells[0].classList).toContain('even');
365+
expect(cells[1].classList).not.toContain('even');
366+
});
367+
368+
it('should call dateClass with the correct view name', () => {
369+
expect(dateClassSpy).toHaveBeenCalledWith(jasmine.any(Date), 'multi-year');
370+
});
371+
});
372+
346373
});
347374

348375
@Component({
@@ -387,3 +414,16 @@ class MultiYearViewWithMinMaxDate {
387414
minDate: Date | null;
388415
maxDate: Date | null;
389416
}
417+
418+
419+
@Component({
420+
template: `
421+
<mat-multi-year-view [activeDate]="activeDate" [dateClass]="dateClass"></mat-multi-year-view>
422+
`
423+
})
424+
class MultiYearViewWithDateClass {
425+
activeDate = new Date(2017, JAN, 1);
426+
dateClass(date: Date) {
427+
return date.getFullYear() % 2 == 0 ? 'even' : undefined;
428+
}
429+
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ import {
3333
} from '@angular/core';
3434
import {DateAdapter} from '@angular/material/core';
3535
import {Directionality} from '@angular/cdk/bidi';
36-
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';
36+
import {
37+
MatCalendarBody,
38+
MatCalendarCell,
39+
MatCalendarUserEvent,
40+
MatCalendarCellClassFunction,
41+
} from './calendar-body';
3742
import {createMissingDateImplError} from './datepicker-errors';
3843
import {Subscription} from 'rxjs';
3944
import {startWith} from 'rxjs/operators';
@@ -109,6 +114,9 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
109114
/** A function used to filter which dates are selectable. */
110115
@Input() dateFilter: (date: D) => boolean;
111116

117+
/** Function that can be used to add custom CSS classes to date cells. */
118+
@Input() dateClass: MatCalendarCellClassFunction<D>;
119+
112120
/** Emits when a new year is selected. */
113121
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
114122

@@ -251,8 +259,11 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
251259

252260
/** Creates an MatCalendarCell for the given year. */
253261
private _createCellForYear(year: number) {
254-
let yearName = this._dateAdapter.getYearName(this._dateAdapter.createDate(year, 0, 1));
255-
return new MatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year));
262+
const date = this._dateAdapter.createDate(year, 0, 1);
263+
const yearName = this._dateAdapter.getYearName(date);
264+
const cellClasses = this.dateClass ? this.dateClass(date, 'multi-year') : undefined;
265+
266+
return new MatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year), cellClasses);
256267
}
257268

258269
/** Whether the given year is enabled. */

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('MatYearView', () => {
3333
// Test components.
3434
StandardYearView,
3535
YearViewWithDateFilter,
36+
YearViewWithDateClass,
3637
],
3738
providers: [
3839
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
@@ -329,6 +330,32 @@ describe('MatYearView', () => {
329330
});
330331

331332
});
333+
334+
describe('year view with custom date classes', () => {
335+
let fixture: ComponentFixture<YearViewWithDateClass>;
336+
let yearViewNativeElement: Element;
337+
let dateClassSpy: jasmine.Spy;
338+
339+
beforeEach(() => {
340+
fixture = TestBed.createComponent(YearViewWithDateClass);
341+
dateClassSpy = spyOn(fixture.componentInstance, 'dateClass').and.callThrough();
342+
fixture.detectChanges();
343+
344+
let yearViewDebugElement = fixture.debugElement.query(By.directive(MatYearView))!;
345+
yearViewNativeElement = yearViewDebugElement.nativeElement;
346+
});
347+
348+
it('should be able to add a custom class to some month cells', () => {
349+
let cells = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
350+
expect(cells[0].classList).toContain('even');
351+
expect(cells[1].classList).not.toContain('even');
352+
});
353+
354+
it('should call dateClass with the correct view name', () => {
355+
expect(dateClassSpy).toHaveBeenCalledWith(jasmine.any(Date), 'year');
356+
});
357+
});
358+
332359
});
333360

334361

@@ -368,3 +395,14 @@ class YearViewWithDateFilter {
368395
return true;
369396
}
370397
}
398+
399+
400+
@Component({
401+
template: `<mat-year-view [activeDate]="activeDate" [dateClass]="dateClass"></mat-year-view>`
402+
})
403+
class YearViewWithDateClass {
404+
activeDate = new Date(2017, JAN, 1);
405+
dateClass(date: Date) {
406+
return date.getMonth() % 2 == 0 ? 'even' : undefined;
407+
}
408+
}

src/material/datepicker/year-view.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ import {
3434
} from '@angular/core';
3535
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
3636
import {Directionality} from '@angular/cdk/bidi';
37-
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';
37+
import {
38+
MatCalendarBody,
39+
MatCalendarCell,
40+
MatCalendarUserEvent,
41+
MatCalendarCellClassFunction,
42+
} from './calendar-body';
3843
import {createMissingDateImplError} from './datepicker-errors';
3944
import {Subscription} from 'rxjs';
4045
import {startWith} from 'rxjs/operators';
@@ -103,6 +108,9 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
103108
/** A function used to filter which dates are selectable. */
104109
@Input() dateFilter: (date: D) => boolean;
105110

111+
/** Function that can be used to add custom CSS classes to date cells. */
112+
@Input() dateClass: MatCalendarCellClassFunction<D>;
113+
106114
/** Emits when a new month is selected. */
107115
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
108116

@@ -254,11 +262,12 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
254262

255263
/** Creates an MatCalendarCell for the given month. */
256264
private _createCellForMonth(month: number, monthName: string) {
257-
let ariaLabel = this._dateAdapter.format(
258-
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1),
259-
this._dateFormats.display.monthYearA11yLabel);
260-
return new MatCalendarCell(
261-
month, monthName.toLocaleUpperCase(), ariaLabel, this._shouldEnableMonth(month));
265+
const date = this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1);
266+
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.monthYearA11yLabel);
267+
const cellClasses = this.dateClass ? this.dateClass(date, 'year') : undefined;
268+
269+
return new MatCalendarCell(month, monthName.toLocaleUpperCase(), ariaLabel,
270+
this._shouldEnableMonth(month), cellClasses);
262271
}
263272

264273
/** Whether the given month is enabled. */

0 commit comments

Comments
 (0)