feat(datepicker): add theming support (#9407)

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.
This commit is contained in:
Kristiyan Kostadinov
2018-02-13 16:35:02 +01:00
committed by Jeremy Elbourn
parent 21a83bae8d
commit 038370461b
7 changed files with 174 additions and 43 deletions
+9 -1
View File
@@ -5,6 +5,13 @@
<mat-checkbox [(ngModel)]="yearView">Start in year view</mat-checkbox>
<mat-checkbox [(ngModel)]="datepickerDisabled">Disable datepicker</mat-checkbox>
<mat-checkbox [(ngModel)]="inputDisabled">Disable input</mat-checkbox>
<mat-form-field>
<mat-select [(ngModel)]="color" placeholder="Color">
<mat-option value="primary">Primary</mat-option>
<mat-option value="accent">Accent</mat-option>
<mat-option value="warn">Warn</mat-option>
</mat-select>
</mat-form-field>
</p>
<p>
<mat-form-field>
@@ -53,7 +60,8 @@
[touchUi]="touch"
[disabled]="datepickerDisabled"
[startAt]="startAt"
[startView]="yearView ? 'year' : 'month'">
[startView]="yearView ? 'year' : 'month'"
[color]="color">
</mat-datepicker>
<mat-error *ngIf="resultPickerModel.hasError('matDatepickerParse')">
"{{resultPickerModel.getError('matDatepickerParse').text}}" is not a valid date!
+4 -1
View File
@@ -8,7 +8,9 @@
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import {MatDatepickerInputEvent} from '@angular/material';
import {MatDatepickerInputEvent} from '@angular/material/datepicker';
import {ThemePalette} from '@angular/material/core';
@Component({
moduleId: module.id,
@@ -29,6 +31,7 @@ export class DatepickerDemo {
date: Date;
lastDateInput: Date | null;
lastDateChange: Date | null;
color: ThemePalette;
dateCtrl = new FormControl();
+46 -31
View File
@@ -3,24 +3,32 @@
@import '../core/typography/typography-utils';
$mat-datepicker-selected-today-box-shadow-width: 1px;
$mat-datepicker-selected-fade-amount: 0.6;
$mat-datepicker-today-fade-amount: 0.2;
$mat-calendar-body-font-size: 13px !default;
$mat-calendar-weekday-table-font-size: 11px !default;
@mixin _mat-datepicker-color($palette) {
.mat-calendar-body-selected {
background-color: mat-color($palette);
color: mat-color($palette, default-contrast);
}
.mat-calendar-body-disabled > .mat-calendar-body-selected {
background-color: fade-out(mat-color($palette), $mat-datepicker-selected-fade-amount);
}
.mat-calendar-body-today.mat-calendar-body-selected {
box-shadow: inset 0 0 0 $mat-datepicker-selected-today-box-shadow-width
mat-color($palette, default-contrast);
}
}
@mixin mat-datepicker-theme($theme) {
$primary: map-get($theme, primary);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$mat-datepicker-selected-today-box-shadow-width: 1px;
$mat-datepicker-selected-fade-amount: 0.6;
$mat-datepicker-today-fade-amount: 0.2;
.mat-datepicker-content {
background-color: mat-color($background, card);
color: mat-color($foreground, text);
}
.mat-calendar-arrow {
border-top-color: mat-color($foreground, icon);
}
@@ -59,34 +67,41 @@ $mat-calendar-weekday-table-font-size: 11px !default;
}
}
.mat-calendar-body-selected {
background-color: mat-color($primary);
color: mat-color($primary, default-contrast);
}
.mat-calendar-body-disabled > .mat-calendar-body-selected {
background-color: fade-out(mat-color($primary), $mat-datepicker-selected-fade-amount);
}
.mat-calendar-body-today {
&:not(.mat-calendar-body-selected) {
// Note: though it's not text, the border is a hint about the fact that this is today's date,
// so we use the hint color.
border-color: mat-color($foreground, hint-text);
}
&.mat-calendar-body-selected {
box-shadow: inset 0 0 0 $mat-datepicker-selected-today-box-shadow-width
mat-color($primary, default-contrast);
}
.mat-calendar-body-today:not(.mat-calendar-body-selected) {
// Note: though it's not text, the border is a hint about the fact that this is today's date,
// so we use the hint color.
border-color: mat-color($foreground, hint-text);
}
.mat-calendar-body-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected) {
border-color: fade-out(mat-color($foreground, hint-text), $mat-datepicker-today-fade-amount);
}
@include _mat-datepicker-color(map-get($theme, primary));
.mat-datepicker-content {
background-color: mat-color($background, card);
color: mat-color($foreground, text);
&.mat-accent {
@include _mat-datepicker-color(map-get($theme, accent));
}
&.mat-warn {
@include _mat-datepicker-color(map-get($theme, warn));
}
}
.mat-datepicker-toggle-active {
color: mat-color($primary);
color: mat-color(map-get($theme, primary));
&.mat-accent {
color: mat-color(map-get($theme, accent));
}
&.mat-warn {
color: mat-color(map-get($theme, warn));
}
}
}
+5
View File
@@ -337,6 +337,11 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
}
/** Returns the palette used by the input's form field, if any. */
_getThemePalette() {
return this._formField ? this._formField.color : undefined;
}
/**
* @param obj The object to check.
* @returns The given object if it is both a date instance and valid, otherwise null.
+2
View File
@@ -42,6 +42,8 @@ export class MatDatepickerToggleIcon {}
host: {
'class': 'mat-datepicker-toggle',
'[class.mat-datepicker-toggle-active]': 'datepicker && datepicker.opened',
'[class.mat-accent]': 'datepicker && datepicker.color === "accent"',
'[class.mat-warn]': 'datepicker && datepicker.color === "warn"',
},
exportAs: 'matDatepickerToggle',
encapsulation: ViewEncapsulation.None,
+65 -4
View File
@@ -14,7 +14,7 @@ import {
NativeDateModule,
SEP,
} from '@angular/material/core';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatFormFieldModule, MatFormField} from '@angular/material/form-field';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatInputModule} from '../input/index';
@@ -98,6 +98,29 @@ describe('MatDatepicker', () => {
.not.toBeNull();
});
it('should pass the datepicker theme color to the overlay', fakeAsync(() => {
testComponent.datepicker.color = 'primary';
testComponent.datepicker.open();
fixture.detectChanges();
let contentEl = document.querySelector('.mat-datepicker-content')!;
expect(contentEl.classList).toContain('mat-primary');
testComponent.datepicker.close();
fixture.detectChanges();
flush();
testComponent.datepicker.color = 'warn';
testComponent.datepicker.open();
contentEl = document.querySelector('.mat-datepicker-content')!;
fixture.detectChanges();
expect(contentEl.classList).toContain('mat-warn');
expect(contentEl.classList).not.toContain('mat-primary');
}));
it('should open datepicker if opened input is set to true', () => {
testComponent.opened = true;
fixture.detectChanges();
@@ -137,9 +160,10 @@ describe('MatDatepicker', () => {
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
});
it('close should close popup', () => {
it('close should close popup', fakeAsync(() => {
testComponent.datepicker.open();
fixture.detectChanges();
flush();
let popup = document.querySelector('.cdk-overlay-pane')!;
expect(popup).not.toBeNull();
@@ -147,9 +171,10 @@ describe('MatDatepicker', () => {
testComponent.datepicker.close();
fixture.detectChanges();
flush();
expect(parseInt(getComputedStyle(popup).height as string)).toBe(0);
});
}));
it('should close the popup when pressing ESCAPE', fakeAsync(() => {
testComponent.datepicker.open();
@@ -848,13 +873,13 @@ describe('MatDatepicker', () => {
beforeEach(fakeAsync(() => {
fixture = createComponent(FormFieldDatepicker, [MatNativeDateModule]);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(fakeAsync(() => {
testComponent.datepicker.close();
fixture.detectChanges();
flush();
}));
it('should float the placeholder when an invalid value is entered', () => {
@@ -865,6 +890,41 @@ describe('MatDatepicker', () => {
expect(fixture.debugElement.nativeElement.querySelector('mat-form-field').classList)
.toContain('mat-form-field-should-float');
});
it('should pass the form field theme color to the overlay', fakeAsync(() => {
testComponent.formField.color = 'primary';
testComponent.datepicker.open();
fixture.detectChanges();
let contentEl = document.querySelector('.mat-datepicker-content')!;
expect(contentEl.classList).toContain('mat-primary');
testComponent.datepicker.close();
fixture.detectChanges();
flush();
testComponent.formField.color = 'warn';
testComponent.datepicker.open();
contentEl = document.querySelector('.mat-datepicker-content')!;
fixture.detectChanges();
expect(contentEl.classList).toContain('mat-warn');
expect(contentEl.classList).not.toContain('mat-primary');
}));
it('should prefer the datepicker color over the form field one', fakeAsync(() => {
testComponent.datepicker.color = 'accent';
testComponent.formField.color = 'warn';
testComponent.datepicker.open();
fixture.detectChanges();
const contentEl = document.querySelector('.mat-datepicker-content')!;
expect(contentEl.classList).toContain('mat-accent');
expect(contentEl.classList).not.toContain('mat-warn');
}));
});
describe('datepicker with min and max dates and validation', () => {
@@ -1423,6 +1483,7 @@ class DatepickerWithCustomIcon {}
class FormFieldDatepicker {
@ViewChild('d') datepicker: MatDatepicker<Date>;
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
@ViewChild(MatFormField) formField: MatFormField;
}
+43 -6
View File
@@ -36,6 +36,7 @@ import {
ViewChild,
ViewContainerRef,
ViewEncapsulation,
ElementRef,
} from '@angular/core';
import {DateAdapter} from '@angular/material/core';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@@ -46,6 +47,7 @@ import {merge} from 'rxjs/observable/merge';
import {MatCalendar} from './calendar';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerInput} from './datepicker-input';
import {CanColor, mixinColor, ThemePalette} from '@angular/material/core';
/** Used to generate a unique ID for each datepicker instance. */
@@ -68,6 +70,12 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER = {
useFactory: MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER_FACTORY,
};
// Boilerplate for applying mixins to MatDatepickerContent.
/** @docs-private */
export class MatDatepickerContentBase {
constructor(public _elementRef: ElementRef) {}
}
export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBase);
/**
* 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 = {
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
inputs: ['color'],
})
export class MatDatepickerContent<D> implements AfterContentInit {
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
implements AfterContentInit, CanColor {
datepicker: MatDatepicker<D>;
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;
constructor(elementRef: ElementRef) {
super(elementRef);
}
ngAfterContentInit() {
this._calendar._focusActiveCell();
}
@@ -114,7 +128,7 @@ export class MatDatepickerContent<D> implements AfterContentInit {
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
})
export class MatDatepicker<D> implements OnDestroy {
export class MatDatepicker<D> implements OnDestroy, CanColor {
/** The date to open the calendar to initially. */
@Input()
get startAt(): D | null {
@@ -130,6 +144,9 @@ export class MatDatepicker<D> implements OnDestroy {
/** The view that the calendar should start in. */
@Input() startView: 'month' | 'year' = 'month';
/** Color palette to use on the datepicker's calendar. */
@Input() color: ThemePalette;
/**
* Whether the calendar UI is in touch mode. In touch mode the calendar opens in a dialog rather
* than a popup and elements have more padding to allow for bigger touch targets.
@@ -218,14 +235,18 @@ export class MatDatepicker<D> implements OnDestroy {
private _popupRef: OverlayRef;
/** A reference to the dialog when the calendar is opened as a dialog. */
private _dialogRef: MatDialogRef<any> | null;
private _dialogRef: MatDialogRef<MatDatepickerContent<D>> | null;
/** A portal containing the calendar for this datepicker. */
private _calendarPortal: ComponentPortal<MatDatepickerContent<D>>;
/** Reference to the component instantiated in popup mode. */
private _popupComponentRef: ComponentRef<MatDatepickerContent<D>> | null;
/** The element that was focused before the datepicker was opened. */
private _focusedElementBeforeOpen: HTMLElement | null = null;
/** Subscription to value changes in the associated input element. */
private _inputSubscription = Subscription.EMPTY;
/** The input element this datepicker is associated with. */
@@ -254,6 +275,7 @@ export class MatDatepicker<D> implements OnDestroy {
if (this._popupRef) {
this._popupRef.dispose();
this._popupComponentRef = null;
}
}
@@ -355,6 +377,7 @@ export class MatDatepicker<D> implements OnDestroy {
});
this._dialogRef.afterClosed().subscribe(() => this.close());
this._dialogRef.componentInstance.datepicker = this;
this._setColor();
}
/** Open the calendar as a popup. */
@@ -368,9 +391,9 @@ export class MatDatepicker<D> implements OnDestroy {
}
if (!this._popupRef.hasAttached()) {
let componentRef: ComponentRef<MatDatepickerContent<D>> =
this._popupRef.attach(this._calendarPortal);
componentRef.instance.datepicker = this;
this._popupComponentRef = this._popupRef.attach(this._calendarPortal);
this._popupComponentRef.instance.datepicker = this;
this._setColor();
// Update the position once the calendar has rendered.
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
@@ -427,4 +450,18 @@ export class MatDatepicker<D> implements OnDestroy {
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}
/** Passes the current theme color along to the calendar overlay. */
private _setColor(): void {
const input = this._datepickerInput;
const color = this.color || (input ? input._getThemePalette() : undefined);
if (this._popupComponentRef) {
this._popupComponentRef.instance.color = color;
}
if (this._dialogRef) {
this._dialogRef.componentInstance.color = color;
}
}
}