Skip to content

Commit

Permalink
feat(datepicker): add month & year view (angular#2904)
Browse files Browse the repository at this point in the history
* date picker initial commit

* month view

* added month view functionality

* more month view tweaking

* started extracting common stuff to calendar-table.

* base month view on calendar table

* added year view

* add disclaimers

* addressed comments

* fix lint

* fixed aot and added comment

* started on tests

* calendar table tests

* tests for month and year view

* rebase on top of CalendarLocale & SimpleDate

* add some additional functionality to SimpleDate

* fix lint

* addressed comments

* add comment
  • Loading branch information
mmalerba committed Feb 9, 2017
1 parent 7f984e9 commit 403c9a6
Show file tree
Hide file tree
Showing 24 changed files with 793 additions and 2 deletions.
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 {PlatformDemo} from './platform/platform-demo';
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
import {InputContainerDemo} from './input/input-container-demo';
import {StyleDemo} from './style/style-demo';
import {DatepickerDemo} from './datepicker/datepicker-demo';

@NgModule({
imports: [
Expand All @@ -56,6 +57,7 @@ import {StyleDemo} from './style/style-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 @@ -33,13 +33,15 @@ import {PlatformDemo} from '../platform/platform-demo';
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
import {InputContainerDemo} from '../input/input-container-demo';
import {StyleDemo} from '../style/style-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));
});

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() {
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. (0-indexed, 0 = January). */
get month() {
return this._date.getMonth();
}

/** The date component of this date. (1-indexed, 1 = 1st of month). */
get date() {
return this._date.getDate();
}

/** The day component of this date. (0-indexed, 0 = Sunday) */
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 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);
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;
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 selectedCell = calendarTableNativeElement.querySelector('.mat-calendar-table-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.contains('mat-calendar-table-label')).toBe(
true, '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

0 comments on commit 403c9a6

Please sign in to comment.