Skip to content

Commit 391ca68

Browse files
committed
feat(datepicker): add theming support
Adds support for theming the datepicker using the `color` attribute, as well as inheriting the theme color from the form field around a datepicker input.
1 parent 60b0625 commit 391ca68

File tree

6 files changed

+154
-36
lines changed

6 files changed

+154
-36
lines changed

src/demo-app/datepicker/datepicker-demo.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ <h2>Options</h2>
55
<mat-checkbox [(ngModel)]="yearView">Start in year view</mat-checkbox>
66
<mat-checkbox [(ngModel)]="datepickerDisabled">Disable datepicker</mat-checkbox>
77
<mat-checkbox [(ngModel)]="inputDisabled">Disable input</mat-checkbox>
8+
<mat-form-field>
9+
<mat-select [(ngModel)]="color" placeholder="Color">
10+
<mat-option value="primary">Primary</mat-option>
11+
<mat-option value="accent">Accent</mat-option>
12+
<mat-option value="warn">Warn</mat-option>
13+
</mat-select>
14+
</mat-form-field>
815
</p>
916
<p>
1017
<mat-form-field>
@@ -50,7 +57,8 @@ <h2>Result</h2>
5057
[touchUi]="touch"
5158
[disabled]="datepickerDisabled"
5259
[startAt]="startAt"
53-
[startView]="yearView ? 'year' : 'month'">
60+
[startView]="yearView ? 'year' : 'month'"
61+
[color]="color">
5462
</mat-datepicker>
5563
<mat-error *ngIf="resultPickerModel.hasError('matDatepickerParse')">
5664
"{{resultPickerModel.getError('matDatepickerParse').text}}" is not a valid date!

src/demo-app/datepicker/datepicker-demo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {ChangeDetectionStrategy, Component} from '@angular/core';
1010
import {FormControl} from '@angular/forms';
1111
import {MatDatepickerInputEvent} from '@angular/material/datepicker';
12+
import {ThemePalette} from '@angular/material/core';
1213

1314

