mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(datepicker): merge datepicker branch into master (#4404)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
<md-checkbox [(ngModel)]="touch">Use touch UI</md-checkbox>
|
||||
<md-checkbox [(ngModel)]="filterOdd">Filter odd months and dates</md-checkbox>
|
||||
<md-checkbox [(ngModel)]="yearView">Start in year view</md-checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="minDatePicker" [(ngModel)]="minDate" placeholder="Min date">
|
||||
<button mdSuffix [mdDatepickerToggle]="minDatePicker"></button>
|
||||
</md-input-container>
|
||||
<md-datepicker #minDatePicker [touchUi]="touch"></md-datepicker>
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="maxDatePicker" [(ngModel)]="maxDate" placeholder="Max date">
|
||||
<button mdSuffix [mdDatepickerToggle]="maxDatePicker"></button>
|
||||
</md-input-container>
|
||||
<md-datepicker #maxDatePicker [touchUi]="touch"></md-datepicker>
|
||||
</p>
|
||||
<p>
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="startAtPicker" [(ngModel)]="startAt" placeholder="Start at date">
|
||||
<button mdSuffix [mdDatepickerToggle]="startAtPicker"></button>
|
||||
</md-input-container>
|
||||
<md-datepicker #startAtPicker [touchUi]="touch"></md-datepicker>
|
||||
</p>
|
||||
|
||||
<h2>Result</h2>
|
||||
|
||||
<p>
|
||||
<button [mdDatepickerToggle]="resultPicker"></button>
|
||||
<md-input-container>
|
||||
<input mdInput
|
||||
#resultPickerModel="ngModel"
|
||||
[mdDatepicker]="resultPicker"
|
||||
[(ngModel)]="date"
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
|
||||
placeholder="Pick a date">
|
||||
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMin')">Too early!</md-error>
|
||||
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMax')">Too late!</md-error>
|
||||
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerFilter')">Date unavailable!</md-error>
|
||||
</md-input-container>
|
||||
<md-datepicker
|
||||
#resultPicker
|
||||
[touchUi]="touch"
|
||||
[startAt]="startAt"
|
||||
[startView]="yearView ? 'year' : 'month'">
|
||||
</md-datepicker>
|
||||
</p>
|
||||
<p>
|
||||
<input [mdDatepicker]="resultPicker2"
|
||||
[(ngModel)]="date"
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
|
||||
placeholder="Pick a date">
|
||||
<button [mdDatepickerToggle]="resultPicker2"></button>
|
||||
<md-datepicker
|
||||
#resultPicker2
|
||||
[touchUi]="touch"
|
||||
[startAt]="startAt"
|
||||
[startView]="yearView ? 'year' : 'month'">
|
||||
</md-datepicker>
|
||||
</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
md-calendar {
|
||||
width: 300px;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'datepicker-demo',
|
||||
templateUrl: 'datepicker-demo.html',
|
||||
styleUrls: ['datepicker-demo.css'],
|
||||
})
|
||||
export class DatepickerDemo {
|
||||
touch: boolean;
|
||||
filterOdd: boolean;
|
||||
yearView: boolean;
|
||||
minDate: Date;
|
||||
maxDate: Date;
|
||||
startAt: Date;
|
||||
date: Date;
|
||||
dateFilter = (date: Date) => date.getMonth() % 2 == 1 && date.getDate() % 2 == 0;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {NgModule, ApplicationRef} from '@angular/core';
|
||||
import {ApplicationRef, NgModule} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {HttpModule} from '@angular/http';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
@@ -6,14 +6,15 @@ import {RouterModule} from '@angular/router';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {DemoApp, Home} from './demo-app/demo-app';
|
||||
import {
|
||||
MaterialModule,
|
||||
OverlayContainer,
|
||||
FullscreenOverlayContainer,
|
||||
MaterialModule,
|
||||
MdNativeDateModule,
|
||||
MdSelectionModule,
|
||||
OverlayContainer
|
||||
} from '@angular/material';
|
||||
import {DEMO_APP_ROUTES} from './demo-app/routes';
|
||||
import {ProgressBarDemo} from './progress-bar/progress-bar-demo';
|
||||
import {JazzDialog, ContentElementDialog, DialogDemo, IFrameDialog} from './dialog/dialog-demo';
|
||||
import {ContentElementDialog, DialogDemo, IFrameDialog, JazzDialog} from './dialog/dialog-demo';
|
||||
import {RippleDemo} from './ripple/ripple-demo';
|
||||
import {IconDemo} from './icon/icon-demo';
|
||||
import {GesturesDemo} from './gestures/gestures-demo';
|
||||
@@ -27,22 +28,23 @@ import {ListDemo} from './list/list-demo';
|
||||
import {BaselineDemo} from './baseline/baseline-demo';
|
||||
import {GridListDemo} from './grid-list/grid-list-demo';
|
||||
import {LiveAnnouncerDemo} from './live-announcer/live-announcer-demo';
|
||||
import {OverlayDemo, SpagettiPanel, RotiniPanel} from './overlay/overlay-demo';
|
||||
import {OverlayDemo, RotiniPanel, SpagettiPanel} from './overlay/overlay-demo';
|
||||
import {SlideToggleDemo} from './slide-toggle/slide-toggle-demo';
|
||||
import {ToolbarDemo} from './toolbar/toolbar-demo';
|
||||
import {ButtonDemo} from './button/button-demo';
|
||||
import {MdCheckboxDemoNestedChecklist, CheckboxDemo} from './checkbox/checkbox-demo';
|
||||
import {CheckboxDemo, MdCheckboxDemoNestedChecklist} from './checkbox/checkbox-demo';
|
||||
import {SelectDemo} from './select/select-demo';
|
||||
import {SliderDemo} from './slider/slider-demo';
|
||||
import {SidenavDemo} from './sidenav/sidenav-demo';
|
||||
import {SnackBarDemo} from './snack-bar/snack-bar-demo';
|
||||
import {PortalDemo, ScienceJoke} from './portal/portal-demo';
|
||||
import {MenuDemo} from './menu/menu-demo';
|
||||
import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tabs/tabs-demo';
|
||||
import {FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo} from './tabs/tabs-demo';
|
||||
import {PlatformDemo} from './platform/platform-demo';
|
||||
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
|
||||
import {InputDemo} from './input/input-demo';
|
||||
import {StyleDemo} from './style/style-demo';
|
||||
import {DatepickerDemo} from './datepicker/datepicker-demo';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -54,6 +56,7 @@ import {StyleDemo} from './style/style-demo';
|
||||
ReactiveFormsModule,
|
||||
RouterModule.forRoot(DEMO_APP_ROUTES),
|
||||
MaterialModule,
|
||||
MdNativeDateModule,
|
||||
MdSelectionModule,
|
||||
],
|
||||
declarations: [
|
||||
@@ -64,6 +67,7 @@ import {StyleDemo} from './style/style-demo';
|
||||
CardDemo,
|
||||
ChipsDemo,
|
||||
CheckboxDemo,
|
||||
DatepickerDemo,
|
||||
DemoApp,
|
||||
DialogDemo,
|
||||
GesturesDemo,
|
||||
|
||||
@@ -30,6 +30,7 @@ export class DemoApp {
|
||||
{name: 'Card', route: 'card'},
|
||||
{name: 'Chips', route: 'chips'},
|
||||
{name: 'Checkbox', route: 'checkbox'},
|
||||
{name: 'Datepicker', route: 'datepicker'},
|
||||
{name: 'Dialog', route: 'dialog'},
|
||||
{name: 'Gestures', route: 'gestures'},
|
||||
{name: 'Grid List', route: 'grid-list'},
|
||||
|
||||
@@ -32,6 +32,7 @@ import {PlatformDemo} from '../platform/platform-demo';
|
||||
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
|
||||
import {InputDemo} from '../input/input-demo';
|
||||
import {StyleDemo} from '../style/style-demo';
|
||||
import {DatepickerDemo} from '../datepicker/datepicker-demo';
|
||||
|
||||
export const DEMO_APP_ROUTES: Routes = [
|
||||
{path: '', component: Home},
|
||||
@@ -39,6 +40,7 @@ export const DEMO_APP_ROUTES: Routes = [
|
||||
{path: 'button', component: ButtonDemo},
|
||||
{path: 'card', component: CardDemo},
|
||||
{path: 'chips', component: ChipsDemo},
|
||||
{path: 'datepicker', component: DatepickerDemo},
|
||||
{path: 'radio', component: RadioDemo},
|
||||
{path: 'select', component: SelectDemo},
|
||||
{path: 'sidenav', component: SidenavDemo},
|
||||
|
||||
@@ -103,6 +103,8 @@ export {CompatibilityModule, NoConflictStyleCompatibilityMode} from './compatibi
|
||||
// Common material module
|
||||
export {MdCommonModule} from './common-behaviors/common-module';
|
||||
|
||||
// Datetime
|
||||
export * from './datetime/index';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/** Adapts type `D` to be usable as a date by cdk-based components that work with dates. */
|
||||
export abstract class DateAdapter<D> {
|
||||
/** The locale to use for all dates. */
|
||||
protected locale: any;
|
||||
|
||||
/**
|
||||
* Gets the year component of the given date.
|
||||
* @param date The date to extract the year from.
|
||||
* @returns The year component.
|
||||
*/
|
||||
abstract getYear(date: D): number;
|
||||
|
||||
/**
|
||||
* Gets the month component of the given date.
|
||||
* @param date The date to extract the month from.
|
||||
* @returns The month component (0-indexed, 0 = January).
|
||||
*/
|
||||
abstract getMonth(date: D): number;
|
||||
|
||||
/**
|
||||
* Gets the date of the month component of the given date.
|
||||
* @param date The date to extract the date of the month from.
|
||||
* @returns The month component (1-indexed, 1 = first of month).
|
||||
*/
|
||||
abstract getDate(date: D): number;
|
||||
|
||||
/**
|
||||
* Gets the day of the week component of the given date.
|
||||
* @param date The date to extract the day of the week from.
|
||||
* @returns The month component (0-indexed, 0 = Sunday).
|
||||
*/
|
||||
abstract getDayOfWeek(date: D): number;
|
||||
|
||||
/**
|
||||
* Gets a list of names for the months.
|
||||
* @param style The naming style (e.g. long = 'January', short = 'Jan', narrow = 'J').
|
||||
* @returns An ordered list of all month names, starting with January.
|
||||
*/
|
||||
abstract getMonthNames(style: 'long' | 'short' | 'narrow'): string[];
|
||||
|
||||
/**
|
||||
* Gets a list of names for the dates of the month.
|
||||
* @returns An ordered list of all date of the month names, starting with '1'.
|
||||
*/
|
||||
abstract getDateNames(): string[];
|
||||
|
||||
/**
|
||||
* Gets a list of names for the days of the week.
|
||||
* @param style The naming style (e.g. long = 'Sunday', short = 'Sun', narrow = 'S').
|
||||
* @returns An ordered list of all weekday names, starting with Sunday.
|
||||
*/
|
||||
abstract getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[];
|
||||
|
||||
/**
|
||||
* Gets the name for the year of the given date.
|
||||
* @param date The date to get the year name for.
|
||||
* @returns The name of the given year (e.g. '2017').
|
||||
*/
|
||||
abstract getYearName(date: D): string;
|
||||
|
||||
/**
|
||||
* Gets the first day of the week.
|
||||
* @returns The first day of the week (0-indexed, 0 = Sunday).
|
||||
*/
|
||||
abstract getFirstDayOfWeek(): number;
|
||||
|
||||
/**
|
||||
* Gets the number of days in the month of the given date.
|
||||
* @param date The date whose month should be checked.
|
||||
* @returns The number of days in the month of the given date.
|
||||
*/
|
||||
abstract getNumDaysInMonth(date: D): number;
|
||||
|
||||
/**
|
||||
* Clones the given date.
|
||||
* @param date The date to clone
|
||||
* @returns A new date equal to the given date.
|
||||
*/
|
||||
abstract clone(date: D): D;
|
||||
|
||||
/**
|
||||
* Creates a date with the given year, month, and date. Does not allow over/under-flow of the
|
||||
* month and date.
|
||||
* @param year The full year of the date. (e.g. 89 means the year 89, not the year 1989).
|
||||
* @param month The month of the date (0-indexed, 0 = January). Must be an integer 0 - 11.
|
||||
* @param date The date of month of the date. Must be an integer 1 - length of the given month.
|
||||
* @returns The new date, or null if invalid.
|
||||
*/
|
||||
abstract createDate(year: number, month: number, date: number): D;
|
||||
|
||||
/**
|
||||
* Gets today's date.
|
||||
* @returns Today's date.
|
||||
*/
|
||||
abstract today(): D;
|
||||
|
||||
/**
|
||||
* Parses a date from a value.
|
||||
* @param value The value to parse.
|
||||
* @param parseFormat The expected format of the value being parsed
|
||||
* (type is implementation-dependent).
|
||||
* @returns The parsed date, or null if date could not be parsed.
|
||||
*/
|
||||
abstract parse(value: any, parseFormat: any): D | null;
|
||||
|
||||
/**
|
||||
* Formats a date as a string.
|
||||
* @param date The value to parse.
|
||||
* @param displayFormat The format to use to display the date as a string.
|
||||
* @returns The parsed date, or null if date could not be parsed.
|
||||
*/
|
||||
abstract format(date: D, displayFormat: any): string;
|
||||
|
||||
/**
|
||||
* Adds the given number of years to the date. Years are counted as if flipping 12 pages on the
|
||||
* calendar for each year and then finding the closest date in the new month. For example when
|
||||
* adding 1 year to Feb 29, 2016, the resulting date will be Feb 28, 2017.
|
||||
* @param date The date to add years to.
|
||||
* @param years The number of years to add (may be negative).
|
||||
* @returns A new date equal to the given one with the specified number of years added.
|
||||
*/
|
||||
abstract addCalendarYears(date: D, years: number): D;
|
||||
|
||||
/**
|
||||
* Adds the given number of months to the date. Months are counted as if flipping a page on the
|
||||
* calendar for each month and then finding the closest date in the new month. For example when
|
||||
* adding 1 month to Jan 31, 2017, the resulting date will be Feb 28, 2017.
|
||||
* @param date The date to add months to.
|
||||
* @param months The number of months to add (may be negative).
|
||||
* @returns A new date equal to the given one with the specified number of months added.
|
||||
*/
|
||||
abstract addCalendarMonths(date: D, months: number): D;
|
||||
|
||||
/**
|
||||
* Adds the given number of days to the date. Days are counted as if moving one cell on the
|
||||
* calendar for each day.
|
||||
* @param date The date to add days to.
|
||||
* @param days The number of days to add (may be negative).
|
||||
* @returns A new date equal to the given one with the specified number of days added.
|
||||
*/
|
||||
abstract addCalendarDays(date: D, days: number): D;
|
||||
|
||||
/**
|
||||
* Gets the RFC 3339 compatible date string (https://tools.ietf.org/html/rfc3339) for the given
|
||||
* date.
|
||||
* @param date The date to get the ISO date string for.
|
||||
* @returns The ISO date string date string.
|
||||
*/
|
||||
abstract getISODateString(date: D): string;
|
||||
|
||||
/**
|
||||
* Sets the locale used for all dates.
|
||||
* @param locale The new locale.
|
||||
*/
|
||||
setLocale(locale: any) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two dates.
|
||||
* @param first The first date to compare.
|
||||
* @param second The second date to compare.
|
||||
* @returns 0 if the dates are equal, a number less than 0 if the first date is earlier,
|
||||
* a number greater than 0 if the first date is later.
|
||||
*/
|
||||
compareDate(first: D, second: D): number {
|
||||
return this.getYear(first) - this.getYear(second) ||
|
||||
this.getMonth(first) - this.getMonth(second) ||
|
||||
this.getDate(first) - this.getDate(second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two dates are equal.
|
||||
* @param first The first date to check.
|
||||
* @param second The second date to check.
|
||||
* @returns {boolean} Whether the two dates are equal.
|
||||
* Null dates are considered equal to other null dates.
|
||||
*/
|
||||
sameDate(first: D | null, second: D | null): boolean {
|
||||
return first && second ? !this.compareDate(first, second) : first == second;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp the given date between min and max dates.
|
||||
* @param date The date to clamp.
|
||||
* @param min The minimum value to allow. If null or omitted no min is enforced.
|
||||
* @param max The maximum value to allow. If null or omitted no max is enforced.
|
||||
* @returns `min` if `date` is less than `min`, `max` if date is greater than `max`,
|
||||
* otherwise `date`.
|
||||
*/
|
||||
clampDate(date: D, min?: D | null, max?: D | null): D {
|
||||
if (min && this.compareDate(date, min) < 0) {
|
||||
return min;
|
||||
}
|
||||
if (max && this.compareDate(date, max) > 0) {
|
||||
return max;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {InjectionToken} from '@angular/core';
|
||||
|
||||
|
||||
export type MdDateFormats = {
|
||||
parse: {
|
||||
dateInput: any
|
||||
},
|
||||
display: {
|
||||
dateInput: any,
|
||||
monthYearLabel: any,
|
||||
dateA11yLabel: any,
|
||||
monthYearA11yLabel: any,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const MD_DATE_FORMATS = new InjectionToken<MdDateFormats>('md-date-formats');
|
||||
@@ -0,0 +1,22 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {DateAdapter} from './date-adapter';
|
||||
import {NativeDateAdapter} from './native-date-adapter';
|
||||
import {MD_DATE_FORMATS} from './date-formats';
|
||||
import {MD_NATIVE_DATE_FORMATS} from './native-date-formats';
|
||||
|
||||
|
||||
export * from './date-adapter';
|
||||
export * from './native-date-adapter';
|
||||
|
||||
|
||||
@NgModule({
|
||||
providers: [{provide: DateAdapter, useClass: NativeDateAdapter}],
|
||||
})
|
||||
export class NativeDateModule {}
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [NativeDateModule],
|
||||
providers: [{provide: MD_DATE_FORMATS, useValue: MD_NATIVE_DATE_FORMATS}],
|
||||
})
|
||||
export class MdNativeDateModule {}
|
||||
@@ -0,0 +1,299 @@
|
||||
import {NativeDateAdapter} from './native-date-adapter';
|
||||
import {Platform} from '../platform/index';
|
||||
|
||||
|
||||
const SUPPORTS_INTL = typeof Intl != 'undefined';
|
||||
|
||||
|
||||
// When constructing a Date, the month is zero-based. This can be confusing, since people are
|
||||
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
|
||||
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
|
||||
NOV = 10, DEC = 11;
|
||||
|
||||
|
||||
describe('NativeDateAdapter', () => {
|
||||
let adapter;
|
||||
let platform;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new NativeDateAdapter();
|
||||
platform = new Platform();
|
||||
});
|
||||
|
||||
it('should get year', () => {
|
||||
expect(adapter.getYear(new Date(2017, JAN, 1))).toBe(2017);
|
||||
});
|
||||
|
||||
it('should get month', () => {
|
||||
expect(adapter.getMonth(new Date(2017, JAN, 1))).toBe(0);
|
||||
});
|
||||
|
||||
it('should get date', () => {
|
||||
expect(adapter.getDate(new Date(2017, JAN, 1))).toBe(1);
|
||||
});
|
||||
|
||||
it('should get day of week', () => {
|
||||
expect(adapter.getDayOfWeek(new Date(2017, JAN, 1))).toBe(0);
|
||||
});
|
||||
|
||||
it('should get long month names', () => {
|
||||
expect(adapter.getMonthNames('long')).toEqual([
|
||||
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
|
||||
'October', 'November', 'December'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get long month names', () => {
|
||||
expect(adapter.getMonthNames('short')).toEqual([
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get narrow month names', () => {
|
||||
// Edge & IE use same value for short and narrow.
|
||||
if (platform.EDGE || platform.TRIDENT) {
|
||||
expect(adapter.getMonthNames('narrow')).toEqual([
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
]);
|
||||
} else {
|
||||
expect(adapter.getMonthNames('narrow')).toEqual([
|
||||
'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should get month names in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
if (SUPPORTS_INTL) {
|
||||
expect(adapter.getMonthNames('long')).toEqual([
|
||||
'1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'
|
||||
]);
|
||||
} else {
|
||||
expect(adapter.getMonthNames('long')).toEqual([
|
||||
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
|
||||
'October', 'November', 'December'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should get date names', () => {
|
||||
expect(adapter.getDateNames()).toEqual([
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17',
|
||||
'18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get date names in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
if (SUPPORTS_INTL) {
|
||||
expect(adapter.getDateNames()).toEqual([
|
||||
'1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日', '11日', '12日',
|
||||
'13日', '14日', '15日', '16日', '17日', '18日', '19日', '20日', '21日', '22日', '23日', '24日',
|
||||
'25日', '26日', '27日', '28日', '29日', '30日', '31日'
|
||||
]);
|
||||
} else {
|
||||
expect(adapter.getDateNames()).toEqual([
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17',
|
||||
'18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should get long day of week names', () => {
|
||||
expect(adapter.getDayOfWeekNames('long')).toEqual([
|
||||
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get short day of week names', () => {
|
||||
expect(adapter.getDayOfWeekNames('short')).toEqual([
|
||||
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get narrow day of week names', () => {
|
||||
// Edge & IE use two-letter narrow days.
|
||||
if (platform.EDGE || platform.TRIDENT) {
|
||||
expect(adapter.getDayOfWeekNames('narrow')).toEqual([
|
||||
'Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'
|
||||
]);
|
||||
} else {
|
||||
expect(adapter.getDayOfWeekNames('narrow')).toEqual([
|
||||
'S', 'M', 'T', 'W', 'T', 'F', 'S'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should get day of week names in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
if (SUPPORTS_INTL) {
|
||||
expect(adapter.getDayOfWeekNames('long')).toEqual([
|
||||
'日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'
|
||||
]);
|
||||
} else {
|
||||
expect(adapter.getDayOfWeekNames('long')).toEqual([
|
||||
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should get year name', () => {
|
||||
expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017');
|
||||
});
|
||||
|
||||
it('should get year name in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
if (SUPPORTS_INTL) {
|
||||
expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017年');
|
||||
} else {
|
||||
expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017');
|
||||
}
|
||||
});
|
||||
|
||||
it('should get first day of week', () => {
|
||||
expect(adapter.getFirstDayOfWeek()).toBe(0);
|
||||
});
|
||||
|
||||
it('should create Date', () => {
|
||||
expect(adapter.createDate(2017, JAN, 1)).toEqual(new Date(2017, JAN, 1));
|
||||
});
|
||||
|
||||
it('should not create Date with month over/under-flow', () => {
|
||||
expect(adapter.createDate(2017, DEC + 1, 1)).toBeNull();
|
||||
expect(adapter.createDate(2017, JAN - 1, 1)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not create Date with date over/under-flow', () => {
|
||||
expect(adapter.createDate(2017, JAN, 32)).toBeNull();
|
||||
expect(adapter.createDate(2017, JAN, 0)).toBeNull();
|
||||
});
|
||||
|
||||
it('should create Date with low year number', () => {
|
||||
expect(adapter.createDate(-1, JAN, 1).getFullYear()).toBe(-1);
|
||||
expect(adapter.createDate(0, JAN, 1).getFullYear()).toBe(0);
|
||||
expect(adapter.createDate(50, JAN, 1).getFullYear()).toBe(50);
|
||||
expect(adapter.createDate(99, JAN, 1).getFullYear()).toBe(99);
|
||||
expect(adapter.createDate(100, JAN, 1).getFullYear()).toBe(100);
|
||||
});
|
||||
|
||||
it("should get today's date", () => {
|
||||
expect(adapter.sameDate(adapter.today(), new Date()))
|
||||
.toBe(true, "should be equal to today's date");
|
||||
});
|
||||
|
||||
it('should parse string', () => {
|
||||
expect(adapter.parse('1/1/2017')).toEqual(new Date(2017, JAN, 1));
|
||||
});
|
||||
|
||||
it('should parse number', () => {
|
||||
let timestamp = new Date().getTime();
|
||||
expect(adapter.parse(timestamp)).toEqual(new Date(timestamp));
|
||||
});
|
||||
|
||||
it ('should parse Date', () => {
|
||||
let date = new Date(2017, JAN, 1);
|
||||
expect(adapter.parse(date)).toEqual(date);
|
||||
expect(adapter.parse(date)).not.toBe(date);
|
||||
});
|
||||
|
||||
it('should parse invalid value as null', () => {
|
||||
expect(adapter.parse('hello')).toBeNull();
|
||||
});
|
||||
|
||||
it('should format', () => {
|
||||
if (SUPPORTS_INTL) {
|
||||
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('1/1/2017');
|
||||
} else {
|
||||
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('Sun Jan 01 2017');
|
||||
}
|
||||
});
|
||||
|
||||
it('should format with custom format', () => {
|
||||
if (SUPPORTS_INTL) {
|
||||
expect(adapter.format(new Date(2017, JAN, 1), {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})).toEqual('January 1, 2017');
|
||||
} else {
|
||||
expect(adapter.format(new Date(2017, JAN, 1), {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})).toEqual('Sun Jan 01 2017');
|
||||
}
|
||||
});
|
||||
|
||||
it('should format with a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
if (SUPPORTS_INTL) {
|
||||
// Edge & IE use a different format in Japanese.
|
||||
if (platform.EDGE || platform.TRIDENT) {
|
||||
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('2017年1月1日');
|
||||
} else {
|
||||
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('2017/1/1');
|
||||
}
|
||||
} else {
|
||||
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('Sun Jan 01 2017');
|
||||
}
|
||||
});
|
||||
|
||||
it('should add years', () => {
|
||||
expect(adapter.addCalendarYears(new Date(2017, JAN, 1), 1)).toEqual(new Date(2018, JAN, 1));
|
||||
expect(adapter.addCalendarYears(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, JAN, 1));
|
||||
});
|
||||
|
||||
it('should respect leap years when adding years', () => {
|
||||
expect(adapter.addCalendarYears(new Date(2016, FEB, 29), 1)).toEqual(new Date(2017, FEB, 28));
|
||||
expect(adapter.addCalendarYears(new Date(2016, FEB, 29), -1)).toEqual(new Date(2015, FEB, 28));
|
||||
});
|
||||
|
||||
it('should add months', () => {
|
||||
expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, FEB, 1));
|
||||
expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 1));
|
||||
});
|
||||
|
||||
it('should respect month length differences when adding months', () => {
|
||||
expect(adapter.addCalendarMonths(new Date(2017, JAN, 31), 1)).toEqual(new Date(2017, FEB, 28));
|
||||
expect(adapter.addCalendarMonths(new Date(2017, MAR, 31), -1)).toEqual(new Date(2017, FEB, 28));
|
||||
});
|
||||
|
||||
it('should add days', () => {
|
||||
expect(adapter.addCalendarDays(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, JAN, 2));
|
||||
expect(adapter.addCalendarDays(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 31));
|
||||
});
|
||||
|
||||
it('should clone', () => {
|
||||
let date = new Date(2017, JAN, 1);
|
||||
expect(adapter.clone(date)).toEqual(date);
|
||||
expect(adapter.clone(date)).not.toBe(date);
|
||||
});
|
||||
|
||||
it('should compare dates', () => {
|
||||
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 2))).toBeLessThan(0);
|
||||
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, FEB, 1))).toBeLessThan(0);
|
||||
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2018, JAN, 1))).toBeLessThan(0);
|
||||
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 1))).toBe(0);
|
||||
expect(adapter.compareDate(new Date(2018, JAN, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0);
|
||||
expect(adapter.compareDate(new Date(2017, FEB, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0);
|
||||
expect(adapter.compareDate(new Date(2017, JAN, 2), new Date(2017, JAN, 1))).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should clamp date at lower bound', () => {
|
||||
expect(adapter.clampDate(
|
||||
new Date(2017, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1)))
|
||||
.toEqual(new Date(2018, JAN, 1));
|
||||
});
|
||||
|
||||
it('should clamp date at upper bound', () => {
|
||||
expect(adapter.clampDate(
|
||||
new Date(2020, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1)))
|
||||
.toEqual(new Date(2019, JAN, 1));
|
||||
});
|
||||
|
||||
it('should clamp date already within bounds', () => {
|
||||
expect(adapter.clampDate(
|
||||
new Date(2018, FEB, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1)))
|
||||
.toEqual(new Date(2018, FEB, 1));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
import {DateAdapter} from './date-adapter';
|
||||
|
||||
|
||||
// TODO(mmalerba): Remove when we no longer support safari 9.
|
||||
/** Whether the browser supports the Intl API. */
|
||||
const SUPPORTS_INTL_API = typeof Intl != 'undefined';
|
||||
|
||||
|
||||
/** The default month names to use if Intl API is not available. */
|
||||
const DEFAULT_MONTH_NAMES = {
|
||||
'long': [
|
||||
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
|
||||
'October', 'November', 'December'
|
||||
],
|
||||
'short': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
'narrow': ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D']
|
||||
};
|
||||
|
||||
|
||||
/** The default date names to use if Intl API is not available. */
|
||||
const DEFAULT_DATE_NAMES = range(31, i => String(i + 1));
|
||||
|
||||
|
||||
/** The default day of the week names to use if Intl API is not available. */
|
||||
const DEFAULT_DAY_OF_WEEK_NAMES = {
|
||||
'long': ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
||||
'short': ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
'narrow': ['S', 'M', 'T', 'W', 'T', 'F', 'S']
|
||||
};
|
||||
|
||||
|
||||
/** Creates an array and fills it with values. */
|
||||
function range<T>(length: number, valueFunction: (index: number) => T): T[] {
|
||||
return Array.apply(null, Array(length)).map((v: undefined, i: number) => valueFunction(i));
|
||||
}
|
||||
|
||||
|
||||
/** Adapts the native JS Date for use with cdk-based components that work with dates. */
|
||||
export class NativeDateAdapter extends DateAdapter<Date> {
|
||||
getYear(date: Date): number {
|
||||
return date.getFullYear();
|
||||
}
|
||||
|
||||
getMonth(date: Date): number {
|
||||
return date.getMonth();
|
||||
}
|
||||
|
||||
getDate(date: Date): number {
|
||||
return date.getDate();
|
||||
}
|
||||
|
||||
getDayOfWeek(date: Date): number {
|
||||
return date.getDay();
|
||||
}
|
||||
|
||||
getMonthNames(style: 'long' | 'short' | 'narrow'): string[] {
|
||||
if (SUPPORTS_INTL_API) {
|
||||
let dtf = new Intl.DateTimeFormat(this.locale, {month: style});
|
||||
return range(12, i => this._stripDirectionalityCharacters(dtf.format(new Date(2017, i, 1))));
|
||||
}
|
||||
return DEFAULT_MONTH_NAMES[style];
|
||||
}
|
||||
|
||||
getDateNames(): string[] {
|
||||
if (SUPPORTS_INTL_API) {
|
||||
let dtf = new Intl.DateTimeFormat(this.locale, {day: 'numeric'});
|
||||
return range(31, i => this._stripDirectionalityCharacters(
|
||||
dtf.format(new Date(2017, 0, i + 1))));
|
||||
}
|
||||
return DEFAULT_DATE_NAMES;
|
||||
}
|
||||
|
||||
getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
|
||||
if (SUPPORTS_INTL_API) {
|
||||
let dtf = new Intl.DateTimeFormat(this.locale, {weekday: style});
|
||||
return range(7, i => this._stripDirectionalityCharacters(
|
||||
dtf.format(new Date(2017, 0, i + 1))));
|
||||
}
|
||||
return DEFAULT_DAY_OF_WEEK_NAMES[style];
|
||||
}
|
||||
|
||||
getYearName(date: Date): string {
|
||||
if (SUPPORTS_INTL_API) {
|
||||
let dtf = new Intl.DateTimeFormat(this.locale, {year: 'numeric'});
|
||||
return this._stripDirectionalityCharacters(dtf.format(date));
|
||||
}
|
||||
return String(this.getYear(date));
|
||||
}
|
||||
|
||||
getFirstDayOfWeek(): number {
|
||||
// We can't tell using native JS Date what the first day of the week is, we default to Sunday.
|
||||
return 0;
|
||||
}
|
||||
|
||||
getNumDaysInMonth(date: Date): number {
|
||||
return this.getDate(this._createDateWithOverflow(
|
||||
this.getYear(date), this.getMonth(date) + 1, 0));
|
||||
}
|
||||
|
||||
clone(date: Date): Date {
|
||||
return this.createDate(this.getYear(date), this.getMonth(date), this.getDate(date));
|
||||
}
|
||||
|
||||
createDate(year: number, month: number, date: number): Date {
|
||||
// Check for invalid month and date (except upper bound on date which we have to check after
|
||||
// creating the Date).
|
||||
if (month < 0 || month > 11 || date < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = this._createDateWithOverflow(year, month, date);
|
||||
|
||||
// Check that the date wasn't above the upper bound for the month, causing the month to
|
||||
// overflow.
|
||||
if (result.getMonth() != month) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
today(): Date {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
parse(value: any, parseFormat: Object): Date | null {
|
||||
// We have no way using the native JS Date to set the parse format or locale, so we ignore these
|
||||
// parameters.
|
||||
let timestamp = typeof value == 'number' ? value : Date.parse(value);
|
||||
return isNaN(timestamp) ? null : new Date(timestamp);
|
||||
}
|
||||
|
||||
format(date: Date, displayFormat: Object): string {
|
||||
if (SUPPORTS_INTL_API) {
|
||||
let dtf = new Intl.DateTimeFormat(this.locale, displayFormat);
|
||||
return this._stripDirectionalityCharacters(dtf.format(date));
|
||||
}
|
||||
return this._stripDirectionalityCharacters(date.toDateString());
|
||||
}
|
||||
|
||||
addCalendarYears(date: Date, years: number): Date {
|
||||
return this.addCalendarMonths(date, years * 12);
|
||||
}
|
||||
|
||||
addCalendarMonths(date: Date, months: number): Date {
|
||||
let newDate = this._createDateWithOverflow(
|
||||
this.getYear(date), this.getMonth(date) + months, this.getDate(date));
|
||||
|
||||
// It's possible to wind up in the wrong month if the original month has more days than the new
|
||||
// month. In this case we want to go to the last day of the desired month.
|
||||
// Note: the additional + 12 % 12 ensures we end up with a positive number, since JS % doesn't
|
||||
// guarantee this.
|
||||
if (this.getMonth(newDate) != ((this.getMonth(date) + months) % 12 + 12) % 12) {
|
||||
newDate = this._createDateWithOverflow(this.getYear(newDate), this.getMonth(newDate), 0);
|
||||
}
|
||||
|
||||
return newDate;
|
||||
}
|
||||
|
||||
addCalendarDays(date: Date, days: number): Date {
|
||||
return this._createDateWithOverflow(
|
||||
this.getYear(date), this.getMonth(date), this.getDate(date) + days);
|
||||
}
|
||||
|
||||
getISODateString(date: Date): string {
|
||||
return [
|
||||
date.getUTCFullYear(),
|
||||
this._2digit(date.getUTCMonth() + 1),
|
||||
this._2digit(date.getUTCDate())
|
||||
].join('-');
|
||||
}
|
||||
|
||||
/** Creates a date but allows the month and date to overflow. */
|
||||
private _createDateWithOverflow(year: number, month: number, date: number) {
|
||||
let result = new Date(year, month, date);
|
||||
|
||||
// We need to correct for the fact that JS native Date treats years in range [0, 99] as
|
||||
// abbreviations for 19xx.
|
||||
if (year >= 0 && year < 100) {
|
||||
result.setFullYear(this.getYear(result) - 1900);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads a number to make it two digits.
|
||||
* @param n The number to pad.
|
||||
* @returns The padded number.
|
||||
*/
|
||||
private _2digit(n: number) {
|
||||
return ('00' + n).slice(-2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted dates while
|
||||
* other browsers do not. We remove them to make output consistent and because they interfere with
|
||||
* date parsing.
|
||||
* @param s The string to strip direction characters from.
|
||||
* @returns The stripped string.
|
||||
*/
|
||||
private _stripDirectionalityCharacters(s: string) {
|
||||
return s.replace(/[\u200e\u200f]/g, '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {MdDateFormats} from './date-formats';
|
||||
|
||||
|
||||
export const MD_NATIVE_DATE_FORMATS: MdDateFormats = {
|
||||
parse: {
|
||||
dateInput: null,
|
||||
},
|
||||
display: {
|
||||
dateInput: {year: 'numeric', month: 'numeric', day: 'numeric'},
|
||||
monthYearLabel: {year: 'numeric', month: 'short'},
|
||||
dateA11yLabel: {year: 'numeric', month: 'long', day: 'numeric'},
|
||||
monthYearA11yLabel: {year: 'numeric', month: 'long'},
|
||||
}
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
@import '../../card/card-theme';
|
||||
@import '../../checkbox/checkbox-theme';
|
||||
@import '../../chips/chips-theme';
|
||||
@import '../../datepicker/datepicker-theme';
|
||||
@import '../../dialog/dialog-theme';
|
||||
@import '../../grid-list/grid-list-theme';
|
||||
@import '../../icon/icon-theme';
|
||||
@@ -33,6 +34,7 @@
|
||||
@include mat-card-theme($theme);
|
||||
@include mat-checkbox-theme($theme);
|
||||
@include mat-chips-theme($theme);
|
||||
@include mat-datepicker-theme($theme);
|
||||
@include mat-dialog-theme($theme);
|
||||
@include mat-grid-list-theme($theme);
|
||||
@include mat-icon-theme($theme);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Please see the official documentation at https://material.angular.io/components/component/datepicker
|
||||
@@ -0,0 +1,80 @@
|
||||
@import '../core/theming/palette';
|
||||
@import '../core/theming/theming';
|
||||
|
||||
|
||||
@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-calendar {
|
||||
background-color: mat-color($background, card);
|
||||
}
|
||||
|
||||
.mat-calendar-arrow {
|
||||
border-top-color: mat-color($foreground, icon);
|
||||
}
|
||||
|
||||
.mat-calendar-next-button,
|
||||
.mat-calendar-previous-button {
|
||||
color: mat-color($foreground, icon);
|
||||
}
|
||||
|
||||
.mat-calendar-table-header {
|
||||
color: mat-color($foreground, hint-text);
|
||||
}
|
||||
|
||||
.mat-calendar-table-header-divider::after {
|
||||
background: mat-color($foreground, divider);
|
||||
}
|
||||
|
||||
.mat-calendar-body-label {
|
||||
color: mat-color($foreground, secondary-text);
|
||||
}
|
||||
|
||||
.mat-calendar-body-cell-content {
|
||||
color: mat-color($foreground, text);
|
||||
border-color: transparent;
|
||||
|
||||
.mat-calendar-body-disabled > &:not(.mat-calendar-body-selected) {
|
||||
color: mat-color($foreground, disabled-text);
|
||||
}
|
||||
}
|
||||
|
||||
:not(.mat-calendar-body-disabled):hover,
|
||||
.cdk-keyboard-focused .mat-calendar-body-active {
|
||||
& > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected) {
|
||||
background-color: mat-color($background, hover);
|
||||
}
|
||||
}
|
||||
|
||||
.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-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected) {
|
||||
border-color: fade-out(mat-color($foreground, hint-text), $mat-datepicker-today-fade-amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<!--
|
||||
If there's not enough space in the first row, create a separate label row. We mark this row as
|
||||
aria-hidden because we don't want it to be read out as one of the weeks in the month.
|
||||
-->
|
||||
<tr *ngIf="_firstRowOffset < labelMinRequiredCells" aria-hidden="true">
|
||||
<td class="mat-calendar-body-label" [attr.colspan]="numCols" >{{label}}</td>
|
||||
</tr>
|
||||
|
||||
<!-- Create the first row separately so we can include a special spacer cell. -->
|
||||
<tr *ngFor="let row of rows; let rowIndex = index" role="row">
|
||||
<!--
|
||||
We mark this cell as aria-hidden so it doesn't get read out as one of the days in the week.
|
||||
-->
|
||||
<td *ngIf="rowIndex === 0 && _firstRowOffset"
|
||||
aria-hidden="true"
|
||||
class="mat-calendar-body-label"
|
||||
[attr.colspan]="_firstRowOffset">
|
||||
{{_firstRowOffset >= labelMinRequiredCells ? label : ''}}
|
||||
</td>
|
||||
<td *ngFor="let item of row; let colIndex = index"
|
||||
role="gridcell"
|
||||
class="mat-calendar-body-cell"
|
||||
[tabindex]="_isActiveCell(rowIndex, colIndex) ? 0 : -1"
|
||||
[class.mat-calendar-body-disabled]="!item.enabled"
|
||||
[class.mat-calendar-body-active]="_isActiveCell(rowIndex, colIndex)"
|
||||
[attr.aria-label]="item.ariaLabel"
|
||||
[attr.aria-disabled]="!item.enabled || null"
|
||||
(click)="_cellClicked(item)">
|
||||
<div class="mat-calendar-body-cell-content"
|
||||
[class.mat-calendar-body-selected]="selectedValue === item.value"
|
||||
[class.mat-calendar-body-today]="todayValue === item.value">
|
||||
{{item.displayValue}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -0,0 +1,65 @@
|
||||
$mat-calendar-body-font-size: 13px !default;
|
||||
$mat-calendar-body-header-font-size: 14px !default;
|
||||
$mat-calendar-body-label-padding-start: 5% !default;
|
||||
$mat-calendar-body-label-translation: -6px !default;
|
||||
$mat-calendar-body-cell-min-size: 32px !default;
|
||||
$mat-calendar-body-cell-size: 100% / 7 !default;
|
||||
$mat-calendar-body-cell-content-margin: 5% !default;
|
||||
$mat-calendar-body-cell-content-border-width: 1px !default;
|
||||
|
||||
$mat-calendar-body-min-size: 7 * $mat-calendar-body-cell-min-size !default;
|
||||
$mat-calendar-body-cell-padding: $mat-calendar-body-cell-size / 2 !default;
|
||||
$mat-calendar-body-cell-content-size: 100% - $mat-calendar-body-cell-content-margin * 2 !default;
|
||||
|
||||
|
||||
.mat-calendar-body {
|
||||
font-size: $mat-calendar-body-font-size;
|
||||
min-width: $mat-calendar-body-min-size;
|
||||
}
|
||||
|
||||
.mat-calendar-body-label {
|
||||
padding: $mat-calendar-body-cell-padding 0
|
||||
$mat-calendar-body-cell-padding $mat-calendar-body-cell-padding;
|
||||
height: 0;
|
||||
line-height: 0;
|
||||
transform: translateX($mat-calendar-body-label-translation);
|
||||
text-align: left;
|
||||
font-size: $mat-calendar-body-header-font-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mat-calendar-body-cell {
|
||||
position: relative;
|
||||
width: $mat-calendar-body-cell-size;
|
||||
height: 0;
|
||||
line-height: 0;
|
||||
padding: $mat-calendar-body-cell-padding 0;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mat-calendar-body-cell-content {
|
||||
position: absolute;
|
||||
top: $mat-calendar-body-cell-content-margin;
|
||||
left: $mat-calendar-body-cell-content-margin;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: $mat-calendar-body-cell-content-size;
|
||||
height: $mat-calendar-body-cell-content-size;
|
||||
|
||||
border-width: $mat-calendar-body-cell-content-border-width;
|
||||
border-style: solid;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
[dir='rtl'] {
|
||||
.mat-calendar-body-label {
|
||||
padding: 0 $mat-calendar-body-cell-padding 0 0;
|
||||
transform: translateX(-$mat-calendar-body-label-translation);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {MdCalendarBody, MdCalendarCell} from './calendar-body';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
||||
|
||||
describe('MdCalendarBody', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
MdCalendarBody,
|
||||
|
||||
// Test components.
|
||||
StandardCalendarBody,
|
||||
CalendarBodyWithDisabledCells,
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('standard calendar body', () => {
|
||||
let fixture: ComponentFixture<StandardCalendarBody>;
|
||||
let testComponent: StandardCalendarBody;
|
||||
let calendarBodyNativeElement: Element;
|
||||
let rowEls: NodeListOf<Element>;
|
||||
let labelEls: NodeListOf<Element>;
|
||||
let cellEls: NodeListOf<Element>;
|
||||
|
||||
let refreshElementLists = () => {
|
||||
rowEls = calendarBodyNativeElement.querySelectorAll('tr');
|
||||
labelEls = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-label');
|
||||
cellEls = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StandardCalendarBody);
|
||||
fixture.detectChanges();
|
||||
|
||||
let calendarBodyDebugElement = fixture.debugElement.query(By.directive(MdCalendarBody));
|
||||
calendarBodyNativeElement = calendarBodyDebugElement.nativeElement;
|
||||
testComponent = fixture.componentInstance;
|
||||
|
||||
refreshElementLists();
|
||||
});
|
||||
|
||||
it('creates body', () => {
|
||||
expect(rowEls.length).toBe(3);
|
||||
expect(labelEls.length).toBe(1);
|
||||
expect(cellEls.length).toBe(14);
|
||||
});
|
||||
|
||||
it('highlights today', () => {
|
||||
let todayCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-today');
|
||||
expect(todayCell).not.toBeNull();
|
||||
expect(todayCell.innerHTML.trim()).toBe('3');
|
||||
});
|
||||
|
||||
it('highlights selected', () => {
|
||||
let selectedCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-selected');
|
||||
expect(selectedCell).not.toBeNull();
|
||||
expect(selectedCell.innerHTML.trim()).toBe('4');
|
||||
});
|
||||
|
||||
it('places label in first row if space is available', () => {
|
||||
testComponent.rows[0] = testComponent.rows[0].slice(3);
|
||||
testComponent.rows = testComponent.rows.slice();
|
||||
fixture.detectChanges();
|
||||
refreshElementLists();
|
||||
|
||||
expect(rowEls.length).toBe(2);
|
||||
expect(labelEls.length).toBe(1);
|
||||
expect(cellEls.length).toBe(11);
|
||||
expect(rowEls[0].firstElementChild.classList)
|
||||
.toContain('mat-calendar-body-label', 'first cell should be the label');
|
||||
expect(labelEls[0].getAttribute('colspan')).toBe('3');
|
||||
});
|
||||
|
||||
it('cell should be selected on click', () => {
|
||||
let todayElement =
|
||||
calendarBodyNativeElement.querySelector('.mat-calendar-body-today') as HTMLElement;
|
||||
todayElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(todayElement.classList)
|
||||
.toContain('mat-calendar-body-selected', 'today should be selected');
|
||||
});
|
||||
|
||||
it('should mark active date', () => {
|
||||
expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11');
|
||||
expect(cellEls[10].classList).toContain('mat-calendar-body-active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calendar body with disabled cells', () => {
|
||||
let fixture: ComponentFixture<CalendarBodyWithDisabledCells>;
|
||||
let testComponent: CalendarBodyWithDisabledCells;
|
||||
let calendarBodyNativeElement: Element;
|
||||
let cellEls: NodeListOf<Element>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CalendarBodyWithDisabledCells);
|
||||
fixture.detectChanges();
|
||||
|
||||
let calendarBodyDebugElement = fixture.debugElement.query(By.directive(MdCalendarBody));
|
||||
calendarBodyNativeElement = calendarBodyDebugElement.nativeElement;
|
||||
testComponent = fixture.componentInstance;
|
||||
cellEls = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
});
|
||||
|
||||
it('should only allow selection of disabled cells when allowDisabledSelection is true', () => {
|
||||
(cellEls[0] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toBeFalsy();
|
||||
|
||||
testComponent.allowDisabledSelection = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
(cellEls[0] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<table md-calendar-body
|
||||
[label]="label"
|
||||
[rows]="rows"
|
||||
[todayValue]="todayValue"
|
||||
[selectedValue]="selectedValue"
|
||||
[labelMinRequiredCells]="labelMinRequiredCells"
|
||||
[numCols]="numCols"
|
||||
[activeCell]="10"
|
||||
(selectedValueChange)="onSelect($event)">
|
||||
</table>`,
|
||||
})
|
||||
class StandardCalendarBody {
|
||||
label = 'Jan 2017';
|
||||
rows = [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14]].map(r => r.map(createCell));
|
||||
todayValue = 3;
|
||||
selectedValue = 4;
|
||||
labelMinRequiredCells = 3;
|
||||
numCols = 7;
|
||||
|
||||
onSelect(value: number) {
|
||||
this.selectedValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<table md-calendar-body
|
||||
[rows]="rows"
|
||||
[allowDisabledSelection]="allowDisabledSelection"
|
||||
(selectedValueChange)="selected = $event">
|
||||
</table>`
|
||||
})
|
||||
class CalendarBodyWithDisabledCells {
|
||||
rows = [[1, 2, 3, 4]].map(r => r.map(d => {
|
||||
let cell = createCell(d);
|
||||
cell.enabled = d % 2 == 0;
|
||||
return cell;
|
||||
}));
|
||||
allowDisabledSelection = false;
|
||||
selected: Date;
|
||||
}
|
||||
|
||||
|
||||
function createCell(value: number) {
|
||||
return new MdCalendarCell(value, `${value}`, `${value}-label`, true);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
|
||||
|
||||
/**
|
||||
* An internal class that represents the data corresponding to a single calendar cell.
|
||||
* @docs-private
|
||||
*/
|
||||
export class MdCalendarCell {
|
||||
constructor(public value: number,
|
||||
public displayValue: string,
|
||||
public ariaLabel: string,
|
||||
public enabled: boolean) {}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An internal component used to display calendar data in a table.
|
||||
* @docs-private
|
||||
*/
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: '[md-calendar-body]',
|
||||
templateUrl: 'calendar-body.html',
|
||||
styleUrls: ['calendar-body.css'],
|
||||
host: {
|
||||
'class': 'mat-calendar-body',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdCalendarBody {
|
||||
/** The label for the table. (e.g. "Jan 2017"). */
|
||||
@Input() label: string;
|
||||
|
||||
/** The cells to display in the table. */
|
||||
@Input() rows: MdCalendarCell[][];
|
||||
|
||||
/** The value in the table that corresponds to today. */
|
||||
@Input() todayValue: number;
|
||||
|
||||
/** The value in the table that is currently selected. */
|
||||
@Input() selectedValue: number;
|
||||
|
||||
/** The minimum number of free cells needed to fit the label in the first row. */
|
||||
@Input() labelMinRequiredCells: number;
|
||||
|
||||
/** The number of columns in the table. */
|
||||
@Input() numCols = 7;
|
||||
|
||||
/** Whether to allow selection of disabled cells. */
|
||||
@Input() allowDisabledSelection = false;
|
||||
|
||||
/** The cell number of the active cell in the table. */
|
||||
@Input() activeCell = 0;
|
||||
|
||||
/** Emits when a new value is selected. */
|
||||
@Output() selectedValueChange = new EventEmitter<number>();
|
||||
|
||||
_cellClicked(cell: MdCalendarCell): void {
|
||||
if (!this.allowDisabledSelection && !cell.enabled) {
|
||||
return;
|
||||
}
|
||||
this.selectedValueChange.emit(cell.value);
|
||||
}
|
||||
|
||||
/** The number of blank cells to put at the beginning for the first row. */
|
||||
get _firstRowOffset(): number {
|
||||
return this.rows && this.rows.length && this.rows[0].length ?
|
||||
this.numCols - this.rows[0].length : 0;
|
||||
}
|
||||
|
||||
_isActiveCell(rowIndex: number, colIndex: number): boolean {
|
||||
let cellNumber = rowIndex * this.numCols + colIndex;
|
||||
|
||||
// Account for the fact that the first row may not have as many cells.
|
||||
if (rowIndex) {
|
||||
cellNumber -= this._firstRowOffset;
|
||||
}
|
||||
|
||||
return cellNumber == this.activeCell;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="mat-calendar-header">
|
||||
<div class="mat-calendar-controls">
|
||||
<button md-button class="mat-calendar-period-button" (click)="_currentPeriodClicked()"
|
||||
[attr.aria-label]="_periodButtonLabel">
|
||||
{{_periodButtonText}}
|
||||
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="!_monthView"></div>
|
||||
</button>
|
||||
<div class="mat-calendar-spacer"></div>
|
||||
<button md-icon-button class="mat-calendar-previous-button" [disabled]="!_previousEnabled()"
|
||||
(click)="_previousClicked()" [attr.aria-label]="_prevButtonLabel">
|
||||
</button>
|
||||
<button md-icon-button class="mat-calendar-next-button" [disabled]="!_nextEnabled()"
|
||||
(click)="_nextClicked()" [attr.aria-label]="_nextButtonLabel">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
|
||||
cdkMonitorSubtreeFocus>
|
||||
<md-month-view
|
||||
*ngIf="_monthView"
|
||||
[activeDate]="_activeDate"
|
||||
[selected]="selected"
|
||||
[dateFilter]="_dateFilterForViews"
|
||||
(selectedChange)="_dateSelected($event)">
|
||||
</md-month-view>
|
||||
|
||||
<md-year-view
|
||||
*ngIf="!_monthView"
|
||||
[activeDate]="_activeDate"
|
||||
[selected]="selected"
|
||||
[dateFilter]="_dateFilterForViews"
|
||||
(selectedChange)="_monthSelected($event)">
|
||||
</md-year-view>
|
||||
</div>
|
||||
@@ -0,0 +1,122 @@
|
||||
$mat-calendar-padding: 8px !default;
|
||||
$mat-calendar-header-divider-width: 1px !default;
|
||||
$mat-calendar-controls-vertical-padding: 5%;
|
||||
// We want to indent to the middle of the first tile. There are 7 tiles, so 100% / 7 / 2.
|
||||
// Then we back up a little bit since the text in the cells is center-aligned.
|
||||
$mat-calendar-controls-start-padding: calc(100% / 14 - 22px);
|
||||
// Same as above, but on other side for arrows.
|
||||
$mat-calendar-controls-end-padding: calc(100% / 14 - 22px);
|
||||
$mat-calendar-period-font-size: 14px;
|
||||
$mat-calendar-arrow-size: 5px !default;
|
||||
$mat-calendar-arrow-disabled-opacity: 0.5 !default;
|
||||
$mat-calendar-weekday-table-font-size: 11px !default;
|
||||
|
||||
// Values chosen to approximate https://material.io/icons/#ic_navigate_before and
|
||||
// https://material.io/icons/#ic_navigate_next as closely as possible.
|
||||
$mat-calendar-prev-next-icon-border-width: 2px;
|
||||
$mat-calendar-prev-next-icon-margin: 15.5px;
|
||||
$mat-calendar-prev-icon-transform: translateX(2px) rotate(-45deg);
|
||||
$mat-calendar-next-icon-transform: translateX(-2px) rotate(45deg);
|
||||
|
||||
|
||||
.mat-calendar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mat-calendar-header {
|
||||
padding: $mat-calendar-padding $mat-calendar-padding 0 $mat-calendar-padding;
|
||||
}
|
||||
|
||||
.mat-calendar-content {
|
||||
padding: 0 $mat-calendar-padding $mat-calendar-padding $mat-calendar-padding;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mat-calendar-controls {
|
||||
display: flex;
|
||||
padding: $mat-calendar-controls-vertical-padding $mat-calendar-controls-end-padding
|
||||
$mat-calendar-controls-vertical-padding $mat-calendar-controls-start-padding;
|
||||
}
|
||||
|
||||
.mat-calendar-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.mat-calendar-period-button {
|
||||
font: inherit;
|
||||
font-size: $mat-calendar-period-font-size;
|
||||
font-weight: bold;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mat-calendar-arrow {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: $mat-calendar-arrow-size solid transparent;
|
||||
border-right: $mat-calendar-arrow-size solid transparent;
|
||||
border-top-width: $mat-calendar-arrow-size;
|
||||
border-top-style: solid;
|
||||
margin: 0 0 0 $mat-calendar-arrow-size;
|
||||
vertical-align: middle;
|
||||
|
||||
&.mat-calendar-invert {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-calendar-previous-button,
|
||||
.mat-calendar-next-button {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: $mat-calendar-prev-next-icon-margin;
|
||||
border: 0 solid currentColor;
|
||||
border-top-width: $mat-calendar-prev-next-icon-border-width;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-calendar-previous-button::after {
|
||||
border-left-width: $mat-calendar-prev-next-icon-border-width;
|
||||
transform: $mat-calendar-prev-icon-transform;
|
||||
}
|
||||
|
||||
.mat-calendar-next-button::after {
|
||||
border-right-width: $mat-calendar-prev-next-icon-border-width;
|
||||
transform: $mat-calendar-next-icon-transform;
|
||||
}
|
||||
|
||||
.mat-calendar-table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-calendar-table-header th {
|
||||
text-align: center;
|
||||
font-size: $mat-calendar-weekday-table-font-size;
|
||||
font-weight: normal;
|
||||
padding: 0 0 $mat-calendar-padding 0;
|
||||
}
|
||||
|
||||
.mat-calendar-table-header-divider {
|
||||
position: relative;
|
||||
height: $mat-calendar-header-divider-width;
|
||||
|
||||
// We use an absolutely positioned pseudo-element as the divider line for the table header so we
|
||||
// can extend it all the way to the edge of the calendar.
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -$mat-calendar-padding;
|
||||
right: -$mat-calendar-padding;
|
||||
height: $mat-calendar-header-divider-width;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {MdCalendar} from './calendar';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdMonthView} from './month-view';
|
||||
import {MdYearView} from './year-view';
|
||||
import {MdCalendarBody} from './calendar-body';
|
||||
import {
|
||||
dispatchFakeEvent,
|
||||
dispatchKeyboardEvent,
|
||||
dispatchMouseEvent
|
||||
} from '../core/testing/dispatch-events';
|
||||
import {
|
||||
DOWN_ARROW,
|
||||
END,
|
||||
ENTER,
|
||||
HOME,
|
||||
LEFT_ARROW,
|
||||
PAGE_DOWN,
|
||||
PAGE_UP,
|
||||
RIGHT_ARROW,
|
||||
UP_ARROW
|
||||
} from '../core/keyboard/keycodes';
|
||||
import {MdDatepickerIntl} from './datepicker-intl';
|
||||
import {MdNativeDateModule} from '../core/datetime/index';
|
||||
|
||||
|
||||
// When constructing a Date, the month is zero-based. This can be confusing, since people are
|
||||
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
|
||||
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
|
||||
NOV = 10, DEC = 11;
|
||||
|
||||
|
||||
describe('MdCalendar', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MdNativeDateModule,
|
||||
],
|
||||
declarations: [
|
||||
MdCalendar,
|
||||
MdCalendarBody,
|
||||
MdMonthView,
|
||||
MdYearView,
|
||||
|
||||
// Test components.
|
||||
StandardCalendar,
|
||||
CalendarWithMinMax,
|
||||
CalendarWithDateFilter,
|
||||
],
|
||||
providers: [
|
||||
MdDatepickerIntl,
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('standard calendar', () => {
|
||||
let fixture: ComponentFixture<StandardCalendar>;
|
||||
let testComponent: StandardCalendar;
|
||||
let calendarElement: HTMLElement;
|
||||
let periodButton: HTMLElement;
|
||||
let prevButton: HTMLElement;
|
||||
let nextButton: HTMLElement;
|
||||
let calendarInstance: MdCalendar<Date>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StandardCalendar);
|
||||
fixture.detectChanges();
|
||||
|
||||
let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar));
|
||||
calendarElement = calendarDebugElement.nativeElement;
|
||||
periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
|
||||
prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement;
|
||||
nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement;
|
||||
|
||||
calendarInstance = calendarDebugElement.componentInstance;
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should be in month view with specified month active', () => {
|
||||
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
});
|
||||
|
||||
it('should toggle view when period clicked', () => {
|
||||
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
|
||||
|
||||
periodButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(false, 'should be in year view');
|
||||
|
||||
periodButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
|
||||
});
|
||||
|
||||
it('should go to next and previous month', () => {
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
|
||||
nextButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
|
||||
|
||||
prevButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 28));
|
||||
});
|
||||
|
||||
it('should go to previous and next year', () => {
|
||||
periodButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(false, 'should be in year view');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
|
||||
nextButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 31));
|
||||
|
||||
prevButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
});
|
||||
|
||||
it('should go back to month view after selecting month in year view', () => {
|
||||
periodButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(false, 'should be in year view');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
|
||||
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
(monthCells[monthCells.length - 1] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
|
||||
expect(testComponent.selected).toBeFalsy('no date should be selected yet');
|
||||
});
|
||||
|
||||
it('should select date in month view', () => {
|
||||
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
(monthCells[monthCells.length - 1] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
|
||||
expect(testComponent.selected).toEqual(new Date(2017, JAN, 31));
|
||||
});
|
||||
|
||||
describe('a11y', () => {
|
||||
describe('calendar body', () => {
|
||||
let calendarBodyEl: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
calendarBodyEl = calendarElement.querySelector('.mat-calendar-content') as HTMLElement;
|
||||
expect(calendarBodyEl).not.toBeNull();
|
||||
|
||||
dispatchFakeEvent(calendarBodyEl, 'focus');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should initially set start date active', () => {
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
});
|
||||
|
||||
describe('month view', () => {
|
||||
it('should decrement date on left arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
|
||||
|
||||
calendarInstance._activeDate = new Date(2017, JAN, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
|
||||
});
|
||||
|
||||
it('should increment date on right arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 1));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 2));
|
||||
});
|
||||
|
||||
it('should go up a row on up arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 24));
|
||||
|
||||
calendarInstance._activeDate = new Date(2017, JAN, 7);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
|
||||
});
|
||||
|
||||
it('should go down a row on down arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 7));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 14));
|
||||
});
|
||||
|
||||
it('should go to beginning of the month on home press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
|
||||
});
|
||||
|
||||
it('should go to end of the month on end press', () => {
|
||||
calendarInstance._activeDate = new Date(2017, JAN, 10);
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
|
||||
});
|
||||
|
||||
it('should go back one month on page up press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, NOV, 30));
|
||||
});
|
||||
|
||||
it('should go forward one month on page down press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 28));
|
||||
});
|
||||
|
||||
it('should select active date on enter', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toBeNull();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toEqual(new Date(2017, JAN, 30));
|
||||
});
|
||||
});
|
||||
|
||||
describe('year view', () => {
|
||||
beforeEach(() => {
|
||||
dispatchMouseEvent(periodButton, 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(false);
|
||||
});
|
||||
|
||||
it('should decrement month on left arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, NOV, 30));
|
||||
});
|
||||
|
||||
it('should increment month on right arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 28));
|
||||
});
|
||||
|
||||
it('should go up a row on up arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, AUG, 31));
|
||||
|
||||
calendarInstance._activeDate = new Date(2017, JUL, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, JUL, 1));
|
||||
|
||||
calendarInstance._activeDate = new Date(2017, DEC, 10);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAY, 10));
|
||||
});
|
||||
|
||||
it('should go down a row on down arrow press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, AUG, 31));
|
||||
|
||||
calendarInstance._activeDate = new Date(2017, JUN, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2018, JUN, 1));
|
||||
|
||||
calendarInstance._activeDate = new Date(2017, SEP, 30);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2018, FEB, 28));
|
||||
});
|
||||
|
||||
it('should go to first month of the year on home press', () => {
|
||||
calendarInstance._activeDate = new Date(2017, SEP, 30);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
|
||||
});
|
||||
|
||||
it('should go to last month of the year on end press', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
|
||||
});
|
||||
|
||||
it('should go back one year on page up press', () => {
|
||||
calendarInstance._activeDate = new Date(2016, FEB, 29);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2015, FEB, 28));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2014, FEB, 28));
|
||||
});
|
||||
|
||||
it('should go forward one year on page down press', () => {
|
||||
calendarInstance._activeDate = new Date(2016, FEB, 29);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2018, FEB, 28));
|
||||
});
|
||||
|
||||
it('should return to month view on enter', () => {
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(true);
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
|
||||
expect(testComponent.selected).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('calendar with min and max date', () => {
|
||||
let fixture: ComponentFixture<CalendarWithMinMax>;
|
||||
let testComponent: CalendarWithMinMax;
|
||||
let calendarElement: HTMLElement;
|
||||
let prevButton: HTMLButtonElement;
|
||||
let nextButton: HTMLButtonElement;
|
||||
let calendarInstance: MdCalendar<Date>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CalendarWithMinMax);
|
||||
|
||||
let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar));
|
||||
calendarElement = calendarDebugElement.nativeElement;
|
||||
prevButton =
|
||||
calendarElement.querySelector('.mat-calendar-previous-button') as HTMLButtonElement;
|
||||
nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLButtonElement;
|
||||
calendarInstance = calendarDebugElement.componentInstance;
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should clamp startAt value below min date', () => {
|
||||
testComponent.startAt = new Date(2000, JAN, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
|
||||
});
|
||||
|
||||
it('should clamp startAt value above max date', () => {
|
||||
testComponent.startAt = new Date(2020, JAN, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
|
||||
});
|
||||
|
||||
it('should not go back past min date', () => {
|
||||
testComponent.startAt = new Date(2016, FEB, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(prevButton.disabled).toBe(false, 'previous button should not be disabled');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, FEB, 1));
|
||||
|
||||
prevButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(prevButton.disabled).toBe(true, 'previous button should be disabled');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
|
||||
|
||||
prevButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
|
||||
});
|
||||
|
||||
it('should not go forward past max date', () => {
|
||||
testComponent.startAt = new Date(2017, DEC, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(nextButton.disabled).toBe(false, 'next button should not be disabled');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 1));
|
||||
|
||||
nextButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(nextButton.disabled).toBe(true, 'next button should be disabled');
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
|
||||
|
||||
nextButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
|
||||
});
|
||||
});
|
||||
|
||||
describe('calendar with date filter', () => {
|
||||
let fixture: ComponentFixture<CalendarWithDateFilter>;
|
||||
let testComponent: CalendarWithDateFilter;
|
||||
let calendarElement: HTMLElement;
|
||||
let calendarInstance: MdCalendar<Date>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CalendarWithDateFilter);
|
||||
fixture.detectChanges();
|
||||
|
||||
let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar));
|
||||
calendarElement = calendarDebugElement.nativeElement;
|
||||
calendarInstance = calendarDebugElement.componentInstance;
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should disable and prevent selection of filtered dates', () => {
|
||||
let cells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
(cells[0] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toBeFalsy();
|
||||
|
||||
(cells[1] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toEqual(new Date(2017, JAN, 2));
|
||||
});
|
||||
|
||||
describe('a11y', () => {
|
||||
let calendarBodyEl: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
calendarBodyEl = calendarElement.querySelector('.mat-calendar-content') as HTMLElement;
|
||||
expect(calendarBodyEl).not.toBeNull();
|
||||
|
||||
dispatchFakeEvent(calendarBodyEl, 'focus');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not allow selection of disabled date in month view', () => {
|
||||
expect(calendarInstance._monthView).toBe(true);
|
||||
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toBeNull();
|
||||
});
|
||||
|
||||
it('should allow entering month view at disabled month', () => {
|
||||
let periodButton =
|
||||
calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
|
||||
dispatchMouseEvent(periodButton, 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
calendarInstance._activeDate = new Date(2017, NOV, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(false);
|
||||
|
||||
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(calendarInstance._monthView).toBe(true);
|
||||
expect(testComponent.selected).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<md-calendar [startAt]="startDate" [(selected)]="selected"></md-calendar>`
|
||||
})
|
||||
class StandardCalendar {
|
||||
selected: Date = null;
|
||||
startDate = new Date(2017, JAN, 31);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-calendar [startAt]="startAt" [minDate]="minDate" [maxDate]="maxDate"></md-calendar>
|
||||
`
|
||||
})
|
||||
class CalendarWithMinMax {
|
||||
startAt: Date;
|
||||
minDate = new Date(2016, JAN, 1);
|
||||
maxDate = new Date(2018, JAN, 1);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-calendar [startAt]="startDate" [(selected)]="selected" [dateFilter]="dateFilter">
|
||||
</md-calendar>
|
||||
`
|
||||
})
|
||||
class CalendarWithDateFilter {
|
||||
selected: Date = null;
|
||||
startDate = new Date(2017, JAN, 1);
|
||||
|
||||
dateFilter (date: Date) {
|
||||
return date.getDate() % 2 == 0 && date.getMonth() != NOV;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
NgZone,
|
||||
Optional,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {
|
||||
DOWN_ARROW,
|
||||
END,
|
||||
ENTER,
|
||||
HOME,
|
||||
LEFT_ARROW,
|
||||
PAGE_DOWN,
|
||||
PAGE_UP,
|
||||
RIGHT_ARROW,
|
||||
UP_ARROW
|
||||
} from '../core/keyboard/keycodes';
|
||||
import {DateAdapter} from '../core/datetime/index';
|
||||
import {MdDatepickerIntl} from './datepicker-intl';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
|
||||
|
||||
|
||||
/**
|
||||
* A calendar that is used as part of the datepicker.
|
||||
* @docs-private
|
||||
*/
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-calendar',
|
||||
templateUrl: 'calendar.html',
|
||||
styleUrls: ['calendar.css'],
|
||||
host: {
|
||||
'[class.mat-calendar]': 'true',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdCalendar<D> implements AfterContentInit {
|
||||
/** A date representing the period (month or year) to start the calendar in. */
|
||||
@Input() startAt: D;
|
||||
|
||||
/** Whether the calendar should be started in month or year view. */
|
||||
@Input() startView: 'month' | 'year' = 'month';
|
||||
|
||||
/** The currently selected date. */
|
||||
@Input() selected: D;
|
||||
|
||||
/** The minimum selectable date. */
|
||||
@Input() minDate: D;
|
||||
|
||||
/** The maximum selectable date. */
|
||||
@Input() maxDate: D;
|
||||
|
||||
/** A function used to filter which dates are selectable. */
|
||||
@Input() dateFilter: (date: D) => boolean;
|
||||
|
||||
/** Emits when the currently selected date changes. */
|
||||
@Output() selectedChange = new EventEmitter<D>();
|
||||
|
||||
/** Date filter for the month and year views. */
|
||||
_dateFilterForViews = (date: D) => {
|
||||
return !!date &&
|
||||
(!this.dateFilter || this.dateFilter(date)) &&
|
||||
(!this.minDate || this._dateAdapter.compareDate(date, this.minDate) >= 0) &&
|
||||
(!this.maxDate || this._dateAdapter.compareDate(date, this.maxDate) <= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current active date. This determines which time period is shown and which date is
|
||||
* highlighted when using keyboard navigation.
|
||||
*/
|
||||
get _activeDate(): D { return this._clampedActiveDate; }
|
||||
set _activeDate(value: D) {
|
||||
this._clampedActiveDate = this._dateAdapter.clampDate(value, this.minDate, this.maxDate);
|
||||
}
|
||||
private _clampedActiveDate: D;
|
||||
|
||||
/** Whether the calendar is in month view. */
|
||||
_monthView: boolean;
|
||||
|
||||
/** The label for the current calendar view. */
|
||||
get _periodButtonText(): string {
|
||||
return this._monthView ?
|
||||
this._dateAdapter.format(this._activeDate, this._dateFormats.display.monthYearLabel)
|
||||
.toLocaleUpperCase() :
|
||||
this._dateAdapter.getYearName(this._activeDate);
|
||||
}
|
||||
|
||||
get _periodButtonLabel(): string {
|
||||
return this._monthView ? this._intl.switchToYearViewLabel : this._intl.switchToMonthViewLabel;
|
||||
}
|
||||
|
||||
/** The label for the the previous button. */
|
||||
get _prevButtonLabel(): string {
|
||||
return this._monthView ? this._intl.prevMonthLabel : this._intl.prevYearLabel;
|
||||
}
|
||||
|
||||
/** The label for the the next button. */
|
||||
get _nextButtonLabel(): string {
|
||||
return this._monthView ? this._intl.nextMonthLabel : this._intl.nextYearLabel;
|
||||
}
|
||||
|
||||
constructor(private _elementRef: ElementRef,
|
||||
private _intl: MdDatepickerIntl,
|
||||
private _ngZone: NgZone,
|
||||
@Optional() private _dateAdapter: DateAdapter<D>,
|
||||
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
|
||||
if (!this._dateAdapter) {
|
||||
throw createMissingDateImplError('DateAdapter');
|
||||
}
|
||||
if (!this._dateFormats) {
|
||||
throw createMissingDateImplError('MD_DATE_FORMATS');
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._activeDate = this.startAt || this._dateAdapter.today();
|
||||
this._focusActiveCell();
|
||||
this._monthView = this.startView != 'year';
|
||||
}
|
||||
|
||||
/** Handles date selection in the month view. */
|
||||
_dateSelected(date: D): void {
|
||||
if (!this._dateAdapter.sameDate(date, this.selected)) {
|
||||
this.selectedChange.emit(date);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles month selection in the year view. */
|
||||
_monthSelected(month: D): void {
|
||||
this._activeDate = month;
|
||||
this._monthView = true;
|
||||
}
|
||||
|
||||
/** Handles user clicks on the period label. */
|
||||
_currentPeriodClicked(): void {
|
||||
this._monthView = !this._monthView;
|
||||
}
|
||||
|
||||
/** Handles user clicks on the previous button. */
|
||||
_previousClicked(): void {
|
||||
this._activeDate = this._monthView ?
|
||||
this._dateAdapter.addCalendarMonths(this._activeDate, -1) :
|
||||
this._dateAdapter.addCalendarYears(this._activeDate, -1);
|
||||
}
|
||||
|
||||
/** Handles user clicks on the next button. */
|
||||
_nextClicked(): void {
|
||||
this._activeDate = this._monthView ?
|
||||
this._dateAdapter.addCalendarMonths(this._activeDate, 1) :
|
||||
this._dateAdapter.addCalendarYears(this._activeDate, 1);
|
||||
}
|
||||
|
||||
/** Whether the previous period button is enabled. */
|
||||
_previousEnabled(): boolean {
|
||||
if (!this.minDate) {
|
||||
return true;
|
||||
}
|
||||
return !this.minDate || !this._isSameView(this._activeDate, this.minDate);
|
||||
}
|
||||
|
||||
/** Whether the next period button is enabled. */
|
||||
_nextEnabled(): boolean {
|
||||
return !this.maxDate || !this._isSameView(this._activeDate, this.maxDate);
|
||||
}
|
||||
|
||||
/** Handles keydown events on the calendar body. */
|
||||
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
|
||||
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
|
||||
// disabled ones from being selected. This may not be ideal, we should look into whether
|
||||
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
|
||||
if (this._monthView) {
|
||||
this._handleCalendarBodyKeydownInMonthView(event);
|
||||
} else {
|
||||
this._handleCalendarBodyKeydownInYearView(event);
|
||||
}
|
||||
}
|
||||
|
||||
/** Focuses the active cell after the microtask queue is empty. */
|
||||
_focusActiveCell() {
|
||||
this._ngZone.runOutsideAngular(() => this._ngZone.onStable.first().subscribe(() => {
|
||||
let activeEl = this._elementRef.nativeElement.querySelector('.mat-calendar-body-active');
|
||||
activeEl.focus();
|
||||
}));
|
||||
}
|
||||
|
||||
/** Whether the two dates represent the same view in the current view mode (month or year). */
|
||||
private _isSameView(date1: D, date2: D): boolean {
|
||||
return this._monthView ?
|
||||
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
|
||||
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2) :
|
||||
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
|
||||
}
|
||||
|
||||
/** Handles keydown events on the calendar body when calendar is in month view. */
|
||||
private _handleCalendarBodyKeydownInMonthView(event: KeyboardEvent): void {
|
||||
switch (event.keyCode) {
|
||||
case LEFT_ARROW:
|
||||
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, -1);
|
||||
break;
|
||||
case RIGHT_ARROW:
|
||||
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 1);
|
||||
break;
|
||||
case UP_ARROW:
|
||||
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, -7);
|
||||
break;
|
||||
case DOWN_ARROW:
|
||||
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 7);
|
||||
break;
|
||||
case HOME:
|
||||
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
|
||||
1 - this._dateAdapter.getDate(this._activeDate));
|
||||
break;
|
||||
case END:
|
||||
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
|
||||
(this._dateAdapter.getNumDaysInMonth(this._activeDate) -
|
||||
this._dateAdapter.getDate(this._activeDate)));
|
||||
break;
|
||||
case PAGE_UP:
|
||||
this._activeDate = event.altKey ?
|
||||
this._dateAdapter.addCalendarYears(this._activeDate, -1) :
|
||||
this._dateAdapter.addCalendarMonths(this._activeDate, -1);
|
||||
break;
|
||||
case PAGE_DOWN:
|
||||
this._activeDate = event.altKey ?
|
||||
this._dateAdapter.addCalendarYears(this._activeDate, 1) :
|
||||
this._dateAdapter.addCalendarMonths(this._activeDate, 1);
|
||||
break;
|
||||
case ENTER:
|
||||
if (this._dateFilterForViews(this._activeDate)) {
|
||||
this._dateSelected(this._activeDate);
|
||||
// Prevent unexpected default actions such as form submission.
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
default:
|
||||
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
|
||||
return;
|
||||
}
|
||||
|
||||
this._focusActiveCell();
|
||||
// Prevent unexpected default actions such as form submission.
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/** Handles keydown events on the calendar body when calendar is in year view. */
|
||||
private _handleCalendarBodyKeydownInYearView(event: KeyboardEvent): void {
|
||||
switch (event.keyCode) {
|
||||
case LEFT_ARROW:
|
||||
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, -1);
|
||||
break;
|
||||
case RIGHT_ARROW:
|
||||
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 1);
|
||||
break;
|
||||
case UP_ARROW:
|
||||
this._activeDate = this._prevMonthInSameCol(this._activeDate);
|
||||
break;
|
||||
case DOWN_ARROW:
|
||||
this._activeDate = this._nextMonthInSameCol(this._activeDate);
|
||||
break;
|
||||
case HOME:
|
||||
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
|
||||
-this._dateAdapter.getMonth(this._activeDate));
|
||||
break;
|
||||
case END:
|
||||
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
|
||||
11 - this._dateAdapter.getMonth(this._activeDate));
|
||||
break;
|
||||
case PAGE_UP:
|
||||
this._activeDate =
|
||||
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? -10 : -1);
|
||||
break;
|
||||
case PAGE_DOWN:
|
||||
this._activeDate =
|
||||
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? 10 : 1);
|
||||
break;
|
||||
case ENTER:
|
||||
this._monthSelected(this._activeDate);
|
||||
break;
|
||||
default:
|
||||
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
|
||||
return;
|
||||
}
|
||||
|
||||
this._focusActiveCell();
|
||||
// Prevent unexpected default actions such as form submission.
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the date for the month that comes before the given month in the same column in the
|
||||
* calendar table.
|
||||
*/
|
||||
private _prevMonthInSameCol(date: D): D {
|
||||
// Determine how many months to jump forward given that there are 2 empty slots at the beginning
|
||||
// of each year.
|
||||
let increment = this._dateAdapter.getMonth(date) <= 4 ? -5 :
|
||||
(this._dateAdapter.getMonth(date) >= 7 ? -7 : -12);
|
||||
return this._dateAdapter.addCalendarMonths(date, increment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the date for the month that comes after the given month in the same column in the
|
||||
* calendar table.
|
||||
*/
|
||||
private _nextMonthInSameCol(date: D): D {
|
||||
// Determine how many months to jump forward given that there are 2 empty slots at the beginning
|
||||
// of each year.
|
||||
let increment = this._dateAdapter.getMonth(date) <= 4 ? 7 :
|
||||
(this._dateAdapter.getMonth(date) >= 7 ? 5 : 12);
|
||||
return this._dateAdapter.addCalendarMonths(date, increment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<md-calendar cdkTrapFocus
|
||||
[id]="datepicker.id"
|
||||
[startAt]="datepicker.startAt"
|
||||
[startView]="datepicker.startView"
|
||||
[minDate]="datepicker._minDate"
|
||||
[maxDate]="datepicker._maxDate"
|
||||
[dateFilter]="datepicker._dateFilter"
|
||||
[selected]="datepicker._selected"
|
||||
(selectedChange)="datepicker._selectAndClose($event)">
|
||||
</md-calendar>
|
||||
@@ -0,0 +1,46 @@
|
||||
@import '../core/style/elevation';
|
||||
|
||||
|
||||
$md-datepicker-calendar-padding: 8px;
|
||||
$md-datepicker-non-touch-calendar-cell-size: 40px;
|
||||
$md-datepicker-non-touch-calendar-width:
|
||||
$md-datepicker-non-touch-calendar-cell-size * 7 + $md-datepicker-calendar-padding * 2;
|
||||
|
||||
// Ideally the calendar would have a constant aspect ratio, no matter its size, and we would base
|
||||
// these measurements off the aspect ratio. Unfortunately, the aspect ratio does change a little as
|
||||
// the calendar grows, since some of the elements have pixel-based sizes. These numbers have been
|
||||
// chosen to minimize extra whitespace at larger sizes, while still ensuring we won't need
|
||||
// scrollbars at smaller sizes.
|
||||
$md-datepicker-touch-width: 64vmin;
|
||||
$md-datepicker-touch-height: 80vmin;
|
||||
$md-datepicker-touch-min-width: 250px;
|
||||
$md-datepicker-touch-min-height: 312px;
|
||||
$md-datepicker-touch-max-width: 750px;
|
||||
$md-datepicker-touch-max-height: 788px;
|
||||
|
||||
|
||||
.mat-calendar {
|
||||
width: $md-datepicker-non-touch-calendar-width;
|
||||
@include mat-elevation(8);
|
||||
}
|
||||
|
||||
.mat-datepicker-content-touch {
|
||||
display: block;
|
||||
// make sure the dialog scrolls rather than being cropped on ludicrously small screens
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
|
||||
// TODO(mmalerba): hack to offset the padding of the dialog. Can be removed when we switch away
|
||||
// from using dialog.
|
||||
margin: -24px;
|
||||
|
||||
.mat-calendar {
|
||||
width: $md-datepicker-touch-width;
|
||||
height: $md-datepicker-touch-height;
|
||||
min-width: $md-datepicker-touch-min-width;
|
||||
min-height: $md-datepicker-touch-min-height;
|
||||
max-width: $md-datepicker-touch-max-width;
|
||||
max-height: $md-datepicker-touch-max-height;
|
||||
@include mat-elevation(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @docs-private */
|
||||
export function createMissingDateImplError(provider: string) {
|
||||
return new Error(
|
||||
`MdDatepicker: No provider found for ${provider}. You must import one of the following` +
|
||||
`modules at your application root: MdNativeDateModule, or provide a custom implementation.`);
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Optional,
|
||||
Renderer2
|
||||
} from '@angular/core';
|
||||
import {MdDatepicker} from './datepicker';
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {MdInputContainer} from '../input/input-container';
|
||||
import {DOWN_ARROW} from '../core/keyboard/keycodes';
|
||||
import {DateAdapter} from '../core/datetime/index';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
|
||||
|
||||
|
||||
export const MD_DATEPICKER_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => MdDatepickerInput),
|
||||
multi: true
|
||||
};
|
||||
|
||||
|
||||
export const MD_DATEPICKER_VALIDATORS: any = {
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => MdDatepickerInput),
|
||||
multi: true
|
||||
};
|
||||
|
||||
|
||||
/** Directive used to connect an input to a MdDatepicker. */
|
||||
@Directive({
|
||||
selector: 'input[mdDatepicker], input[matDatepicker]',
|
||||
providers: [MD_DATEPICKER_VALUE_ACCESSOR, MD_DATEPICKER_VALIDATORS],
|
||||
host: {
|
||||
'[attr.aria-expanded]': '_datepicker?.opened || "false"',
|
||||
'[attr.aria-haspopup]': 'true',
|
||||
'[attr.aria-owns]': '_datepicker?.id',
|
||||
'[attr.min]': 'min ? _dateAdapter.getISODateString(min) : null',
|
||||
'[attr.max]': 'max ? _dateAdapter.getISODateString(max) : null',
|
||||
'(input)': '_onInput($event.target.value)',
|
||||
'(blur)': '_onTouched()',
|
||||
'(keydown)': '_onKeydown($event)',
|
||||
}
|
||||
})
|
||||
export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAccessor, OnDestroy,
|
||||
Validator {
|
||||
/** The datepicker that this input is associated with. */
|
||||
@Input()
|
||||
set mdDatepicker(value: MdDatepicker<D>) {
|
||||
if (value) {
|
||||
this._datepicker = value;
|
||||
this._datepicker._registerInput(this);
|
||||
}
|
||||
}
|
||||
_datepicker: MdDatepicker<D>;
|
||||
|
||||
@Input() set matDatepicker(value: MdDatepicker<D>) { this.mdDatepicker = value; }
|
||||
|
||||
@Input() set mdDatepickerFilter(filter: (date: D | null) => boolean) {
|
||||
this._dateFilter = filter;
|
||||
this._validatorOnChange();
|
||||
}
|
||||
_dateFilter: (date: D | null) => boolean;
|
||||
|
||||
@Input() set matDatepickerFilter(filter: (date: D | null) => boolean) {
|
||||
this.mdDatepickerFilter = filter;
|
||||
}
|
||||
|
||||
/** The value of the input. */
|
||||
@Input()
|
||||
get value(): D {
|
||||
return this._dateAdapter.parse(this._elementRef.nativeElement.value,
|
||||
this._dateFormats.parse.dateInput);
|
||||
}
|
||||
set value(value: D) {
|
||||
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
|
||||
let oldDate = this.value;
|
||||
this._renderer.setProperty(this._elementRef.nativeElement, 'value',
|
||||
date ? this._dateAdapter.format(date, this._dateFormats.display.dateInput) : '');
|
||||
if (!this._dateAdapter.sameDate(oldDate, date)) {
|
||||
this._valueChange.emit(date);
|
||||
}
|
||||
}
|
||||
|
||||
/** The minimum valid date. */
|
||||
@Input()
|
||||
get min(): D { return this._min; }
|
||||
set min(value: D) {
|
||||
this._min = value;
|
||||
this._validatorOnChange();
|
||||
}
|
||||
private _min: D;
|
||||
|
||||
/** The maximum valid date. */
|
||||
@Input()
|
||||
get max(): D { return this._max; }
|
||||
set max(value: D) {
|
||||
this._max = value;
|
||||
this._validatorOnChange();
|
||||
}
|
||||
private _max: D;
|
||||
|
||||
/** Emits when the value changes (either due to user input or programmatic change). */
|
||||
_valueChange = new EventEmitter<D>();
|
||||
|
||||
_onTouched = () => {};
|
||||
|
||||
private _cvaOnChange = (value: any) => {};
|
||||
|
||||
private _validatorOnChange = () => {};
|
||||
|
||||
private _datepickerSubscription: Subscription;
|
||||
|
||||
/** The form control validator for the min date. */
|
||||
private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
|
||||
return (!this.min || !control.value ||
|
||||
this._dateAdapter.compareDate(this.min, control.value) < 0) ?
|
||||
null : {'mdDatepickerMin': {'min': this.min, 'actual': control.value}};
|
||||
}
|
||||
|
||||
/** The form control validator for the max date. */
|
||||
private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
|
||||
return (!this.max || !control.value ||
|
||||
this._dateAdapter.compareDate(this.max, control.value) > 0) ?
|
||||
null : {'mdDatepickerMax': {'max': this.max, 'actual': control.value}};
|
||||
}
|
||||
|
||||
/** The form control validator for the date filter. */
|
||||
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
|
||||
return !this._dateFilter || !control.value || this._dateFilter(control.value) ?
|
||||
null : {'mdDatepickerFilter': true};
|
||||
}
|
||||
|
||||
/** The combined form control validator for this input. */
|
||||
private _validator: ValidatorFn =
|
||||
Validators.compose([this._minValidator, this._maxValidator, this._filterValidator]);
|
||||
|
||||
constructor(
|
||||
private _elementRef: ElementRef,
|
||||
private _renderer: Renderer2,
|
||||
@Optional() private _dateAdapter: DateAdapter<D>,
|
||||
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats,
|
||||
@Optional() private _mdInputContainer: MdInputContainer) {
|
||||
if (!this._dateAdapter) {
|
||||
throw createMissingDateImplError('DateAdapter');
|
||||
}
|
||||
if (!this._dateFormats) {
|
||||
throw createMissingDateImplError('MD_DATE_FORMATS');
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
if (this._datepicker) {
|
||||
this._datepickerSubscription =
|
||||
this._datepicker.selectedChanged.subscribe((selected: D) => {
|
||||
this.value = selected;
|
||||
this._cvaOnChange(selected);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this._datepickerSubscription) {
|
||||
this._datepickerSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
registerOnValidatorChange(fn: () => void): void {
|
||||
this._validatorOnChange = fn;
|
||||
}
|
||||
|
||||
validate(c: AbstractControl): ValidationErrors | null {
|
||||
return this._validator ? this._validator(c) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the element that the datepicker popup should be connected to.
|
||||
* @return The element to connect the popup to.
|
||||
*/
|
||||
getPopupConnectionElementRef(): ElementRef {
|
||||
return this._mdInputContainer ? this._mdInputContainer.underlineRef : this._elementRef;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
writeValue(value: D): void {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
registerOnChange(fn: (value: any) => void): void {
|
||||
this._cvaOnChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor
|
||||
setDisabledState(disabled: boolean): void {
|
||||
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', disabled);
|
||||
}
|
||||
|
||||
_onKeydown(event: KeyboardEvent) {
|
||||
if (event.altKey && event.keyCode === DOWN_ARROW) {
|
||||
this._datepicker.open();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_onInput(value: string) {
|
||||
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
|
||||
this._cvaOnChange(date);
|
||||
this._valueChange.emit(date);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
|
||||
/** Datepicker data that requires internationalization. */
|
||||
@Injectable()
|
||||
export class MdDatepickerIntl {
|
||||
/** A label for the calendar popup (used by screen readers). */
|
||||
calendarLabel = 'Calendar';
|
||||
|
||||
/** A label for the button used to open the calendar popup (used by screen readers). */
|
||||
openCalendarLabel = 'Open calendar';
|
||||
|
||||
/** A label for the previous month button (used by screen readers). */
|
||||
prevMonthLabel = 'Previous month';
|
||||
|
||||
/** A label for the next month button (used by screen readers). */
|
||||
nextMonthLabel = 'Next month';
|
||||
|
||||
/** A label for the previous year button (used by screen readers). */
|
||||
prevYearLabel = 'Previous year';
|
||||
|
||||
/** A label for the next year button (used by screen readers). */
|
||||
nextYearLabel = 'Next year';
|
||||
|
||||
/** A label for the 'switch to month view' button (used by screen readers). */
|
||||
switchToMonthViewLabel = 'Change to month view';
|
||||
|
||||
/** A label for the 'switch to year view' button (used by screen readers). */
|
||||
switchToYearViewLabel = 'Change to year view';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
$mat-datepicker-toggle-icon-size: 24px !default;
|
||||
|
||||
|
||||
.mat-datepicker-toggle {
|
||||
display: inline-block;
|
||||
// Note: SVG needs to be base64 encoded or it will not work on IE11.
|
||||
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE5IDNoLTFWMWgtMnYySDhWMUg2djJINWMtMS4xMSAwLTEuOTkuOS0xLjk5IDJMMyAxOWMwIDEuMS44OSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWOGgxNHYxMXpNNyAxMGg1djVIN3oiLz48L3N2Zz4=') no-repeat;
|
||||
background-size: contain;
|
||||
height: $mat-datepicker-toggle-icon-size;
|
||||
width: $mat-datepicker-toggle-icon-size;
|
||||
border: none;
|
||||
outline: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
|
||||
import {MdDatepicker} from './datepicker';
|
||||
import {MdDatepickerIntl} from './datepicker-intl';
|
||||
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'button[mdDatepickerToggle], button[matDatepickerToggle]',
|
||||
template: '',
|
||||
styleUrls: ['datepicker-toggle.css'],
|
||||
host: {
|
||||
'[class.mat-datepicker-toggle]': 'true',
|
||||
'[attr.aria-label]': '_intl.openCalendarLabel',
|
||||
'(click)': '_open($event)',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdDatepickerToggle<D> {
|
||||
@Input('mdDatepickerToggle') datepicker: MdDatepicker<D>;
|
||||
|
||||
@Input('matDatepickerToggle')
|
||||
get _datepicker() { return this.datepicker; }
|
||||
set _datepicker(v: MdDatepicker<D>) { this.datepicker = v; }
|
||||
|
||||
constructor(public _intl: MdDatepickerIntl) {}
|
||||
|
||||
_open(event: Event): void {
|
||||
if (this.datepicker) {
|
||||
this.datepicker.open();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
The datepicker allows users to enter a date either through text input, or by choosing a date from
|
||||
the calendar. It is made up of several components and directives that work together:
|
||||
|
||||
<!-- TODO: INSERT OVERVIEW EXAMPLE HERE -->
|
||||
|
||||
### Current state
|
||||
Currently the datepicker is in the beginning stages and supports basic date selection functionality.
|
||||
There are many more features that will be added in future iterations, including:
|
||||
* Support for datetimes (e.g. May 2, 2017 at 12:30pm) and month + year only (e.g. May 2017)
|
||||
* Support for selecting and displaying date ranges
|
||||
* Support for custom time zones
|
||||
* Infinite scrolling through calendar months
|
||||
* Built in support for [Moment.js](https://momentjs.com/) dates
|
||||
|
||||
### Connecting a datepicker to an input
|
||||
A datepicker is composed of a text input and a calendar pop-up, connected via the `mdDatepicker`
|
||||
property on the text input.
|
||||
|
||||
```html
|
||||
<input [mdDatepicker]="myDatepicker">
|
||||
<md-datepicker #myDatepicker></md-datepicker>
|
||||
```
|
||||
|
||||
An optional datepicker toggle button is available. A toggle can be added to the example above:
|
||||
|
||||
```html
|
||||
<input [mdDatepicker]="myDatepicker">
|
||||
<button [mdDatepickerToggle]="myDatepicker"></button>
|
||||
<md-datepicker #myDatepicker></md-datepicker>
|
||||
```
|
||||
|
||||
This works exactly the same with an input that is part of an `<md-input-container>` and the toggle
|
||||
can easily be used as a prefix or suffix on the material input:
|
||||
|
||||
```html
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="myDatepicker">
|
||||
<button mdSuffix [mdDatepickerToggle]="myDatepicker"></button>
|
||||
</md-input-container>
|
||||
<md-datepicker #myDatepicker></md-datepicker>
|
||||
```
|
||||
|
||||
### Setting the calendar starting view
|
||||
By default the calendar will open in month view, this can be changed by setting the `startView`
|
||||
property of `md-datepicker` to `"year"`. In year view the user will see all months of the year and
|
||||
then proceed to month view after choosing a month.
|
||||
|
||||
The month or year that the calendar opens to is determined by first checking if any date is
|
||||
currently selected, if so it will open to the month or year containing that date. Otherwise it will
|
||||
open to the month or year containing today's date. This behavior can be overridden by using the
|
||||
`startAt` property of `md-datepicker`. In this case the calendar will open to the month or year
|
||||
containing the `startAt` date.
|
||||
|
||||
```ts
|
||||
startDate = new Date(1990, 0, 1);
|
||||
```
|
||||
|
||||
```html
|
||||
...
|
||||
<md-datepicker startView="year" [startAt]="startDate"></md-datepicker>
|
||||
```
|
||||
|
||||
### Date validation
|
||||
There are three properties that add date validation to the datepicker input. The first two are the
|
||||
`min` and `max` properties. In addition to enforcing validation on the input, these properties will
|
||||
disable all dates on the calendar popup before or after the respective values and prevent the user
|
||||
from advancing the calendar past the `month` or `year` (depending on current view) containing the
|
||||
`min` or `max` date.
|
||||
|
||||
The second way to add date validation is using the `mdDatepickerFilter` property of the datepicker
|
||||
input. This property accepts a function of `<D> => boolean` (where `<D>` is the date type used by
|
||||
the datepicker, see section on
|
||||
[choosing a date implementation](#choosing-a-date-implementation-and-date-format-settings)).
|
||||
A result of `true` indicates that the date is valid and a result of `false` indicates that it is
|
||||
not. Again this will also disable the dates on the calendar that are invalid. However, one important
|
||||
difference between using `mdDatepickerFilter` vs using `min` or `max` is that filtering out all
|
||||
dates before or after a certain point, will not prevent the user from advancing the calendar past
|
||||
that point.
|
||||
|
||||
```ts
|
||||
myFilter = (d: Date) => d.getFullYear() > 2005
|
||||
minDate = new Date(2000, 0, 1);
|
||||
maxDate = new Date(2020, 11, 31);
|
||||
```
|
||||
|
||||
```html
|
||||
<input [mdDatepicker]="d" [mdDatepickerFilter]="myFilter" [min]="minDate" [max]="maxDate" ngModel>
|
||||
<md-datepicker #d></md-datepicker>
|
||||
```
|
||||
|
||||
In this example the user can back past 2005, but all of the dates before then will be unselectable.
|
||||
They will not be able to go further back in the calendar than 2000. If they manually type in a date
|
||||
that is before the min, after the max, or filtered out, the input will have validation errors.
|
||||
|
||||
Each validation property has a different error that can be checked:
|
||||
* A value that violates the `min` property will have a `mdDatepickerMin` error.
|
||||
* A value that violates the `max` property will have a `mdDatepickerMax` error.
|
||||
* A value that violates the `mdDatepickerFilter` property will have a `mdDatepickerFilter` error.
|
||||
|
||||
### Touch UI mode
|
||||
The datepicker normally opens as a popup under the input. However this is not ideal for touch
|
||||
devices that don't have as much screen real estate and need bigger click targets. For this reason
|
||||
`md-datepicker` has a `touchUi` property that can be set to `true` in order to enable a more touch
|
||||
friendly UI where the calendar opens in a large dialog.
|
||||
|
||||
### Manually opening and closing the calendar
|
||||
The calendar popup can be programmatically controlled using the `open` and `close` methods on the
|
||||
`md-datepicker`. It also has an `opened` property that reflects the status of the popup.
|
||||
|
||||
```ts
|
||||
@Component({...})
|
||||
export class MyComponent implements AfterViewInit {
|
||||
@ViewChild(MdDatepicker) dp: MdDatepicker<Date>;
|
||||
|
||||
ngAfterViewInit() {
|
||||
dp.open();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Choosing a date implementation and date format settings
|
||||
The datepicker was built to be date implementation agnostic. This means that it can be made to work
|
||||
with a variety of different date implementations. However it also means that developers need to make
|
||||
sure to provide the appropriate pieces for the datepicker to work with their chosen implementation.
|
||||
The easiest way to ensure this is just to import one of the pre-made modules (currently
|
||||
`MdNativeDateModule` is the only implementation that ships with material, but there are plans to add
|
||||
a module for Moment.js support):
|
||||
* `MdNativeDateModule` - support for native JavaScript Date object
|
||||
|
||||
These modules include providers for `DateAdapter` and `MD_DATE_FORMATS`
|
||||
|
||||
```ts
|
||||
@NgModule({
|
||||
imports: [MdDatepickerModule, MdNativeDateModule],
|
||||
})
|
||||
export class MyApp {}
|
||||
```
|
||||
|
||||
Because `DateAdapter` is a generic class, `MdDatepicker` and `MdDatepickerInput` also need to be
|
||||
made generic. When working with these classes (for example as a `ViewChild`) you should include the
|
||||
appropriate generic type that corresponds to the `DateAdapter` implementation you are using. For
|
||||
example:
|
||||
|
||||
```ts
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
@ViewChild(MdDatepicker) datepicker: MdDatepicker<Date>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Customizing the date implementation
|
||||
The datepicker does all of its interaction with date objects via the `DateAdapter`. Making the
|
||||
datepicker work with a different date implementation is as easy as extending `DateAdapter`, and
|
||||
using your subclass as the provider. You will also want to make sure that the `MD_DATE_FORMATS`
|
||||
provided in your app are formats that can be understood by your date implementation.
|
||||
|
||||
```ts
|
||||
@NgModule({
|
||||
imports: [MdDatepickerModule],
|
||||
providers: [
|
||||
{provide: DateAdapter, useClass: MyDateAdapter},
|
||||
{provide: MD_DATE_FORMATS, useValue: MY_DATE_FORMATS},
|
||||
],
|
||||
})
|
||||
export class MyApp {}
|
||||
```
|
||||
|
||||
#### Customizing the parse and display formats
|
||||
The `MD_DATE_FORMATS` object is just a collection of formats that the datepicker uses when parsing
|
||||
and displaying dates. These formats are passed through to the `DateAdapter` so you will want to make
|
||||
sure that the format objects you're using are compatible with the `DateAdapter` used in your app.
|
||||
This example shows how to use the native `Date` implementation from material, but with custom
|
||||
formats.
|
||||
|
||||
```ts
|
||||
@NgModule({
|
||||
imports: [MdDatepickerModule],
|
||||
providers: [
|
||||
{provide: DateAdapter, useClass: NativeDateAdapter},
|
||||
{provide: MD_DATE_FORMATS, useValue: MY_NATIVE_DATE_FORMATS},
|
||||
],
|
||||
})
|
||||
export class MyApp {}
|
||||
```
|
||||
|
||||
### Localizing labels and messages
|
||||
The various text strings used by the datepicker are provided through `MdDatepickerIntl`.
|
||||
Localization of these messages can be done by providing a subclass with translated values in your
|
||||
application root module.
|
||||
|
||||
```ts
|
||||
@NgModule({
|
||||
imports: [MdDatepickerModule, MdNativeDateModule],
|
||||
providers: [
|
||||
{provide: MdDatepickerIntl, useClass: MyIntl},
|
||||
],
|
||||
})
|
||||
export class MyApp {}
|
||||
```
|
||||
@@ -0,0 +1,626 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {MdDatepickerModule} from './index';
|
||||
import {Component, ViewChild} from '@angular/core';
|
||||
import {MdDatepicker} from './datepicker';
|
||||
import {MdDatepickerInput} from './datepicker-input';
|
||||
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {dispatchFakeEvent, dispatchMouseEvent} from '../core/testing/dispatch-events';
|
||||
import {MdInputModule} from '../input/index';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MdNativeDateModule} from '../core/datetime/index';
|
||||
|
||||
|
||||
// When constructing a Date, the month is zero-based. This can be confusing, since people are
|
||||
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
|
||||
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
|
||||
NOV = 10, DEC = 11;
|
||||
|
||||
|
||||
describe('MdDatepicker', () => {
|
||||
describe('with MdNativeDateModule', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
MdDatepickerModule,
|
||||
MdInputModule,
|
||||
MdNativeDateModule,
|
||||
NoopAnimationsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
declarations: [
|
||||
DatepickerWithFilterAndValidation,
|
||||
DatepickerWithFormControl,
|
||||
DatepickerWithMinAndMax,
|
||||
DatepickerWithNgModel,
|
||||
DatepickerWithStartAt,
|
||||
DatepickerWithToggle,
|
||||
InputContainerDatepicker,
|
||||
MultiInputDatepicker,
|
||||
NoInputDatepicker,
|
||||
StandardDatepicker,
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('standard datepicker', () => {
|
||||
let fixture: ComponentFixture<StandardDatepicker>;
|
||||
let testComponent: StandardDatepicker;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(StandardDatepicker);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('open non-touch should open popup', async(() => {
|
||||
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
|
||||
}));
|
||||
|
||||
it('open touch should open dialog', async(() => {
|
||||
testComponent.touch = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).not.toBeNull();
|
||||
}));
|
||||
|
||||
it('close should close popup', async(() => {
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
let popup = document.querySelector('.cdk-overlay-pane');
|
||||
expect(popup).not.toBeNull();
|
||||
expect(parseInt(getComputedStyle(popup).height)).not.toBe(0);
|
||||
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(parseInt(getComputedStyle(popup).height)).toBe(0);
|
||||
});
|
||||
}));
|
||||
|
||||
it('close should close dialog', async(() => {
|
||||
testComponent.touch = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).not.toBeNull();
|
||||
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('setting selected should update input and close calendar', async(() => {
|
||||
testComponent.touch = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).not.toBeNull();
|
||||
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
|
||||
|
||||
let cells = document.querySelectorAll('.mat-calendar-body-cell');
|
||||
dispatchMouseEvent(cells[1], 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
|
||||
});
|
||||
}));
|
||||
|
||||
it('startAt should fallback to input value', () => {
|
||||
expect(testComponent.datepicker.startAt).toEqual(new Date(2020, JAN, 1));
|
||||
});
|
||||
|
||||
it('should attach popup to native input', () => {
|
||||
let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef();
|
||||
expect(attachToRef.nativeElement.tagName.toLowerCase())
|
||||
.toBe('input', 'popup should be attached to native input');
|
||||
});
|
||||
});
|
||||
|
||||
describe('datepicker with too many inputs', () => {
|
||||
it('should throw when multiple inputs registered', async(() => {
|
||||
let fixture = TestBed.createComponent(MultiInputDatepicker);
|
||||
expect(() => fixture.detectChanges()).toThrow();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('datepicker with no inputs', () => {
|
||||
let fixture: ComponentFixture<NoInputDatepicker>;
|
||||
let testComponent: NoInputDatepicker;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(NoInputDatepicker);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should throw when opened with no registered inputs', async(() => {
|
||||
expect(() => testComponent.datepicker.open()).toThrow();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('datepicker with startAt', () => {
|
||||
let fixture: ComponentFixture<DatepickerWithStartAt>;
|
||||
let testComponent: DatepickerWithStartAt;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(DatepickerWithStartAt);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('explicit startAt should override input value', () => {
|
||||
expect(testComponent.datepicker.startAt).toEqual(new Date(2010, JAN, 1));
|
||||
});
|
||||
});
|
||||
|
||||
describe('datepicker with ngModel', () => {
|
||||
let fixture: ComponentFixture<DatepickerWithNgModel>;
|
||||
let testComponent: DatepickerWithNgModel;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(DatepickerWithNgModel);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should update datepicker when model changes', async(() => {
|
||||
expect(testComponent.datepickerInput.value).toBeNull();
|
||||
expect(testComponent.datepicker._selected).toBeNull();
|
||||
|
||||
let selected = new Date(2017, JAN, 1);
|
||||
testComponent.selected = selected;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.datepickerInput.value).toEqual(selected);
|
||||
expect(testComponent.datepicker._selected).toEqual(selected);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should update model when date is selected', async(() => {
|
||||
expect(testComponent.selected).toBeNull();
|
||||
expect(testComponent.datepickerInput.value).toBeNull();
|
||||
|
||||
let selected = new Date(2017, JAN, 1);
|
||||
testComponent.datepicker._selectAndClose(selected);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.selected).toEqual(selected);
|
||||
expect(testComponent.datepickerInput.value).toEqual(selected);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should mark input dirty after input event', () => {
|
||||
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(inputEl.classList).toContain('ng-pristine');
|
||||
|
||||
dispatchFakeEvent(inputEl, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inputEl.classList).toContain('ng-dirty');
|
||||
});
|
||||
|
||||
it('should mark input dirty after date selected', async(() => {
|
||||
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(inputEl.classList).toContain('ng-pristine');
|
||||
|
||||
testComponent.datepicker._selectAndClose(new Date(2017, JAN, 1));
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inputEl.classList).toContain('ng-dirty');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not mark dirty after model change', async(() => {
|
||||
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(inputEl.classList).toContain('ng-pristine');
|
||||
|
||||
testComponent.selected = new Date(2017, JAN, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inputEl.classList).toContain('ng-pristine');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should mark input touched on blur', () => {
|
||||
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(inputEl.classList).toContain('ng-untouched');
|
||||
|
||||
dispatchFakeEvent(inputEl, 'focus');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inputEl.classList).toContain('ng-untouched');
|
||||
|
||||
dispatchFakeEvent(inputEl, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inputEl.classList).toContain('ng-touched');
|
||||
});
|
||||
});
|
||||
|
||||
describe('datepicker with formControl', () => {
|
||||
let fixture: ComponentFixture<DatepickerWithFormControl>;
|
||||
let testComponent: DatepickerWithFormControl;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(DatepickerWithFormControl);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should update datepicker when formControl changes', () => {
|
||||
expect(testComponent.datepickerInput.value).toBeNull();
|
||||
expect(testComponent.datepicker._selected).toBeNull();
|
||||
|
||||
let selected = new Date(2017, JAN, 1);
|
||||
testComponent.formControl.setValue(selected);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.datepickerInput.value).toEqual(selected);
|
||||
expect(testComponent.datepicker._selected).toEqual(selected);
|
||||
});
|
||||
|
||||
it('should update formControl when date is selected', () => {
|
||||
expect(testComponent.formControl.value).toBeNull();
|
||||
expect(testComponent.datepickerInput.value).toBeNull();
|
||||
|
||||
let selected = new Date(2017, JAN, 1);
|
||||
testComponent.datepicker._selectAndClose(selected);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.formControl.value).toEqual(selected);
|
||||
expect(testComponent.datepickerInput.value).toEqual(selected);
|
||||
});
|
||||
|
||||
it('should disable input when form control disabled', () => {
|
||||
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
expect(inputEl.disabled).toBe(false);
|
||||
|
||||
testComponent.formControl.disable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inputEl.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('datepicker with mdDatepickerToggle', () => {
|
||||
let fixture: ComponentFixture<DatepickerWithToggle>;
|
||||
let testComponent: DatepickerWithToggle;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(DatepickerWithToggle);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should open calendar when toggle clicked', async(() => {
|
||||
expect(document.querySelector('md-dialog-container')).toBeNull();
|
||||
|
||||
let toggle = fixture.debugElement.query(By.css('button'));
|
||||
dispatchMouseEvent(toggle.nativeElement, 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).not.toBeNull();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('datepicker inside input-container', () => {
|
||||
let fixture: ComponentFixture<InputContainerDatepicker>;
|
||||
let testComponent: InputContainerDatepicker;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(InputContainerDatepicker);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should attach popup to input-container underline', () => {
|
||||
let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef();
|
||||
expect(attachToRef.nativeElement.classList.contains('mat-input-underline'))
|
||||
.toBe(true, 'popup should be attached to input-container underline');
|
||||
});
|
||||
});
|
||||
|
||||
describe('datepicker with min and max dates', () => {
|
||||
let fixture: ComponentFixture<DatepickerWithMinAndMax>;
|
||||
let testComponent: DatepickerWithMinAndMax;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(DatepickerWithMinAndMax);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should use min and max dates specified by the input', () => {
|
||||
expect(testComponent.datepicker._minDate).toEqual(new Date(2010, JAN, 1));
|
||||
expect(testComponent.datepicker._maxDate).toEqual(new Date(2020, JAN, 1));
|
||||
});
|
||||
});
|
||||
|
||||
describe('datepicker with filter and validation', () => {
|
||||
let fixture: ComponentFixture<DatepickerWithFilterAndValidation>;
|
||||
let testComponent: DatepickerWithFilterAndValidation;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(DatepickerWithFilterAndValidation);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
testComponent.datepicker.close();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should mark input invalid', async(() => {
|
||||
testComponent.date = new Date(2017, JAN, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('input')).nativeElement.classList)
|
||||
.toContain('ng-invalid');
|
||||
|
||||
testComponent.date = new Date(2017, JAN, 2);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('input')).nativeElement.classList)
|
||||
.not.toContain('ng-invalid');
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should disable filtered calendar cells', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent.datepicker.open();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(document.querySelector('md-dialog-container')).not.toBeNull();
|
||||
|
||||
let cells = document.querySelectorAll('.mat-calendar-body-cell');
|
||||
expect(cells[0].classList).toContain('mat-calendar-body-disabled');
|
||||
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with missing DateAdapter and MD_DATE_FORMATS', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
MdDatepickerModule,
|
||||
MdInputModule,
|
||||
NoopAnimationsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
declarations: [StandardDatepicker],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
it('should throw when created', () => {
|
||||
expect(() => TestBed.createComponent(StandardDatepicker))
|
||||
.toThrowError(/MdDatepicker: No provider found for .*/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [mdDatepicker]="d" [value]="date">
|
||||
<md-datepicker #d [touchUi]="touch"></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class StandardDatepicker {
|
||||
touch = false;
|
||||
date = new Date(2020, JAN, 1);
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [mdDatepicker]="d"><input [mdDatepicker]="d"><md-datepicker #d></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class MultiInputDatepicker {}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<md-datepicker #d></md-datepicker>`,
|
||||
})
|
||||
class NoInputDatepicker {
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [mdDatepicker]="d" [value]="date">
|
||||
<md-datepicker #d [startAt]="startDate"></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class DatepickerWithStartAt {
|
||||
date = new Date(2020, JAN, 1);
|
||||
startDate = new Date(2010, JAN, 1);
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<input [(ngModel)]="selected" [mdDatepicker]="d"><md-datepicker #d></md-datepicker>`,
|
||||
})
|
||||
class DatepickerWithNgModel {
|
||||
selected: Date = null;
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [formControl]="formControl" [mdDatepicker]="d">
|
||||
<md-datepicker #d></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class DatepickerWithFormControl {
|
||||
formControl = new FormControl();
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [mdDatepicker]="d">
|
||||
<button [mdDatepickerToggle]="d"></button>
|
||||
<md-datepicker #d [touchUi]="true"></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class DatepickerWithToggle {
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-input-container>
|
||||
<input mdInput [mdDatepicker]="d">
|
||||
<md-datepicker #d></md-datepicker>
|
||||
</md-input-container>
|
||||
`,
|
||||
})
|
||||
class InputContainerDatepicker {
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [mdDatepicker]="d" [min]="minDate" [max]="maxDate">
|
||||
<md-datepicker #d></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class DatepickerWithMinAndMax {
|
||||
minDate = new Date(2010, JAN, 1);
|
||||
maxDate = new Date(2020, JAN, 1);
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<input [mdDatepicker]="d" [(ngModel)]="date" [mdDatepickerFilter]="filter">
|
||||
<button [mdDatepickerToggle]="d"></button>
|
||||
<md-datepicker #d [touchUi]="true"></md-datepicker>
|
||||
`,
|
||||
})
|
||||
class DatepickerWithFilterAndValidation {
|
||||
@ViewChild('d') datepicker: MdDatepicker<Date>;
|
||||
date: Date;
|
||||
filter = (date: Date) => date.getDate() != 1;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ComponentRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Optional,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {Overlay} from '../core/overlay/overlay';
|
||||
import {OverlayRef} from '../core/overlay/overlay-ref';
|
||||
import {ComponentPortal} from '../core/portal/portal';
|
||||
import {OverlayState} from '../core/overlay/overlay-state';
|
||||
import {Dir} from '../core/rtl/dir';
|
||||
import {MdError} from '../core/errors/error';
|
||||
import {MdDialog} from '../dialog/dialog';
|
||||
import {MdDialogRef} from '../dialog/dialog-ref';
|
||||
import {PositionStrategy} from '../core/overlay/position/position-strategy';
|
||||
import {
|
||||
OriginConnectionPosition,
|
||||
OverlayConnectionPosition
|
||||
} from '../core/overlay/position/connected-position';
|
||||
import {MdDatepickerInput} from './datepicker-input';
|
||||
import 'rxjs/add/operator/first';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {MdDialogConfig} from '../dialog/dialog-config';
|
||||
import {DateAdapter} from '../core/datetime/index';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {ESCAPE} from '../core/keyboard/keycodes';
|
||||
import {MdCalendar} from './calendar';
|
||||
|
||||
|
||||
/** Used to generate a unique ID for each datepicker instance. */
|
||||
let datepickerUid = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Component used as the content for the datepicker dialog and popup. We use this instead of using
|
||||
* MdCalendar directly as the content so we can control the initial focus. This also gives us a
|
||||
* place to put additional features of the popup that are not part of the calendar itself in the
|
||||
* future. (e.g. confirmation buttons).
|
||||
* @docs-internal
|
||||
*/
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-datepicker-content',
|
||||
templateUrl: 'datepicker-content.html',
|
||||
styleUrls: ['datepicker-content.css'],
|
||||
host: {
|
||||
'class': 'mat-datepicker-content',
|
||||
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
|
||||
'(keydown)': '_handleKeydown($event)',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdDatepickerContent<D> implements AfterContentInit {
|
||||
datepicker: MdDatepicker<D>;
|
||||
|
||||
@ViewChild(MdCalendar) _calendar: MdCalendar<D>;
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._calendar._focusActiveCell();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keydown event on datepicker content.
|
||||
* @param event The event.
|
||||
*/
|
||||
_handleKeydown(event: KeyboardEvent): void {
|
||||
switch (event.keyCode) {
|
||||
case ESCAPE:
|
||||
this.datepicker.close();
|
||||
break;
|
||||
default:
|
||||
// Return so that we don't preventDefault on keys that are not explicitly handled.
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO(mmalerba): We use a component instead of a directive here so the user can use implicit
|
||||
// template reference variables (e.g. #d vs #d="mdDatepicker"). We can change this to a directive if
|
||||
// angular adds support for `exportAs: '$implicit'` on directives.
|
||||
/** Component responsible for managing the datepicker popup/dialog. */
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-datepicker, mat-datepicker',
|
||||
template: '',
|
||||
})
|
||||
export class MdDatepicker<D> implements OnDestroy {
|
||||
/** The date to open the calendar to initially. */
|
||||
@Input()
|
||||
get startAt(): D {
|
||||
// If an explicit startAt is set we start there, otherwise we start at whatever the currently
|
||||
// selected value is.
|
||||
return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null);
|
||||
}
|
||||
set startAt(date: D) { this._startAt = date; }
|
||||
private _startAt: D;
|
||||
|
||||
/** The view that the calendar should start in. */
|
||||
@Input() startView: 'month' | 'year' = 'month';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Input() touchUi = false;
|
||||
|
||||
/** Emits new selected date when selected date changes. */
|
||||
@Output() selectedChanged = new EventEmitter<D>();
|
||||
|
||||
/** Whether the calendar is open. */
|
||||
opened = false;
|
||||
|
||||
/** The id for the datepicker calendar. */
|
||||
id = `md-datepicker-${datepickerUid++}`;
|
||||
|
||||
/** The currently selected date. */
|
||||
_selected: D = null;
|
||||
|
||||
/** The minimum selectable date. */
|
||||
get _minDate(): D {
|
||||
return this._datepickerInput && this._datepickerInput.min;
|
||||
}
|
||||
|
||||
/** The maximum selectable date. */
|
||||
get _maxDate(): D {
|
||||
return this._datepickerInput && this._datepickerInput.max;
|
||||
}
|
||||
|
||||
get _dateFilter(): (date: D | null) => boolean {
|
||||
return this._datepickerInput && this._datepickerInput._dateFilter;
|
||||
}
|
||||
|
||||
/** A reference to the overlay when the calendar is opened as a popup. */
|
||||
private _popupRef: OverlayRef;
|
||||
|
||||
/** A reference to the dialog when the calendar is opened as a dialog. */
|
||||
private _dialogRef: MdDialogRef<any>;
|
||||
|
||||
/** A portal containing the calendar for this datepicker. */
|
||||
private _calendarPortal: ComponentPortal<MdDatepickerContent<D>>;
|
||||
|
||||
/** The input element this datepicker is associated with. */
|
||||
private _datepickerInput: MdDatepickerInput<D>;
|
||||
|
||||
private _inputSubscription: Subscription;
|
||||
|
||||
constructor(private _dialog: MdDialog, private _overlay: Overlay,
|
||||
private _viewContainerRef: ViewContainerRef,
|
||||
@Optional() private _dateAdapter: DateAdapter<D>,
|
||||
@Optional() private _dir: Dir) {
|
||||
if (!this._dateAdapter) {
|
||||
throw createMissingDateImplError('DateAdapter');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.close();
|
||||
if (this._popupRef) {
|
||||
this._popupRef.dispose();
|
||||
}
|
||||
if (this._inputSubscription) {
|
||||
this._inputSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/** Selects the given date and closes the currently open popup or dialog. */
|
||||
_selectAndClose(date: D): void {
|
||||
let oldValue = this._selected;
|
||||
this._selected = date;
|
||||
if (!this._dateAdapter.sameDate(oldValue, this._selected)) {
|
||||
this.selectedChanged.emit(date);
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an input with this datepicker.
|
||||
* @param input The datepicker input to register with this datepicker.
|
||||
*/
|
||||
_registerInput(input: MdDatepickerInput<D>): void {
|
||||
if (this._datepickerInput) {
|
||||
throw new MdError('An MdDatepicker can only be associated with a single input.');
|
||||
}
|
||||
this._datepickerInput = input;
|
||||
this._inputSubscription =
|
||||
this._datepickerInput._valueChange.subscribe((value: D) => this._selected = value);
|
||||
}
|
||||
|
||||
/** Open the calendar. */
|
||||
open(): void {
|
||||
if (this.opened) {
|
||||
return;
|
||||
}
|
||||
if (!this._datepickerInput) {
|
||||
throw new MdError('Attempted to open an MdDatepicker with no associated input.');
|
||||
}
|
||||
|
||||
this.touchUi ? this._openAsDialog() : this._openAsPopup();
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
/** Close the calendar. */
|
||||
close(): void {
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
if (this._popupRef && this._popupRef.hasAttached()) {
|
||||
this._popupRef.detach();
|
||||
}
|
||||
if (this._dialogRef) {
|
||||
this._dialogRef.close();
|
||||
this._dialogRef = null;
|
||||
}
|
||||
if (this._calendarPortal && this._calendarPortal.isAttached) {
|
||||
this._calendarPortal.detach();
|
||||
}
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
/** Open the calendar as a dialog. */
|
||||
private _openAsDialog(): void {
|
||||
let config = new MdDialogConfig();
|
||||
config.viewContainerRef = this._viewContainerRef;
|
||||
|
||||
this._dialogRef = this._dialog.open(MdDatepickerContent, config);
|
||||
this._dialogRef.afterClosed().first().subscribe(() => this.close());
|
||||
this._dialogRef.componentInstance.datepicker = this;
|
||||
}
|
||||
|
||||
/** Open the calendar as a popup. */
|
||||
private _openAsPopup(): void {
|
||||
if (!this._calendarPortal) {
|
||||
this._calendarPortal = new ComponentPortal(MdDatepickerContent, this._viewContainerRef);
|
||||
}
|
||||
|
||||
if (!this._popupRef) {
|
||||
this._createPopup();
|
||||
}
|
||||
|
||||
if (!this._popupRef.hasAttached()) {
|
||||
let componentRef: ComponentRef<MdDatepickerContent<D>> =
|
||||
this._popupRef.attach(this._calendarPortal);
|
||||
componentRef.instance.datepicker = this;
|
||||
}
|
||||
|
||||
this._popupRef.backdropClick().first().subscribe(() => this.close());
|
||||
}
|
||||
|
||||
/** Create the popup. */
|
||||
private _createPopup(): void {
|
||||
const overlayState = new OverlayState();
|
||||
overlayState.positionStrategy = this._createPopupPositionStrategy();
|
||||
overlayState.hasBackdrop = true;
|
||||
overlayState.backdropClass = 'md-overlay-transparent-backdrop';
|
||||
overlayState.direction = this._dir ? this._dir.value : 'ltr';
|
||||
|
||||
this._popupRef = this._overlay.create(overlayState);
|
||||
}
|
||||
|
||||
/** Create the popup PositionStrategy. */
|
||||
private _createPopupPositionStrategy(): PositionStrategy {
|
||||
let origin = {originX: 'start', originY: 'bottom'} as OriginConnectionPosition;
|
||||
let overlay = {overlayX: 'start', overlayY: 'top'} as OverlayConnectionPosition;
|
||||
return this._overlay.position().connectedTo(
|
||||
this._datepickerInput.getPopupConnectionElementRef(), origin, overlay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MdMonthView} from './month-view';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {MdCalendarBody} from './calendar-body';
|
||||
import {MdYearView} from './year-view';
|
||||
import {OverlayModule} from '../core/overlay/overlay-directives';
|
||||
import {MdDatepicker, MdDatepickerContent} from './datepicker';
|
||||
import {MdDatepickerInput} from './datepicker-input';
|
||||
import {MdDialogModule} from '../dialog/index';
|
||||
import {MdCalendar} from './calendar';
|
||||
import {MdDatepickerToggle} from './datepicker-toggle';
|
||||
import {StyleModule} from '../core/style/index';
|
||||
import {MdButtonModule} from '../button/index';
|
||||
import {MdDatepickerIntl} from './datepicker-intl';
|
||||
|
||||
|
||||
export * from './calendar';
|
||||
export * from './calendar-body';
|
||||
export * from './datepicker';
|
||||
export * from './datepicker-input';
|
||||
export * from './datepicker-intl';
|
||||
export * from './datepicker-toggle';
|
||||
export * from './month-view';
|
||||
export * from './year-view';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MdButtonModule,
|
||||
MdDialogModule,
|
||||
OverlayModule,
|
||||
StyleModule,
|
||||
],
|
||||
exports: [
|
||||
MdDatepicker,
|
||||
MdDatepickerContent,
|
||||
MdDatepickerInput,
|
||||
MdDatepickerToggle,
|
||||
],
|
||||
declarations: [
|
||||
MdCalendar,
|
||||
MdCalendarBody,
|
||||
MdDatepicker,
|
||||
MdDatepickerContent,
|
||||
MdDatepickerInput,
|
||||
MdDatepickerToggle,
|
||||
MdMonthView,
|
||||
MdYearView,
|
||||
],
|
||||
providers: [
|
||||
MdDatepickerIntl,
|
||||
],
|
||||
entryComponents: [
|
||||
MdDatepickerContent,
|
||||
]
|
||||
})
|
||||
export class MdDatepickerModule {}
|
||||
@@ -0,0 +1,16 @@
|
||||
<table class="mat-calendar-table">
|
||||
<thead class="mat-calendar-table-header">
|
||||
<tr><th *ngFor="let day of _weekdays" [attr.aria-label]="day.long">{{day.narrow}}</th></tr>
|
||||
<tr><th class="mat-calendar-table-header-divider" colspan="7" aria-hidden="true"></th></tr>
|
||||
</thead>
|
||||
<tbody md-calendar-body
|
||||
role="grid"
|
||||
[label]="_monthLabel"
|
||||
[rows]="_weeks"
|
||||
[todayValue]="_todayDate"
|
||||
[selectedValue]="_selectedDate"
|
||||
[labelMinRequiredCells]="3"
|
||||
[activeCell]="_dateAdapter.getDate(activeDate) - 1"
|
||||
(selectedValueChange)="_dateSelected($event)">
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,127 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdMonthView} from './month-view';
|
||||
import {MdCalendarBody} from './calendar-body';
|
||||
import {MdNativeDateModule} from '../core/datetime/index';
|
||||
|
||||
|
||||
// When constructing a Date, the month is zero-based. This can be confusing, since people are
|
||||
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
|
||||
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
|
||||
NOV = 10, DEC = 11;
|
||||
|
||||
|
||||
describe('MdMonthView', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MdNativeDateModule,
|
||||
],
|
||||
declarations: [
|
||||
MdCalendarBody,
|
||||
MdMonthView,
|
||||
|
||||
// Test components.
|
||||
StandardMonthView,
|
||||
MonthViewWithDateFilter,
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('standard month view', () => {
|
||||
let fixture: ComponentFixture<StandardMonthView>;
|
||||
let testComponent: StandardMonthView;
|
||||
let monthViewNativeElement: Element;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StandardMonthView);
|
||||
fixture.detectChanges();
|
||||
|
||||
let monthViewDebugElement = fixture.debugElement.query(By.directive(MdMonthView));
|
||||
monthViewNativeElement = monthViewDebugElement.nativeElement;
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('has correct month label', () => {
|
||||
let labelEl = monthViewNativeElement.querySelector('.mat-calendar-body-label');
|
||||
expect(labelEl.innerHTML.trim()).toBe('JAN');
|
||||
});
|
||||
|
||||
it('has 31 days', () => {
|
||||
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
expect(cellEls.length).toBe(31);
|
||||
});
|
||||
|
||||
it('shows selected date if in same month', () => {
|
||||
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected');
|
||||
expect(selectedEl.innerHTML.trim()).toBe('10');
|
||||
});
|
||||
|
||||
it('does not show selected date if in different month', () => {
|
||||
testComponent.selected = new Date(2017, MAR, 10);
|
||||
fixture.detectChanges();
|
||||
|
||||
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected');
|
||||
expect(selectedEl).toBeNull();
|
||||
});
|
||||
|
||||
it('fires selected change event on cell clicked', () => {
|
||||
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
(cellEls[cellEls.length - 1] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected');
|
||||
expect(selectedEl.innerHTML.trim()).toBe('31');
|
||||
});
|
||||
|
||||
it('should mark active date', () => {
|
||||
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
expect((cellEls[4] as HTMLElement).innerText.trim()).toBe('5');
|
||||
expect(cellEls[4].classList).toContain('mat-calendar-body-active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('month view with date filter', () => {
|
||||
let fixture: ComponentFixture<MonthViewWithDateFilter>;
|
||||
let testComponent: MonthViewWithDateFilter;
|
||||
let monthViewNativeElement: Element;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MonthViewWithDateFilter);
|
||||
fixture.detectChanges();
|
||||
|
||||
let monthViewDebugElement = fixture.debugElement.query(By.directive(MdMonthView));
|
||||
monthViewNativeElement = monthViewDebugElement.nativeElement;
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should disable filtered dates', () => {
|
||||
let cells = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
expect(cells[0].classList).toContain('mat-calendar-body-disabled');
|
||||
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<md-month-view [activeDate]="date" [(selected)]="selected"></md-month-view>`,
|
||||
})
|
||||
class StandardMonthView {
|
||||
date = new Date(2017, JAN, 5);
|
||||
selected = new Date(2017, JAN, 10);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<md-month-view [activeDate]="activeDate" [dateFilter]="dateFilter"></md-month-view>`
|
||||
})
|
||||
class MonthViewWithDateFilter {
|
||||
activeDate = new Date(2017, JAN, 1);
|
||||
dateFilter(date: Date) {
|
||||
return date.getDate() % 2 == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
Optional,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {MdCalendarCell} from './calendar-body';
|
||||
import {DateAdapter} from '../core/datetime/index';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
|
||||
|
||||
|
||||
const DAYS_PER_WEEK = 7;
|
||||
|
||||
|
||||
/**
|
||||
* An internal component used to display a single month in the datepicker.
|
||||
* @docs-private
|
||||
*/
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-month-view',
|
||||
templateUrl: 'month-view.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdMonthView<D> implements AfterContentInit {
|
||||
/**
|
||||
* The date to display in this month view (everything other than the month and year is ignored).
|
||||
*/
|
||||
@Input()
|
||||
get activeDate(): D { return this._activeDate; }
|
||||
set activeDate(value: D) {
|
||||
let oldActiveDate = this._activeDate;
|
||||
this._activeDate = value || this._dateAdapter.today();
|
||||
if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) {
|
||||
this._init();
|
||||
}
|
||||
}
|
||||
private _activeDate: D;
|
||||
|
||||
/** The currently selected date. */
|
||||
@Input()
|
||||
get selected(): D { return this._selected; }
|
||||
set selected(value: D) {
|
||||
this._selected = value;
|
||||
this._selectedDate = this._getDateInCurrentMonth(this.selected);
|
||||
}
|
||||
private _selected: D;
|
||||
|
||||
/** A function used to filter which dates are selectable. */
|
||||
@Input() dateFilter: (date: D) => boolean;
|
||||
|
||||
/** Emits when a new date is selected. */
|
||||
@Output() selectedChange = new EventEmitter<D>();
|
||||
|
||||
/** The label for this month (e.g. "January 2017"). */
|
||||
_monthLabel: string;
|
||||
|
||||
/** Grid of calendar cells representing the dates of the month. */
|
||||
_weeks: MdCalendarCell[][];
|
||||
|
||||
/** The number of blank cells in the first row before the 1st of the month. */
|
||||
_firstWeekOffset: number;
|
||||
|
||||
/**
|
||||
* The date of the month that the currently selected Date falls on.
|
||||
* Null if the currently selected Date is in another month.
|
||||
*/
|
||||
_selectedDate: number;
|
||||
|
||||
/** The date of the month that today falls on. Null if today is in another month. */
|
||||
_todayDate: number;
|
||||
|
||||
/** The names of the weekdays. */
|
||||
_weekdays: {long: string, narrow: string}[];
|
||||
|
||||
constructor(@Optional() public _dateAdapter: DateAdapter<D>,
|
||||
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
|
||||
if (!this._dateAdapter) {
|
||||
throw createMissingDateImplError('DateAdapter');
|
||||
}
|
||||
if (!this._dateFormats) {
|
||||
throw createMissingDateImplError('MD_DATE_FORMATS');
|
||||
}
|
||||
|
||||
const firstDayOfWeek = this._dateAdapter.getFirstDayOfWeek();
|
||||
const narrowWeekdays = this._dateAdapter.getDayOfWeekNames('narrow');
|
||||
const longWeekdays = this._dateAdapter.getDayOfWeekNames('long');
|
||||
|
||||
// Rotate the labels for days of the week based on the configured first day of the week.
|
||||
let weekdays = longWeekdays.map((long, i) => {
|
||||
return {long, narrow: narrowWeekdays[i]};
|
||||
});
|
||||
this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));
|
||||
|
||||
this._activeDate = this._dateAdapter.today();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._init();
|
||||
}
|
||||
|
||||
/** Handles when a new date is selected. */
|
||||
_dateSelected(date: number) {
|
||||
if (this._selectedDate == date) {
|
||||
return;
|
||||
}
|
||||
this.selectedChange.emit(this._dateAdapter.createDate(
|
||||
this._dateAdapter.getYear(this.activeDate), this._dateAdapter.getMonth(this.activeDate),
|
||||
date));
|
||||
}
|
||||
|
||||
/** Initializes this month view. */
|
||||
private _init() {
|
||||
this._selectedDate = this._getDateInCurrentMonth(this.selected);
|
||||
this._todayDate = this._getDateInCurrentMonth(this._dateAdapter.today());
|
||||
this._monthLabel =
|
||||
this._dateAdapter.getMonthNames('short')[this._dateAdapter.getMonth(this.activeDate)]
|
||||
.toLocaleUpperCase();
|
||||
|
||||
let firstOfMonth = this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),
|
||||
this._dateAdapter.getMonth(this.activeDate), 1);
|
||||
this._firstWeekOffset =
|
||||
(DAYS_PER_WEEK + this._dateAdapter.getDayOfWeek(firstOfMonth) -
|
||||
this._dateAdapter.getFirstDayOfWeek()) % DAYS_PER_WEEK;
|
||||
|
||||
this._createWeekCells();
|
||||
}
|
||||
|
||||
/** Creates MdCalendarCells for the dates in this month. */
|
||||
private _createWeekCells() {
|
||||
let daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate);
|
||||
let dateNames = this._dateAdapter.getDateNames();
|
||||
this._weeks = [[]];
|
||||
for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) {
|
||||
if (cell == DAYS_PER_WEEK) {
|
||||
this._weeks.push([]);
|
||||
cell = 0;
|
||||
}
|
||||
let date = this._dateAdapter.createDate(
|
||||
this._dateAdapter.getYear(this.activeDate),
|
||||
this._dateAdapter.getMonth(this.activeDate), i + 1);
|
||||
let enabled = !this.dateFilter ||
|
||||
this.dateFilter(date);
|
||||
let ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
|
||||
this._weeks[this._weeks.length - 1]
|
||||
.push(new MdCalendarCell(i + 1, dateNames[i], ariaLabel, enabled));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the date in this month that the given Date falls on.
|
||||
* Returns null if the given Date is in another month.
|
||||
*/
|
||||
private _getDateInCurrentMonth(date: D): number {
|
||||
return this._hasSameMonthAndYear(date, this.activeDate) ?
|
||||
this._dateAdapter.getDate(date) : null;
|
||||
}
|
||||
|
||||
/** Checks whether the 2 dates are non-null and fall within the same month of the same year. */
|
||||
private _hasSameMonthAndYear(d1: D, d2: D): boolean {
|
||||
return !!(d1 && d2 && this._dateAdapter.getMonth(d1) == this._dateAdapter.getMonth(d2) &&
|
||||
this._dateAdapter.getYear(d1) == this._dateAdapter.getYear(d2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<table class="mat-calendar-table">
|
||||
<thead class="mat-calendar-table-header">
|
||||
<tr><th class="mat-calendar-table-header-divider" colspan="7"></th></tr>
|
||||
</thead>
|
||||
<tbody md-calendar-body
|
||||
role="grid"
|
||||
allowDisabledSelection="true"
|
||||
[label]="_yearLabel"
|
||||
[rows]="_months"
|
||||
[todayValue]="_todayMonth"
|
||||
[selectedValue]="_selectedMonth"
|
||||
[labelMinRequiredCells]="2"
|
||||
[activeCell]="_dateAdapter.getMonth(activeDate)"
|
||||
(selectedValueChange)="_monthSelected($event)">
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,134 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdYearView} from './year-view';
|
||||
import {MdCalendarBody} from './calendar-body';
|
||||
import {MdNativeDateModule} from '../core/datetime/index';
|
||||
|
||||
|
||||
// When constructing a Date, the month is zero-based. This can be confusing, since people are
|
||||
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
|
||||
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
|
||||
NOV = 10, DEC = 11;
|
||||
|
||||
|
||||
describe('MdYearView', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MdNativeDateModule,
|
||||
],
|
||||
declarations: [
|
||||
MdCalendarBody,
|
||||
MdYearView,
|
||||
|
||||
// Test components.
|
||||
StandardYearView,
|
||||
YearViewWithDateFilter,
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('standard year view', () => {
|
||||
let fixture: ComponentFixture<StandardYearView>;
|
||||
let testComponent: StandardYearView;
|
||||
let yearViewNativeElement: Element;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StandardYearView);
|
||||
fixture.detectChanges();
|
||||
|
||||
let yearViewDebugElement = fixture.debugElement.query(By.directive(MdYearView));
|
||||
yearViewNativeElement = yearViewDebugElement.nativeElement;
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('has correct year label', () => {
|
||||
let labelEl = yearViewNativeElement.querySelector('.mat-calendar-body-label');
|
||||
expect(labelEl.innerHTML.trim()).toBe('2017');
|
||||
});
|
||||
|
||||
it('has 12 months', () => {
|
||||
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
expect(cellEls.length).toBe(12);
|
||||
});
|
||||
|
||||
it('shows selected month if in same year', () => {
|
||||
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected');
|
||||
expect(selectedEl.innerHTML.trim()).toBe('MAR');
|
||||
});
|
||||
|
||||
it('does not show selected month if in different year', () => {
|
||||
testComponent.selected = new Date(2016, MAR, 10);
|
||||
fixture.detectChanges();
|
||||
|
||||
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected');
|
||||
expect(selectedEl).toBeNull();
|
||||
});
|
||||
|
||||
it('fires selected change event on cell clicked', () => {
|
||||
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
(cellEls[cellEls.length - 1] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected');
|
||||
expect(selectedEl.innerHTML.trim()).toBe('DEC');
|
||||
});
|
||||
|
||||
it('should mark active date', () => {
|
||||
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('JAN');
|
||||
expect(cellEls[0].classList).toContain('mat-calendar-body-active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('year view with date filter', () => {
|
||||
let fixture: ComponentFixture<YearViewWithDateFilter>;
|
||||
let testComponent: YearViewWithDateFilter;
|
||||
let yearViewNativeElement: Element;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(YearViewWithDateFilter);
|
||||
fixture.detectChanges();
|
||||
|
||||
let yearViewDebugElement = fixture.debugElement.query(By.directive(MdYearView));
|
||||
yearViewNativeElement = yearViewDebugElement.nativeElement;
|
||||
testComponent = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should disabled months with no enabled days', () => {
|
||||
let cells = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
|
||||
expect(cells[0].classList).not.toContain('mat-calendar-body-disabled');
|
||||
expect(cells[1].classList).toContain('mat-calendar-body-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-year-view [activeDate]="date" [(selected)]="selected"></md-year-view>`,
|
||||
})
|
||||
class StandardYearView {
|
||||
date = new Date(2017, JAN, 5);
|
||||
selected = new Date(2017, MAR, 10);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `<md-year-view [activeDate]="activeDate" [dateFilter]="dateFilter"></md-year-view>`
|
||||
})
|
||||
class YearViewWithDateFilter {
|
||||
activeDate = new Date(2017, JAN, 1);
|
||||
dateFilter(date: Date) {
|
||||
if (date.getMonth() == JAN) {
|
||||
return date.getDate() == 10;
|
||||
}
|
||||
if (date.getMonth() == FEB) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
Optional,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {MdCalendarCell} from './calendar-body';
|
||||
import {DateAdapter} from '../core/datetime/index';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
|
||||
|
||||
|
||||
/**
|
||||
* An internal component used to display a single year in the datepicker.
|
||||
* @docs-private
|
||||
*/
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-year-view',
|
||||
templateUrl: 'year-view.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdYearView<D> implements AfterContentInit {
|
||||
/** The date to display in this year view (everything other than the year is ignored). */
|
||||
@Input()
|
||||
get activeDate(): D { return this._activeDate; }
|
||||
set activeDate(value: D) {
|
||||
let oldActiveDate = this._activeDate;
|
||||
this._activeDate = value || this._dateAdapter.today();
|
||||
if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) {
|
||||
this._init();
|
||||
}
|
||||
}
|
||||
private _activeDate: D;
|
||||
|
||||
/** The currently selected date. */
|
||||
@Input()
|
||||
get selected(): D { return this._selected; }
|
||||
set selected(value: D) {
|
||||
this._selected = value;
|
||||
this._selectedMonth = this._getMonthInCurrentYear(this.selected);
|
||||
}
|
||||
private _selected: D;
|
||||
|
||||
/** A function used to filter which dates are selectable. */
|
||||
@Input() dateFilter: (date: D) => boolean;
|
||||
|
||||
/** Emits when a new month is selected. */
|
||||
@Output() selectedChange = new EventEmitter<D>();
|
||||
|
||||
/** Grid of calendar cells representing the months of the year. */
|
||||
_months: MdCalendarCell[][];
|
||||
|
||||
/** The label for this year (e.g. "2017"). */
|
||||
_yearLabel: string;
|
||||
|
||||
/** The month in this year that today falls on. Null if today is in a different year. */
|
||||
_todayMonth: number;
|
||||
|
||||
/**
|
||||
* The month in this year that the selected Date falls on.
|
||||
* Null if the selected Date is in a different year.
|
||||
*/
|
||||
_selectedMonth: number;
|
||||
|
||||
constructor(@Optional() public _dateAdapter: DateAdapter<D>,
|
||||
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
|
||||
if (!this._dateAdapter) {
|
||||
throw createMissingDateImplError('DateAdapter');
|
||||
}
|
||||
if (!this._dateFormats) {
|
||||
throw createMissingDateImplError('MD_DATE_FORMATS');
|
||||
}
|
||||
|
||||
this._activeDate = this._dateAdapter.today();
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init();
|
||||
}
|
||||
|
||||
/** Handles when a new month is selected. */
|
||||
_monthSelected(month: number) {
|
||||
this.selectedChange.emit(this._dateAdapter.createDate(
|
||||
this._dateAdapter.getYear(this.activeDate), month,
|
||||
this._dateAdapter.getDate(this.activeDate)));
|
||||
}
|
||||
|
||||
/** Initializes this month view. */
|
||||
private _init() {
|
||||
this._selectedMonth = this._getMonthInCurrentYear(this.selected);
|
||||
this._todayMonth = this._getMonthInCurrentYear(this._dateAdapter.today());
|
||||
this._yearLabel = this._dateAdapter.getYearName(this.activeDate);
|
||||
|
||||
let monthNames = this._dateAdapter.getMonthNames('short');
|
||||
// First row of months only contains 5 elements so we can fit the year label on the same row.
|
||||
this._months = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9, 10, 11]].map(row => row.map(
|
||||
month => this._createCellForMonth(month, monthNames[month])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the month in this year that the given Date falls on.
|
||||
* Returns null if the given Date is in another year.
|
||||
*/
|
||||
private _getMonthInCurrentYear(date: D) {
|
||||
return date && this._dateAdapter.getYear(date) == this._dateAdapter.getYear(this.activeDate) ?
|
||||
this._dateAdapter.getMonth(date) : null;
|
||||
}
|
||||
|
||||
/** Creates an MdCalendarCell for the given month. */
|
||||
private _createCellForMonth(month: number, monthName: string) {
|
||||
let ariaLabel = this._dateAdapter.format(
|
||||
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1),
|
||||
this._dateFormats.display.monthYearA11yLabel);
|
||||
return new MdCalendarCell(
|
||||
month, monthName.toLocaleUpperCase(), ariaLabel, this._isMonthEnabled(month));
|
||||
}
|
||||
|
||||
/** Whether the given month is enabled. */
|
||||
private _isMonthEnabled(month: number) {
|
||||
if (!this.dateFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let firstOfMonth = this._dateAdapter.createDate(
|
||||
this._dateAdapter.getYear(this.activeDate), month, 1);
|
||||
|
||||
// If any date in the month is enabled count the month as enabled.
|
||||
for (let date = firstOfMonth; this._dateAdapter.getMonth(date) == month;
|
||||
date = this._dateAdapter.addCalendarDays(date, 1)) {
|
||||
if (this.dateFilter(date)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mat-input-underline"
|
||||
<div class="mat-input-underline" #underline
|
||||
[class.mat-disabled]="_mdInputChild.disabled">
|
||||
<span class="mat-input-ripple"
|
||||
[class.mat-accent]="color == 'accent'"
|
||||
|
||||
@@ -32,9 +32,10 @@ $mat-input-underline-disabled-background-image:
|
||||
}
|
||||
|
||||
// Allow icons in a prefix/suffix/hint/etc to adapt to the correct size.
|
||||
& .mat-icon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
& .mat-icon,
|
||||
& .mat-datepicker-toggle {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
QueryList,
|
||||
Renderer2,
|
||||
Self,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
@@ -334,6 +335,8 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit {
|
||||
}
|
||||
private _floatPlaceholder: FloatPlaceholderType = 'auto';
|
||||
|
||||
@ViewChild('underline') underlineRef: ElementRef;
|
||||
|
||||
@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
|
||||
|
||||
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
|
||||
|
||||
+3
-1
@@ -35,6 +35,7 @@ import {MdDialogModule} from './dialog/index';
|
||||
import {PlatformModule} from './core/platform/index';
|
||||
import {MdAutocompleteModule} from './autocomplete/index';
|
||||
import {StyleModule} from './core/style/index';
|
||||
import {MdDatepickerModule} from './datepicker/index';
|
||||
|
||||
const MATERIAL_MODULES = [
|
||||
MdAutocompleteModule,
|
||||
@@ -43,6 +44,7 @@ const MATERIAL_MODULES = [
|
||||
MdCardModule,
|
||||
MdChipsModule,
|
||||
MdCheckboxModule,
|
||||
MdDatepickerModule,
|
||||
MdDialogModule,
|
||||
MdGridListModule,
|
||||
MdIconModule,
|
||||
@@ -68,7 +70,7 @@ const MATERIAL_MODULES = [
|
||||
A11yModule,
|
||||
PlatformModule,
|
||||
MdCommonModule,
|
||||
ObserveContentModule
|
||||
ObserveContentModule,
|
||||
];
|
||||
|
||||
/** @deprecated */
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from './button-toggle/index';
|
||||
export * from './card/index';
|
||||
export * from './chips/index';
|
||||
export * from './checkbox/index';
|
||||
export * from './datepicker/index';
|
||||
export * from './dialog/index';
|
||||
export * from './grid-list/index';
|
||||
export * from './icon/index';
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"unit-whitelist": ["px", "%", "deg", "ms", "em", "vh", "vw"],
|
||||
"unit-whitelist": ["px", "%", "deg", "ms", "em", "vh", "vw", "vmin"],
|
||||
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
|
||||
Reference in New Issue
Block a user