Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datepicker): add calendar component that pulls together month and year views #2994

Merged
merged 2 commits into from
Feb 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/demo-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<h1>Work in progress, not ready for use.</h1>

<md-month-view [date]="date" [(selected)]="selected"></md-month-view>
<md-year-view [date]="date" [(selected)]="selected"></md-year-view>
<md-calendar [startAt]="startAt" [(selected)]="selected"></md-calendar>

<br>
<div>{{selected?.toNativeDate()}}</div>
Expand Down
3 changes: 3 additions & 0 deletions src/demo-app/datepicker/datepicker-demo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
md-calendar {
width: 300px;
}
4 changes: 3 additions & 1 deletion src/demo-app/datepicker/datepicker-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {SimpleDate} from '@angular/material';
@Component({
moduleId: module.id,
selector: 'datepicker-demo',
templateUrl: 'datepicker-demo.html'
templateUrl: 'datepicker-demo.html',
styleUrls: ['datepicker-demo.css'],
})
export class DatepickerDemo {
startAt = new SimpleDate(2017, 0, 1);
date = SimpleDate.today();
selected: SimpleDate;
}
18 changes: 14 additions & 4 deletions src/lib/core/datetime/simple-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,21 @@ export class SimpleDate {
* Adds an amount of time (in days, months, and years) to the date.
* @param amount The amount of time to add.
*/
add(amount: {days: number, months: number, years: number}): SimpleDate {
add(amount: {days?: number, months?: number, years?: number}): SimpleDate {
return new SimpleDate(
this.year + amount.years || 0,
this.month + amount.months || 0,
this.date + amount.days || 0);
this.year + (amount.years || 0),
this.month + (amount.months || 0),
this.date + (amount.days || 0));
}

/**
* Compares this SimpleDate with another SimpleDate.
* @param other The other SimpleDate
* @returns 0 if the dates are equal, a number less than 0 if this date is earlier,
* a number greater than 0 if this date is greater.
*/
compare(other: SimpleDate): number {
return this.year - other.year || this.month - other.month || this.date - other.date;
}

/** Converts the SimpleDate to a native JS Date object. */
Expand Down
39 changes: 39 additions & 0 deletions src/lib/datepicker/calendar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div class="mat-calendar-header">
<button class="mat-calendar-button mat-calendar-period-button" (click)="_currentPeriodClicked()">
{{_label}}
<div class="mat-calendar-arrow"></div>
</button>
<div class="mat-calendar-spacer"></div>
<button class="mat-calendar-button mat-calendar-previous-button" (click)="_previousClicked()">
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"
fill="#000000">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use css color instead of hard code in html? Why do we put this in a svg file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are "keyboard arrow left" and "keyboard arrow right" from go/icons. I'd rather not mess with the contents of the svgs. I'm embedding them because its a pattern we already follow elsewhere in the code (I assume to avoid incurring additional resource requests). see: https://github.com/angular/material2/blob/master/src/lib/checkbox/checkbox.html#L24

<path d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"/>
<path d="M0-.5h24v24H0z" fill="none"/>
</svg>
</button>
<button class="mat-calendar-button mat-calendar-next-button" (click)="_nextClicked()">
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"
fill="#000000">
<path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/>
<path d="M0-.25h24v24H0z" fill="none"/>
</svg>
</button>
</div>

<table class="mat-calendar-weekday-table">
<tr><td *ngFor="let day of _weekdays">{{day}}</td></tr>
</table>

<md-month-view
*ngIf="_monthView"
[date]="_currentPeriod"
[selected]="selected"
(selectedChange)="_dateSelected($event)">
</md-month-view>

<md-year-view
*ngIf="!_monthView"
[date]="_currentPeriod"
[selected]="selected"
(selectedChange)="_monthSelected($event)">
</md-year-view>
36 changes: 36 additions & 0 deletions src/lib/datepicker/calendar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
$mat-calendar-arrow-size: 5px !default;

.mat-calendar {
display: block;
}

.mat-calendar-header {
display: flex;
}

.mat-calendar-spacer {
flex: 1 1 auto;
}

.mat-calendar-button {
background: transparent;
padding: 0;
margin: 0;
border: none;
outline: none;
}

.mat-calendar-button > svg {
vertical-align: middle;
}

.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: $mat-calendar-arrow-size solid;
margin: 0 $mat-calendar-arrow-size;
vertical-align: middle;
}
128 changes: 128 additions & 0 deletions src/lib/datepicker/calendar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {async, TestBed, ComponentFixture} from '@angular/core/testing';
import {MdDatepickerModule} from './index';
import {Component} from '@angular/core';
import {SimpleDate} from '../core/datetime/simple-date';
import {MdCalendar} from './calendar';
import {By} from '@angular/platform-browser';


describe('MdCalendar', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdDatepickerModule],
declarations: [
StandardCalendar,
],
});

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;

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 visible', () => {
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
});

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._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));