1415
@Component({
@@ -30,6 +31,7 @@ export class DatepickerDemo {
3031
date: Date;
3132
lastDateInput: Date | null;
3233
lastDateChange: Date | null;
34+
color: ThemePalette;
3335

3436
dateFilter = (date: Date) => date.getMonth() % 2 == 1 && date.getDate() % 2 == 0;
3537

src/lib/datepicker/_datepicker-theme.scss

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,32 @@
33
@import '../core/typography/typography-utils';
44

55

6+
$mat-datepicker-selected-today-box-shadow-width: 1px;
7+
$mat-datepicker-selected-fade-amount: 0.6;
8+
$mat-datepicker-today-fade-amount: 0.2;
69
$mat-calendar-body-font-size: 13px !default;
710
$mat-calendar-weekday-table-font-size: 11px !default;
811

12+
@mixin _mat-datepicker-color($palette) {
13+
.mat-calendar-body-selected {
14+
background-color: mat-color($palette);
15+
color: mat-color($palette, default-contrast);
16+
}
17+
18+
.mat-calendar-body-disabled > .mat-calendar-body-selected {
19+
background-color: fade-out(mat-color($palette), $mat-datepicker-selected-fade-amount);
20+
}
21+
22+
.mat-calendar-body-today.mat-calendar-body-selected {
23+
box-shadow: inset 0 0 0 $mat-datepicker-selected-today-box-shadow-width
24+
mat-color($palette, default-contrast);
25+
}
26+
}
927

1028
@mixin mat-datepicker-theme($theme) {
11-
$primary: map-get($theme, primary);
1229
$foreground: map-get($theme, foreground);
1330
$background: map-get($theme, background);
1431

15-
$mat-datepicker-selected-today-box-shadow-width: 1px;
16-
$mat-datepicker-selected-fade-amount: 0.6;
17-
$mat-datepicker-today-fade-amount: 0.2;
18-
19-
.mat-datepicker-content {
20-
background-color: mat-color($background, card);
21-
color: mat-color($foreground, text);
22-
}
23-
2432
.mat-calendar-arrow {
2533
border-top-color: mat-color($foreground, icon);
2634
}
@@ -59,30 +67,29 @@ $mat-calendar-weekday-table-font-size: 11px !default;
5967
}
6068
}
6169

62-
.mat-calendar-body-selected {
63-
background-color: mat-color($primary);
64-
color: mat-color($primary, default-contrast);
70+
.mat-calendar-body-today:not(.mat-calendar-body-selected) {
71+
// Note: though it's not text, the border is a hint about the fact that this is today's date,
72+
// so we use the hint color.
73+
border-color: mat-color($foreground, hint-text);
6574
}
6675

67-
.mat-calendar-body-disabled > .mat-calendar-body-selected {
68-
background-color: fade-out(mat-color($primary), $mat-datepicker-selected-fade-amount);
76+
.mat-calendar-body-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected) {
77+
border-color: fade-out(mat-color($foreground, hint-text), $mat-datepicker-today-fade-amount);
6978
}
7079

71-
.mat-calendar-body-today {
72-
&:not(.mat-calendar-body-selected) {
73-
// Note: though it's not text, the border is a hint about the fact that this is today's date,
74-
// so we use the hint color.
75-
border-color: mat-color($foreground, hint-text);
76-
}
80+
@include _mat-datepicker-color(map-get($theme, primary));
7781

78-
&.mat-calendar-body-selected {
79-
box-shadow: inset 0 0 0 $mat-datepicker-selected-today-box-shadow-width
80-
mat-color($primary, default-contrast);
82+
.mat-datepicker-content {
83+
background-color: mat-color($background, card);
84+
color: mat-color($foreground, text);
85+
86+
&.mat-accent {
87+
@include _mat-datepicker-color(map-get($theme, accent));
8188
}
82-
}
8389

84-
.mat-calendar-body-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected) {
85-
border-color: fade-out(mat-color($foreground, hint-text), $mat-datepicker-today-fade-amount);
90+
&.mat-warn {
91+
@include _mat-datepicker-color(map-get($theme, warn));
92+
}
8693
}
8794
}
8895

src/lib/datepicker/datepicker-input.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
328328
this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
329329
}
330330

331+
/** Returns the palette used by the input's form field, if any. */
332+
_getThemePalette() {
333+
return this._formField ? this._formField.color : undefined;
334+
}
335+
331336
/**
332337
* @param obj The object to check.
333338
* @returns The given object if it is both a date instance and valid, otherwise null.

src/lib/datepicker/datepicker.spec.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
NativeDateModule,
1515
SEP,
1616
} from '@angular/material/core';
17-
import {MatFormFieldModule} from '@angular/material/form-field';
17+
import {MatFormFieldModule, MatFormField} from '@angular/material/form-field';
1818
import {By} from '@angular/platform-browser';
1919
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
2020
import {MatInputModule} from '../input/index';
@@ -98,6 +98,29 @@ describe('MatDatepicker', () => {
9898
.not.toBeNull();
9999
});
100100

101+
it('should pass the datepicker theme color to the overlay', fakeAsync(() => {
102+
testComponent.datepicker.color = 'primary';
103+
testComponent.datepicker.open();
104+
fixture.detectChanges();
105+
106+
let contentEl = document.querySelector('.mat-datepicker-content')!;
107+
108+
expect(contentEl.classList).toContain('mat-primary');
109+
110+
testComponent.datepicker.close();
111+
fixture.detectChanges();
112+
flush();
113+
114+
testComponent.datepicker.color = 'warn';
115+
testComponent.datepicker.open();
116+
117+
contentEl = document.querySelector('.mat-datepicker-content')!;
118+
fixture.detectChanges();
119+
120+
expect(contentEl.classList).toContain('mat-warn');
121+
expect(contentEl.classList).not.toContain('mat-primary');
122+
}));
123+
101124
it('should open datepicker if opened input is set to true', () => {
102125
testComponent.opened = true;
103126
fixture.detectChanges();
@@ -749,13 +772,13 @@ describe('MatDatepicker', () => {
749772
beforeEach(fakeAsync(() => {
750773
fixture = createComponent(FormFieldDatepicker, [MatNativeDateModule]);
751774
fixture.detectChanges();
752-
753775
testComponent = fixture.componentInstance;
754776
}));
755777

756778
afterEach(fakeAsync(() => {
757779
testComponent.datepicker.close();
758780
fixture.detectChanges();
781+
flush();
759782
}));
760783

761784
it('should attach popup to mat-form-field underline', () => {
@@ -773,6 +796,41 @@ describe('MatDatepicker', () => {
773796
.toContain('mat-form-field-should-float');
774797
});
775798

799+
it('should pass the form field theme color to the overlay', fakeAsync(() => {
800+
testComponent.formField.color = 'primary';
801+
testComponent.datepicker.open();
802+
fixture.detectChanges();
803+
804+
let contentEl = document.querySelector('.mat-datepicker-content')!;
805+
806+
expect(contentEl.classList).toContain('mat-primary');
807+
808+
testComponent.datepicker.close();
809+
fixture.detectChanges();
810+
flush();
811+
812+
testComponent.formField.color = 'warn';
813+
testComponent.datepicker.open();
814+
815+
contentEl = document.querySelector('.mat-datepicker-content')!;
816+
fixture.detectChanges();
817+
818+
expect(contentEl.classList).toContain('mat-warn');
819+
expect(contentEl.classList).not.toContain('mat-primary');
820+
}));
821+
822+
it('should prefer the datepicker color over the form field one', fakeAsync(() => {
823+
testComponent.datepicker.color = 'accent';
824+
testComponent.formField.color = 'warn';
825+
testComponent.datepicker.open();
826+
fixture.detectChanges();
827+
828+
const contentEl = document.querySelector('.mat-datepicker-content')!;
829+
830+
expect(contentEl.classList).toContain('mat-accent');
831+
expect(contentEl.classList).not.toContain('mat-warn');
832+
}));
833+
776834
});
777835

778836
describe('datepicker with min and max dates and validation', () => {
@@ -1301,6 +1359,7 @@ class DatepickerWithToggle {
13011359
class FormFieldDatepicker {
13021360
@ViewChild('d') datepicker: MatDatepicker<Date>;
13031361
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
1362+
@ViewChild(MatFormField) formField: MatFormField;
13041363
}
13051364

13061365

src/lib/datepicker/datepicker.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
ViewChild,
3737
ViewContainerRef,
3838
ViewEncapsulation,
39+
ElementRef,
3940
} from '@angular/core';
4041
import {DateAdapter} from '@angular/material/core';
4142
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@@ -46,6 +47,7 @@ import {merge} from 'rxjs/observable/merge';
4647
import {MatCalendar} from './calendar';
4748
import {createMissingDateImplError} from './datepicker-errors';
4849
import {MatDatepickerInput} from './datepicker-input';
50+
import {CanColor, mixinColor, ThemePalette} from '@angular/material/core';
4951

5052

5153
/** Used to generate a unique ID for each datepicker instance. */
@@ -68,6 +70,12 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER = {
6870
useFactory: MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER_FACTORY,
6971
};
7072

73+
// Boilerplate for applying mixins to MatDatepickerContent.
74+
/** @docs-private */
75+
export class MatDatepickerContentBase {
76+
constructor(public _elementRef: ElementRef) {}
77+
}
78+
export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBase);
7179

7280
/**
7381
* Component used as the content for the datepicker dialog and popup. We use this instead of using
@@ -89,12 +97,18 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER = {
8997
encapsulation: ViewEncapsulation.None,
9098
preserveWhitespaces: false,
9199
changeDetection: ChangeDetectionStrategy.OnPush,
100+
inputs: ['color'],
92101
})
93-
export class MatDatepickerContent<D> implements AfterContentInit {
102+
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
103+
implements AfterContentInit, CanColor {
94104
datepicker: MatDatepicker<D>;
95105

96106
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;
97107

108+
constructor(elementRef: ElementRef) {
109+
super(elementRef);
110+
}
111+
98112
ngAfterContentInit() {
99113
this._calendar._focusActiveCell();
100114
}
@@ -114,7 +128,7 @@ export class MatDatepickerContent<D> implements AfterContentInit {
114128
encapsulation: ViewEncapsulation.None,
115129
preserveWhitespaces: false,
116130
})
117-
export class MatDatepicker<D> implements OnDestroy {
131+
export class MatDatepicker<D> implements OnDestroy, CanColor {
118132
/** The date to open the calendar to initially. */
119133
@Input()
120134
get startAt(): D | null {
@@ -130,6 +144,9 @@ export class MatDatepicker<D> implements OnDestroy {
130144
/** The view that the calendar should start in. */
131145
@Input() startView: 'month' | 'year' = 'month';
132146

147+
/** Color palette to use on the datepicker's calendar. */
148+
@Input() color: ThemePalette;
149+
133150
/**
134151
* Whether the calendar UI is in touch mode. In touch mode the calendar opens in a dialog rather
135152
* than a popup and elements have more padding to allow for bigger touch targets.
@@ -206,14 +223,18 @@ export class MatDatepicker<D> implements OnDestroy {
206223
private _popupRef: OverlayRef;
207224

208225
/** A reference to the dialog when the calendar is opened as a dialog. */
209-
private _dialogRef: MatDialogRef<any> | null;
226+
private _dialogRef: MatDialogRef<MatDatepickerContent<D>> | null;
210227

211228
/** A portal containing the calendar for this datepicker. */
212229
private _calendarPortal: ComponentPortal<MatDatepickerContent<D>>;
213230

231+
/** Reference to the component instantiated in popup mode. */
232+
private _popupComponentRef: ComponentRef<MatDatepickerContent<D>> | null;
233+
214234
/** The element that was focused before the datepicker was opened. */
215235
private _focusedElementBeforeOpen: HTMLElement | null = null;
216236

237+
/** Subscription to value changes in the associated input element. */
217238
private _inputSubscription = Subscription.EMPTY;
218239

219240
/** The input element this datepicker is associated with. */
@@ -242,6 +263,7 @@ export class MatDatepicker<D> implements OnDestroy {
242263

243264
if (this._popupRef) {
244265
this._popupRef.dispose();
266+
this._popupComponentRef = null;
245267
}
246268
}
247269

@@ -333,6 +355,7 @@ export class MatDatepicker<D> implements OnDestroy {
333355
});
334356
this._dialogRef.afterClosed().subscribe(() => this.close());
335357
this._dialogRef.componentInstance.datepicker = this;
358+
this._setColor();
336359
}
337360

338361
/** Open the calendar as a popup. */
@@ -346,9 +369,9 @@ export class MatDatepicker<D> implements OnDestroy {
346369
}
347370

348371
if (!this._popupRef.hasAttached()) {
349-
let componentRef: ComponentRef<MatDatepickerContent<D>> =
350-
this._popupRef.attach(this._calendarPortal);
351-
componentRef.instance.datepicker = this;
372+
this._popupComponentRef = this._popupRef.attach(this._calendarPortal);
373+
this._popupComponentRef.instance.datepicker = this;
374+
this._setColor();
352375

353376
// Update the position once the calendar has rendered.
354377
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
@@ -411,4 +434,18 @@ export class MatDatepicker<D> implements OnDestroy {
411434
private _getValidDateOrNull(obj: any): D | null {
412435
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
413436
}
437+
438+
/** Passes the current theme color along to the calendar overlay. */
439+
private _setColor(): void {
440+
const input = this._datepickerInput;
441+
const color = this.color || (input ? input._getThemePalette() : undefined);
442+
443+
if (this._popupComponentRef) {
444+
this._popupComponentRef.instance.color = color;
445+
}
446+
447+
if (this._dialogRef) {
448+
this._dialogRef.componentInstance.color = color;
449+
}
450+
}
414451
}

0 commit comments

Comments
 (0)