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 month & year view #2904

Merged
merged 19 commits into from
Feb 7, 2017
Merged
Show file tree
Hide file tree
Changes from 17 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
7 changes: 7 additions & 0 deletions src/demo-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<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>

<br>
<div>{{selected?.toNativeDate()}}</div>
13 changes: 13 additions & 0 deletions src/demo-app/datepicker/datepicker-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Component} from '@angular/core';
import {SimpleDate} from '@angular/material';


@Component({
moduleId: module.id,
selector: 'datepicker-demo',
templateUrl: 'datepicker-demo.html'
})
export class DatepickerDemo {
date = SimpleDate.today();
selected: SimpleDate;
}
2 changes: 2 additions & 0 deletions src/demo-app/demo-app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d
import {PlatformDemo} from './platform/platform-demo';
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
import {InputContainerDemo} from './input/input-container-demo';
import {DatepickerDemo} from './datepicker/datepicker-demo';

@NgModule({
imports: [
Expand All @@ -56,6 +57,7 @@ import {InputContainerDemo} from './input/input-container-demo';
CardDemo,
ChipsDemo,
CheckboxDemo,
DatepickerDemo,
DemoApp,
DialogDemo,
GesturesDemo,
Expand Down
1 change: 1 addition & 0 deletions src/demo-app/demo-app/demo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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'},
Expand Down
2 changes: 2 additions & 0 deletions src/demo-app/demo-app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ import {TABS_DEMO_ROUTES} from '../tabs/routes';
import {PlatformDemo} from '../platform/platform-demo';
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
import {InputContainerDemo} from '../input/input-container-demo';
import {DatepickerDemo} from '../datepicker/datepicker-demo';

export const DEMO_APP_ROUTES: Routes = [
{path: '', component: Home},
{path: 'autocomplete', component: AutocompleteDemo},
{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},
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/datetime/calendar-locale.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,5 @@ describe('DefaultCalendarLocale', () => {

it('gets header label for calendar year', () => {
expect(calendarLocale.getCalendarYearHeaderLabel(new SimpleDate(2017, 0, 1))).toBe('2017');
})
});
});
28 changes: 28 additions & 0 deletions src/lib/core/datetime/simple-date.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,32 @@ describe('SimpleDate', () => {
it('can be converted to native Date', () => {
expect(new SimpleDate(2017, 0, 1).toNativeDate()).toEqual(new Date(2017, 0, 1));
});

it('handles month and date overflow', () => {
expect(new SimpleDate(2017, 12, 32)).toEqual(new SimpleDate(2018, 1, 1));
});

it('handles month and date underflow', () => {
expect(new SimpleDate(2017, -1, 0)).toEqual(new SimpleDate(2016, 10, 30));
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it (2016, 10, 31)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The months are 0-indexed to match native JS Dates, so 10 = November. I considered using 1-indexed because it make more sense, but I thought it was better to just stay consistent with what people already know. I'll add a note about the 0-indexing to the SimpleDate docs.

});

it('handles low year numbers', () => {
expect(new SimpleDate(-1, 0, 1).year).toBe(-1);
expect(new SimpleDate(0, 0, 1).year).toBe(0);
expect(new SimpleDate(50, 0, 1).year).toBe(50);
expect(new SimpleDate(99, 0, 1).year).toBe(99);
expect(new SimpleDate(100, 0, 1).year).toBe(100);
});

it('handles low year number with over/under-flow', () => {
expect(new SimpleDate(50, 12 * 51, 1).year).toBe(101);
expect(new SimpleDate(50, 12, 1).year).toBe(51);
expect(new SimpleDate(50, -12, 1).year).toBe(49);
expect(new SimpleDate(50, -12 * 51, 1).year).toBe(-1);
});

it('adds years, months, and days', () => {
expect(new SimpleDate(2017, 0, 1).add({years: 1, months: 1, days: 1}))
.toEqual(new SimpleDate(2018, 1, 2));
});
});
53 changes: 52 additions & 1 deletion src/lib/core/datetime/simple-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,63 @@
* details and the time component of the native Date.
*/
export class SimpleDate {
/**
* Create a SimpleDate from a native JS Date object.
* @param nativeDate The native JS Date object to convert.
*/
static fromNativeDate(nativeDate: Date) {
return new SimpleDate(nativeDate.getFullYear(), nativeDate.getMonth(), nativeDate.getDate());
}

constructor(public year: number, public month: number, public date: number) {}
/** Creates a SimpleDate object representing today. */
static today() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Return type? Same for methods below.

return SimpleDate.fromNativeDate(new Date());
}

/** The native JS Date. */
private _date: Date;

constructor(year: number, month: number, date: number) {
this._date = 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) {
this._date = new Date(this._date.setFullYear(this.year - 1900));
}
}

/** The year component of this date. */
get year() {
return this._date.getFullYear();
}

/** The month component of this date. */
get month() {
return this._date.getMonth();
}

/** The date component of this date. */
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Also add doc about date index?

get date() {
return this._date.getDate();
}

/** The day component of this date. */
get day() {
return this._date.getDay();
}

/**
* 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}) {
return new SimpleDate(
this.year + amount.years || 0,
this.month + amount.months || 0,
this.date + amount.days || 0);
}

/** Converts the SimpleDate to a native JS Date object. */
toNativeDate() {
return new Date(this.year, this.month, this.date);
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/core/theming/_all-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -33,6 +34,7 @@
@include md-card-theme($theme);
@include md-checkbox-theme($theme);
@include md-chips-theme($theme);
@include mat-datepicker-theme($theme);
@include md-dialog-theme($theme);
@include md-grid-list-theme($theme);
@include md-icon-theme($theme);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/datepicker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# md-datepicker

Work in progress, not ready for use.
30 changes: 30 additions & 0 deletions src/lib/datepicker/_datepicker-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@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-calendar-table-label {
color: md-color($foreground, secondary-text);
}

.mat-calendar-table-cell-content {
color: md-color($foreground, text);

.mat-calendar-table-cell:hover & {
background: md-color($background, hover);
}

.mat-calendar-table-cell &.mat-calendar-table-selected {
background: md-color($primary);
color: md-color($primary, default-contrast);
}

&.mat-calendar-table-today {
border-color: md-color($foreground, divider);
}
}
}
24 changes: 24 additions & 0 deletions src/lib/datepicker/calendar-table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<table class="mat-calendar-table-table">
<!-- If there's not enough space in the first row, create a separate label row. -->
<tr *ngIf="_firstRowOffset < labelMinRequiredCells">
<td class="mat-calendar-table-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 i = index">
<td *ngIf="i === 0 && _firstRowOffset"
class="mat-calendar-table-label"
[attr.colspan]="_firstRowOffset">
{{_firstRowOffset >= labelMinRequiredCells ? label : ''}}
</td>
<td *ngFor="let item of row"
class="mat-calendar-table-cell"
(click)="_cellClicked(item.value)">
<div class="mat-calendar-table-cell-content"
[class.mat-calendar-table-selected]="selectedValue === item.value"
[class.mat-calendar-table-today]="todayValue === item.value">
{{item.displayValue}}
</div>
</td>
</tr>
</table>
39 changes: 39 additions & 0 deletions src/lib/datepicker/calendar-table.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
$mat-calendar-table-font-size: 12px !default;
$mat-calendar-table-cell-padding: 1px !default;
$mat-calendar-table-cell-content-size: 32px !default;
$mat-calendar-table-cell-content-border-width: 1px !default;


.mat-calendar-table-table {
border-spacing: 0;
font-size: $mat-calendar-table-font-size;
}

.mat-calendar-table-label {
height: $mat-calendar-table-cell-content-size;
padding: 0 0 0 10px;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Sass variable for 10px? Seems like the only one missing.

text-align: left;
font-weight: normal;
}

.mat-calendar-table-cell {
padding: $mat-calendar-table-cell-padding;
}

.mat-calendar-table-cell-content {
display: table-cell;
box-sizing: border-box;
width: $mat-calendar-table-cell-content-size;
height: $mat-calendar-table-cell-content-size;
border: $mat-calendar-table-cell-content-border-width solid transparent;
border-radius: 50%;
text-align: center;
vertical-align: middle;
}

[dir='rtl'] {
.mat-calendar-table-label {
padding: 0 10px 0 0;
text-align: right;
}
}
116 changes: 116 additions & 0 deletions src/lib/datepicker/calendar-table.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {async, TestBed, ComponentFixture} from '@angular/core/testing';
import {MdDatepickerModule} from './index';
import {Component} from '@angular/core';
import {MdCalendarTable, MdCalendarCell} from './calendar-table';
import {By} from '@angular/platform-browser';


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

TestBed.compileComponents();
}));

