mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
21a83bae8d
commit
038370461b
@@ -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!
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user