nextButton.click();
fixture.detectChanges();

expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 1, 1));

prevButton.click();
fixture.detectChanges();

expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
});

it('should go to previous and next year', () => {
periodButton.click();
fixture.detectChanges();

expect(calendarInstance._monthView).toBe(false, 'should be in year view');
expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));

nextButton.click();
fixture.detectChanges();

expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1));

prevButton.click();
fixture.detectChanges();

expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
});

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._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));

let monthCells = calendarElement.querySelectorAll('.mat-calendar-table-cell');
(monthCells[monthCells.length - 1] as HTMLElement).click();
fixture.detectChanges();

expect(calendarInstance._monthView).toBe(true, 'should be in month view');
expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 11, 1));
expect(testComponent.selected).toBeFalsy('no date should be selected yet');
});

it('should select date in month view', () => {
let monthCells = calendarElement.querySelectorAll('.mat-calendar-table-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 SimpleDate(2017, 0, 31));
});
});
});


@Component({
template: `<md-calendar startAt="1/31/2017" [(selected)]="selected"></md-calendar>`
})
class StandardCalendar {
selected: SimpleDate;
}
108 changes: 108 additions & 0 deletions src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
ChangeDetectionStrategy,
ViewEncapsulation,
Component,
Input,
AfterContentInit, Output, EventEmitter
} from '@angular/core';
import {SimpleDate} from '../core/datetime/simple-date';
import {CalendarLocale} from '../core/datetime/calendar-locale';


/**
* 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 implements AfterContentInit {
/** A date representing the period (month or year) to start the calendar in. */
@Input()
get startAt() {return this._startAt; }
set startAt(value: any) { this._startAt = this._locale.parseDate(value); }
private _startAt: SimpleDate;

/** Whether the calendar should be started in month or year view. */
@Input() startView: 'month' | 'year' = 'month';

/** The currently selected date. */
@Input()
get selected() { return this._selected; }
set selected(value: any) { this._selected = this._locale.parseDate(value); }
private _selected: SimpleDate;

/** Emits when the currently selected date changes. */
@Output() selectedChange = new EventEmitter<SimpleDate>();

/**
* A date representing the current period shown in the calendar. The current period is always
* normalized to the 1st of a month, this prevents date overflow issues (e.g. adding a month to
* January 31st and overflowing into March).
*/
get _currentPeriod() { return this._normalizedCurrentPeriod; }
set _currentPeriod(value: SimpleDate) {
this._normalizedCurrentPeriod = new SimpleDate(value.year, value.month, 1);
}
private _normalizedCurrentPeriod: SimpleDate;

/** Whether the calendar is in month view. */
_monthView: boolean;

/** The names of the weekdays. */
_weekdays: string[];

/** The label for the current calendar view. */
get _label(): string {
return this._monthView ? this._locale.getCalendarMonthHeaderLabel(this._currentPeriod) :
this._locale.getCalendarYearHeaderLabel(this._currentPeriod);
}

constructor(private _locale: CalendarLocale) {
this._weekdays = this._locale.narrowDays.slice(this._locale.firstDayOfWeek)
.concat(this._locale.narrowDays.slice(0, this._locale.firstDayOfWeek));
}

ngAfterContentInit() {
this._currentPeriod = this.startAt || SimpleDate.today();
this._monthView = this.startView != 'year';
}

/** Handles date selection in the month view. */
_dateSelected(date: SimpleDate) {
if ((!date || !this.selected) && date != this.selected || date.compare(this.selected)) {
this.selectedChange.emit(date);
}
}

/** Handles month selection in the year view. */
_monthSelected(month: SimpleDate) {
this._currentPeriod = month;
this._monthView = true;
}

/** Handles user clicks on the period label. */
_currentPeriodClicked() {
this._monthView = !this._monthView;
}

/** Handles user clicks on the previous button. */
_previousClicked() {
let amount = this._monthView ? {months: -1} : {years: -1};
this._currentPeriod = this._currentPeriod.add(amount);
}

/** Handles user clicks on the next button. */
_nextClicked() {
let amount = this._monthView ? {months: 1} : {years: 1};
this._currentPeriod = this._currentPeriod.add(amount);
}
}
6 changes: 4 additions & 2 deletions src/lib/datepicker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {OverlayModule} from '../core/overlay/overlay-directives';
import {MdDatepicker} from './datepicker';
import {MdDatepickerInput} from './datepicker-input';
import {MdDialogModule} from '../dialog/index';
import {MdCalendar} from './calendar';


export * from './calendar';
export * from './calendar-table';
export * from './datepicker';
export * from './datepicker-input';
Expand All @@ -19,7 +21,7 @@ export * from './year-view';

@NgModule({
imports: [CommonModule, DatetimeModule, MdDialogModule, OverlayModule],
exports: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
declarations: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
exports: [MdCalendar, MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
declarations: [MdCalendar, MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
})
export class MdDatepickerModule {}