diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html index f974fc7228c7..c975b470e636 100644 --- a/src/demo-app/datepicker/datepicker-demo.html +++ b/src/demo-app/datepicker/datepicker-demo.html @@ -1,6 +1,25 @@

Work in progress, not ready for use.

- - - - +

+ Use touch UI +

+
+

+ + + +

+

+ + + + + +

+

+ + + + + +

diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts index 214febf1eff0..f0927ec1203c 100644 --- a/src/demo-app/datepicker/datepicker-demo.ts +++ b/src/demo-app/datepicker/datepicker-demo.ts @@ -10,4 +10,5 @@ import {SimpleDate} from '@angular/material'; }) export class DatepickerDemo { date: SimpleDate; + touch = false; } diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index f7be57566e25..12ab3b830c3b 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -1,5 +1,11 @@ import { - AfterContentInit, Directive, ElementRef, forwardRef, Input, OnDestroy, + AfterContentInit, + Directive, + ElementRef, + forwardRef, + Input, + OnDestroy, + Optional, Renderer } from '@angular/core'; import {MdDatepicker} from './datepicker'; @@ -7,6 +13,7 @@ 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 = { @@ -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) { @@ -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 diff --git a/src/lib/datepicker/datepicker-toggle.scss b/src/lib/datepicker/datepicker-toggle.scss new file mode 100644 index 000000000000..92f1f5a42519 --- /dev/null +++ b/src/lib/datepicker/datepicker-toggle.scss @@ -0,0 +1,13 @@ +$mat-datepicker-toggle-icon-size: 24px !default; + + +.mat-datepicker-toggle { + display: inline-block; + background: url('data:image/svg+xml;utf8,') no-repeat; + background-size: contain; + height: $mat-datepicker-toggle-icon-size; + width: $mat-datepicker-toggle-icon-size; + border: none; + outline: none; + vertical-align: middle; +} diff --git a/src/lib/datepicker/datepicker-toggle.ts b/src/lib/datepicker/datepicker-toggle.ts new file mode 100644 index 000000000000..a6ba593205ac --- /dev/null +++ b/src/lib/datepicker/datepicker-toggle.ts @@ -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(); + } + } +} diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 45923506150a..37f479907508 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -6,13 +6,14 @@ 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, @@ -20,6 +21,8 @@ describe('MdDatepicker', () => { DatepickerWithStartAt, DatepickerWithNgModel, DatepickerWithFormControl, + DatepickerWithToggle, + InputContainerDatepicker, ], }); @@ -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'); @@ -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(); @@ -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(); @@ -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', () => { @@ -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(); }); }); @@ -277,6 +295,46 @@ describe('MdDatepicker', () => { expect(inputEl.disabled).toBe(true); }); }); + + describe('datepicker with mdDatepickerToggle', () => { + let fixture: ComponentFixture; + 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; + 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'); + }); + }); }); @@ -288,9 +346,13 @@ function detectModelChanges(fixture: ComponentFixture) { @Component({ - template: ``, + template: ` + + + `, }) class StandardDatepicker { + touch = false; @ViewChild('d') datepicker: MdDatepicker; @ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput; } @@ -324,7 +386,7 @@ class DatepickerWithStartAt { @Component({ - template: `` + template: ``, }) class DatepickerWithNgModel { selected: SimpleDate = null; @@ -337,10 +399,33 @@ class DatepickerWithNgModel { template: ` - ` + `, }) class DatepickerWithFormControl { formControl = new FormControl(); @ViewChild('d') datepicker: MdDatepicker; @ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput; } + + +@Component({ + template: ` + + + + `, +}) +class DatepickerWithToggle {} + + +@Component({ + template: ` + + + + + `, +}) +class InputContainerDatepicker { + @ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput; +} diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 27a325bb0d73..5e0e20cb3cf6 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -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(); get _selected(): SimpleDate { @@ -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; @@ -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.'); } @@ -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. */ diff --git a/src/lib/datepicker/index.ts b/src/lib/datepicker/index.ts index b8138a0fd46c..f6afa6f473f2 100644 --- a/src/lib/datepicker/index.ts +++ b/src/lib/datepicker/index.ts @@ -9,6 +9,7 @@ import {MdDatepicker} from './datepicker'; import {MdDatepickerInput} from './datepicker-input'; import {MdDialogModule} from '../dialog/index'; import {MdCalendar} from './calendar'; +import {MdDatepickerToggle} from './datepicker-toggle'; export * from './calendar'; @@ -29,12 +30,14 @@ export * from './year-view'; exports: [ MdDatepicker, MdDatepickerInput, + MdDatepickerToggle, ], declarations: [ MdCalendar, MdCalendarTable, MdDatepicker, MdDatepickerInput, + MdDatepickerToggle, MdMonthView, MdYearView, ], diff --git a/src/lib/input/input-container.html b/src/lib/input/input-container.html index cefe5a06f8d1..e19f0c715c53 100644 --- a/src/lib/input/input-container.html +++ b/src/lib/input/input-container.html @@ -29,7 +29,7 @@ -