diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html index f259e3dae6b5..efa35dcf189c 100644 --- a/src/demo-app/datepicker/datepicker-demo.html +++ b/src/demo-app/datepicker/datepicker-demo.html @@ -5,6 +5,13 @@

Options

Start in year view Disable datepicker Disable input + + + Primary + Accent + Warn + +

@@ -53,7 +60,8 @@

Result

[touchUi]="touch" [disabled]="datepickerDisabled" [startAt]="startAt" - [startView]="yearView ? 'year' : 'month'"> + [startView]="yearView ? 'year' : 'month'" + [color]="color"> "{{resultPickerModel.getError('matDatepickerParse').text}}" is not a valid date! diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts index ae4c40758458..61d6a5146883 100644 --- a/src/demo-app/datepicker/datepicker-demo.ts +++ b/src/demo-app/datepicker/datepicker-demo.ts @@ -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(); diff --git a/src/lib/datepicker/_datepicker-theme.scss b/src/lib/datepicker/_datepicker-theme.scss index c448e4b83fab..120cd711d205 100644 --- a/src/lib/datepicker/_datepicker-theme.scss +++ b/src/lib/datepicker/_datepicker-theme.scss @@ -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-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-selected { - background-color: fade-out(mat-color($primary), $mat-datepicker-selected-fade-amount); + .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); } - .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); - } + @include _mat-datepicker-color(map-get($theme, primary)); + + .mat-datepicker-content { + background-color: mat-color($background, card); + color: mat-color($foreground, text); - &.mat-calendar-body-selected { - box-shadow: inset 0 0 0 $mat-datepicker-selected-today-box-shadow-width - mat-color($primary, default-contrast); + &.mat-accent { + @include _mat-datepicker-color(map-get($theme, accent)); } - } - .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); + &.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)); + } } } diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index fad809625c6a..98ee4ca8db8c 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -337,6 +337,11 @@ export class MatDatepickerInput 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. diff --git a/src/lib/datepicker/datepicker-toggle.ts b/src/lib/datepicker/datepicker-toggle.ts index 52a3415af47c..ac50c391f232 100644 --- a/src/lib/datepicker/datepicker-toggle.ts +++ b/src/lib/datepicker/datepicker-toggle.ts @@ -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, diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 3cb8e7af1d27..701160870064 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -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; @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput; + @ViewChild(MatFormField) formField: MatFormField; } diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 780311d32a10..7eb04517d190 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -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 implements AfterContentInit { +export class MatDatepickerContent extends _MatDatepickerContentMixinBase + implements AfterContentInit, CanColor { datepicker: MatDatepicker; @ViewChild(MatCalendar) _calendar: MatCalendar; + constructor(elementRef: ElementRef) { + super(elementRef); + } + ngAfterContentInit() { this._calendar._focusActiveCell(); } @@ -114,7 +128,7 @@ export class MatDatepickerContent implements AfterContentInit { encapsulation: ViewEncapsulation.None, preserveWhitespaces: false, }) -export class MatDatepicker implements OnDestroy { +export class MatDatepicker implements OnDestroy, CanColor { /** The date to open the calendar to initially. */ @Input() get startAt(): D | null { @@ -130,6 +144,9 @@ export class MatDatepicker 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 implements OnDestroy { private _popupRef: OverlayRef; /** A reference to the dialog when the calendar is opened as a dialog. */ - private _dialogRef: MatDialogRef | null; + private _dialogRef: MatDialogRef> | null; /** A portal containing the calendar for this datepicker. */ private _calendarPortal: ComponentPortal>; + /** Reference to the component instantiated in popup mode. */ + private _popupComponentRef: ComponentRef> | 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 implements OnDestroy { if (this._popupRef) { this._popupRef.dispose(); + this._popupComponentRef = null; } } @@ -355,6 +377,7 @@ export class MatDatepicker 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 implements OnDestroy { } if (!this._popupRef.hasAttached()) { - let componentRef: ComponentRef> = - 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 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; + } + } }