Skip to content

Commit

Permalink
feat(datepicker): add md-datepicker-trigger & compatibility w/ md-inp…
Browse files Browse the repository at this point in the history
…ut-container (#3468)

feat(datepicker): add mdDatepickerToggle & compatibility w/ md-input-container
  • Loading branch information
mmalerba committed Apr 14, 2017
1 parent dac4105 commit 949761a
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 44 deletions.
27 changes: 23 additions & 4 deletions src/demo-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
<h1>Work in progress, not ready for use.</h1>

<input [mdDatepicker]="dp" [(ngModel)]="date">
<button (click)="dp.openStandardUi()">open</button>
<button (click)="dp.openTouchUi()">open (mobile)</button>
<md-datepicker #dp></md-datepicker>
<p>
<md-checkbox [(ngModel)]="touch">Use touch UI</md-checkbox>
</p>
<br>
<p>
<input [mdDatepicker]="dp" [(ngModel)]="date">
<button [mdDatepickerToggle]="dp"></button>
<md-datepicker #dp [touchUi]="touch"></md-datepicker>
</p>
<p>
<md-input-container>
<input mdInput [mdDatepicker]="dp2" [(ngModel)]="date">
<button [mdDatepickerToggle]="dp2"></button>
<md-datepicker #dp2 [touchUi]="touch"></md-datepicker>
</md-input-container>
</p>
<p>
<button [mdDatepickerToggle]="dp3"></button>
<md-input-container>
<input mdInput [mdDatepicker]="dp3" [(ngModel)]="date">
<md-datepicker #dp3 [touchUi]="touch"></md-datepicker>
</md-input-container>
</p>
1 change: 1 addition & 0 deletions src/demo-app/datepicker/datepicker-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import {SimpleDate} from '@angular/material';
})
export class DatepickerDemo {
date: SimpleDate;
touch = false;
}
18 changes: 14 additions & 4 deletions src/lib/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import {
AfterContentInit, Directive, ElementRef, forwardRef, Input, OnDestroy,
AfterContentInit,
Directive,
ElementRef,
forwardRef,
Input,
OnDestroy,
Optional,
Renderer
} from '@angular/core';
import {MdDatepicker} from './datepicker';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {SimpleDate} from '../core/datetime/simple-date';
import {CalendarLocale} from '../core/datetime/calendar-locale';
import {Subscription} from 'rxjs';
import {MdInputContainer} from '../input/input-container';