describe('standard calendar table', () => {
let fixture: ComponentFixture<StandardCalendarTable>;
let testComponent: StandardCalendarTable;
let calendarTableNativeElement: Element;
let rowEls: NodeListOf<Element>;
let labelEls: NodeListOf<Element>;
let cellEls: NodeListOf<Element>;

let refreshElementLists = () => {
rowEls = calendarTableNativeElement.querySelectorAll('tr');
labelEls = calendarTableNativeElement.querySelectorAll('.mat-calendar-table-label');
cellEls = calendarTableNativeElement.querySelectorAll('.mat-calendar-table-cell');
};

beforeEach(() => {
fixture = TestBed.createComponent(StandardCalendarTable);
fixture.detectChanges();

let calendarTableDebugElement = fixture.debugElement.query(By.directive(MdCalendarTable));
calendarTableNativeElement = calendarTableDebugElement.nativeElement;
testComponent = fixture.componentInstance;

refreshElementLists();
});

it('creates table', () => {
expect(rowEls.length).toBe(3);
expect(labelEls.length).toBe(1);
expect(cellEls.length).toBe(14);
});

it('highlights today', () => {
let todayCell = calendarTableNativeElement.querySelector('.mat-calendar-table-today');
expect(todayCell).not.toBeNull();
expect(todayCell.innerHTML.trim()).toBe('3');
});

it('highlights selected', () => {
let todayCell = calendarTableNativeElement.querySelector('.mat-calendar-table-selected');
Copy link
Contributor

Choose a reason for hiding this comment

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

very nit: selectedCell?

expect(todayCell).not.toBeNull();
expect(todayCell.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.contains('mat-calendar-table-label')).toBe(
true, 'first cell should be the label');
Copy link
Contributor

Choose a reason for hiding this comment

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

This might be a bit more readable if you use toContain() assertion

 expect(rowEls[0].firstElementChild.classList)
    .toContain('mat-calendar-table-label', 'First cell should be the label.');

expect(labelEls[0].getAttribute('colspan')).toBe('3');
});

it('cell should be selected on click', () => {
let todayElement =
calendarTableNativeElement.querySelector('.mat-calendar-table-today') as HTMLElement;
todayElement.click();
fixture.detectChanges();

expect(todayElement.classList.contains('mat-calendar-table-selected')).toBe(
true, 'today should be selected');
});
});
});


@Component({
template: `<md-calendar-table [label]="label"
[rows]="rows"
[todayValue]="todayValue"
[selectedValue]="selectedValue"
[labelMinRequiredCells]="labelMinRequiredCells"
[numCols]="numCols"
(selectedValueChange)="onSelect($event)">
</md-calendar-table>`,
})
class StandardCalendarTable {
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;
}
}


function createCell(value: number) {
return new MdCalendarCell(value, `${value}`);
}
Loading