export const MD_DATEPICKER_VALUE_ACCESSOR: any = {
Expand Down Expand Up @@ -55,8 +62,11 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor

private _datepickerSubscription: Subscription;

constructor(private _elementRef: ElementRef, private _renderer: Renderer,
private _locale: CalendarLocale) {}
constructor(
private _elementRef: ElementRef,
private _renderer: Renderer,
private _locale: CalendarLocale,
@Optional() private _mdInputContainer: MdInputContainer) {}

ngAfterContentInit() {
if (this._datepicker) {
Expand All @@ -75,7 +85,7 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor
}

getPopupConnectionElementRef(): ElementRef {
return this._elementRef;
return this._mdInputContainer ? this._mdInputContainer.underlineRef : this._elementRef;
}

// Implemented as part of ControlValueAccessor
Expand Down
13 changes: 13 additions & 0 deletions src/lib/datepicker/datepicker-toggle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
$mat-datepicker-toggle-icon-size: 24px !default;


.mat-datepicker-toggle {
display: inline-block;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/></svg>') no-repeat;
background-size: contain;
height: $mat-datepicker-toggle-icon-size;
width: $mat-datepicker-toggle-icon-size;
border: none;
outline: none;
vertical-align: middle;
}
30 changes: 30 additions & 0 deletions src/lib/datepicker/datepicker-toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
import {MdDatepicker} from './datepicker';


@Component({
moduleId: module.id,
selector: 'button[mdDatepickerToggle], button[matDatepickerToggle]',
template: '',
styleUrls: ['datepicker-toggle.css'],
host: {
'[class.mat-datepicker-toggle]': 'true',
'(click)': '_open($event)',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdDatepickerToggle {
@Input('mdDatepickerToggle') datepicker: MdDatepicker;

@Input('matDatepickerToggle')
get _datepicker() { return this.datepicker; }
set _datepicker(v: MdDatepicker) { this.datepicker = v; }

_open(event: Event): void {
if (this.datepicker) {
this.datepicker.open();
event.stopPropagation();
}
}
}
111 changes: 98 additions & 13 deletions src/lib/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ import {MdDatepickerInput} from './datepicker-input';
import {SimpleDate} from '../core/datetime/simple-date';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
import {dispatchFakeEvent, dispatchMouseEvent} from '../core/testing/dispatch-events';
import {MdInputModule} from '../input/index';


describe('MdDatepicker', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdDatepickerModule, FormsModule, ReactiveFormsModule],
imports: [MdDatepickerModule, MdInputModule, FormsModule, ReactiveFormsModule],
declarations: [
StandardDatepicker,
MultiInputDatepicker,
NoInputDatepicker,
DatepickerWithStartAt,
DatepickerWithNgModel,
DatepickerWithFormControl,
DatepickerWithToggle,
InputContainerDatepicker,
],
});

Expand All @@ -37,26 +40,29 @@ describe('MdDatepicker', () => {
testComponent = fixture.componentInstance;
});

it('openStandardUi should open popup', () => {
it('open non-touch should open popup', () => {
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();

testComponent.datepicker.openStandardUi();
testComponent.datepicker.open();
fixture.detectChanges();

expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
});

it('openTouchUi should open dialog', () => {
it('open touch should open dialog', () => {
testComponent.touch = true;
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).toBeNull();

testComponent.datepicker.openTouchUi();
testComponent.datepicker.open();
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).not.toBeNull();
});

it('close should close popup', async(() => {
testComponent.datepicker.openStandardUi();
testComponent.datepicker.open();
fixture.detectChanges();

let popup = document.querySelector('.cdk-overlay-pane');
Expand All @@ -72,7 +78,10 @@ describe('MdDatepicker', () => {
}));

it('close should close dialog', async(() => {
testComponent.datepicker.openTouchUi();
testComponent.touch = true;
fixture.detectChanges();

testComponent.datepicker.open();
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).not.toBeNull();
Expand All @@ -86,7 +95,10 @@ describe('MdDatepicker', () => {
}));

it('setting selected should update input and close calendar', async(() => {
testComponent.datepicker.openTouchUi();
testComponent.touch = true;
fixture.detectChanges();

testComponent.datepicker.open();
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).not.toBeNull();
Expand All @@ -105,6 +117,12 @@ describe('MdDatepicker', () => {
it('startAt should fallback to input value', () => {
expect(testComponent.datepicker.startAt).toEqual(new SimpleDate(2020, 0, 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', () => {
Expand All @@ -126,7 +144,7 @@ describe('MdDatepicker', () => {
});

it('should throw when opened with no registered inputs', () => {
expect(() => testComponent.datepicker.openStandardUi()).toThrow();
expect(() => testComponent.datepicker.open()).toThrow();
});
});

Expand Down Expand Up @@ -277,6 +295,46 @@ describe('MdDatepicker', () => {
expect(inputEl.disabled).toBe(true);
});
});

describe('datepicker with mdDatepickerToggle', () => {
let fixture: ComponentFixture<DatepickerWithToggle>;
let testComponent: DatepickerWithToggle;

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

testComponent = fixture.componentInstance;
});

it('should open calendar when toggle clicked', () => {
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(() => {
fixture = TestBed.createComponent(InputContainerDatepicker);
fixture.detectChanges();

testComponent = fixture.componentInstance;
});

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');
});
});
});


Expand All @@ -288,9 +346,13 @@ function detectModelChanges(fixture: ComponentFixture<any>) {


@Component({
template: `<input [mdDatepicker]="d" value="1/1/2020"><md-datepicker #d></md-datepicker>`,
template: `
<input [mdDatepicker]="d" value="1/1/2020">
<md-datepicker #d [touchUi]="touch"></md-datepicker>
`,
})
class StandardDatepicker {
touch = false;
@ViewChild('d') datepicker: MdDatepicker;
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput;
}
Expand Down Expand Up @@ -324,7 +386,7 @@ class DatepickerWithStartAt {


@Component({
template: `<input [(ngModel)]="selected" [mdDatepicker]="d"><md-datepicker #d></md-datepicker>`
template: `<input [(ngModel)]="selected" [mdDatepicker]="d"><md-datepicker #d></md-datepicker>`,
})
class DatepickerWithNgModel {
selected: SimpleDate = null;
Expand All @@ -337,10 +399,33 @@ class DatepickerWithNgModel {
template: `
<input [formControl]="formControl" [mdDatepicker]="d">
<md-datepicker #d></md-datepicker>
`
`,
})
class DatepickerWithFormControl {
formControl = new FormControl();
@ViewChild('d') datepicker: MdDatepicker;
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput;
}


@Component({
template: `
<input [mdDatepicker]="d">
<button [mdDatepickerToggle]="d"></button>
<md-datepicker #d [touchUi]="true"></md-datepicker>
`,
})
class DatepickerWithToggle {}


@Component({
template: `
<md-input-container>
<input mdInput [mdDatepicker]="d">
<md-datepicker #d></md-datepicker>
</md-input-container>
`,
})
class InputContainerDatepicker {
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput;
}
28 changes: 9 additions & 19 deletions src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export class MdDatepicker implements OnDestroy {
set startAt(date: SimpleDate) { this._startAt = this._locale.parseDate(date); }
private _startAt: SimpleDate;

/**
* 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;

@Output() selectedChanged = new EventEmitter<SimpleDate>();

get _selected(): SimpleDate {
Expand All @@ -63,12 +70,6 @@ export class MdDatepicker implements OnDestroy {
this.close();
}

/**
* 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.
*/
touchUi: boolean;

/** The calendar template. */
@ViewChild(TemplateRef) calendarTemplate: TemplateRef<any>;

Expand Down Expand Up @@ -106,21 +107,11 @@ export class MdDatepicker implements OnDestroy {
this._datepickerInput = input;
}

/** Opens the calendar in standard UI mode. */
openStandardUi(): void {
this._open();
}

/** Opens the calendar in touch UI mode. */
openTouchUi(): void {
this._open(true);
}

/**
* Open the calendar.
* @param touchUi Whether to use the touch UI.
*/
private _open(touchUi = false): void {
open(): void {
if (!this._datepickerInput) {
throw new MdError('Attempted to open an MdDatepicker with no associated input.');
}
Expand All @@ -129,8 +120,7 @@ export class MdDatepicker implements OnDestroy {
this._calendarPortal = new TemplatePortal(this.calendarTemplate, this._viewContainerRef);
}

this.touchUi = touchUi;
touchUi ? this._openAsDialog() : this._openAsPopup();
this.touchUi ? this._openAsDialog() : this._openAsPopup();
}

/** Close the calendar. */
Expand Down
Loading

0 comments on commit 949761a

Please sign in to comment.