-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(datepicker): create the md-datepicker component (#3024)
* 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 * add md-datepicker and input[mdDatepicker] * Add touch UI support * fix some stuff that got messed up in rebasing * addressed comments * move position strategy to separate method * added tests for error cases
- Loading branch information
Showing
6 changed files
with
323 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import {Directive, ElementRef, Input} from '@angular/core'; | ||
import {MdDatepicker} from './datepicker'; | ||
|
||
|
||
/** Directive used to connect an input to a MdDatepicker. */ | ||
@Directive({ | ||
selector: 'input[mdDatepicker], input[matDatepicker]', | ||
}) | ||
export class MdDatepickerInput { | ||
@Input() | ||
set mdDatepicker(value: MdDatepicker) { | ||
if (value) { | ||
this._datepicker = value; | ||
this._datepicker._registerInput(this._elementRef); | ||
} | ||
} | ||
private _datepicker: MdDatepicker; | ||
|
||
@Input() | ||
set matDatepicker(value: MdDatepicker) { this.mdDatepicker = value; } | ||
|
||
constructor(private _elementRef: ElementRef) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<template> | ||
I'm a datepicker (touch ui = {{touchUi}}). | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import {TestBed, async, ComponentFixture} from '@angular/core/testing'; | ||
import {MdDatepickerModule} from './index'; | ||
import {Component, ViewChild} from '@angular/core'; | ||
import {MdDatepicker} from './datepicker'; | ||
|
||
describe('MdDatepicker', () => { | ||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [MdDatepickerModule], | ||
declarations: [ | ||
StandardDatepicker, | ||
MultiInputDatepicker, | ||
NoInputDatepicker, | ||
], | ||
}); | ||
|
||
TestBed.compileComponents(); | ||
})); | ||
|
||
describe('standard datepicker', () => { | ||
let fixture: ComponentFixture<StandardDatepicker>; | ||
let testComponent: StandardDatepicker; | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(StandardDatepicker); | ||
fixture.detectChanges(); | ||
|
||
testComponent = fixture.componentInstance; | ||
}); | ||
|
||
it('openStandardUi should open popup', () => { | ||
expect(document.querySelector('.cdk-overlay-pane')).toBeNull(); | ||
|
||
testComponent.datepicker.openStandardUi(); | ||
fixture.detectChanges(); | ||
|
||
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull(); | ||
}); | ||
|
||
it('openTouchUi should open dialog', () => { | ||
expect(document.querySelector('md-dialog-container')).toBeNull(); | ||
|
||
testComponent.datepicker.openTouchUi(); | ||
fixture.detectChanges(); | ||
|
||
expect(document.querySelector('md-dialog-container')).not.toBeNull(); | ||
}); | ||
|
||
it('close should close popup', () => { | ||
testComponent.datepicker.openStandardUi(); | ||
fixture.detectChanges(); | ||
|
||
let popup = document.querySelector('.cdk-overlay-pane'); | ||
expect(popup).not.toBeNull(); | ||
expect(parseInt(getComputedStyle(popup).height)).not.toBe(0); | ||
|
||
testComponent.datepicker.close(); | ||
fixture.detectChanges(); | ||
|
||
expect(parseInt(getComputedStyle(popup).height)).toBe(0); | ||
}); | ||
|
||
it('close should close dialog', () => { | ||
testComponent.datepicker.openTouchUi(); | ||
fixture.detectChanges(); | ||
|
||
expect(document.querySelector('md-dialog-container')).not.toBeNull(); | ||
|
||
testComponent.datepicker.close(); | ||
fixture.detectChanges(); | ||
|
||
expect(document.querySelector('md-dialog-container')).toBeNull(); | ||
}); | ||
}); | ||
|
||
describe('datepicker with too many inputs', () => { | ||
it('should throw when multiple inputs registered', () => { | ||
let fixture = TestBed.createComponent(MultiInputDatepicker); | ||
expect(() => fixture.detectChanges()).toThrow(); | ||
}); | ||
}); | ||
|
||
describe('datepicker with no inputs', () => { | ||
let fixture: ComponentFixture<NoInputDatepicker>; | ||
let testComponent: NoInputDatepicker; | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(NoInputDatepicker); | ||
fixture.detectChanges(); | ||
|
||
testComponent = fixture.componentInstance; | ||
}); | ||
|
||
it('should throw when opened with no registered inputs', () => { | ||
expect(() => testComponent.datepicker.openStandardUi()).toThrow(); | ||
}); | ||
}); | ||
}); | ||
|
||
|
||
@Component({ | ||
template: `<input [mdDatepicker]="d"><md-datepicker #d></md-datepicker>`, | ||
}) | ||
class StandardDatepicker { | ||
@ViewChild('d') datepicker: MdDatepicker; | ||
} | ||
|
||
|
||
@Component({ | ||
template: ` | ||
<input [mdDatepicker]="d"><input [mdDatepicker]="d"><md-datepicker #d></md-datepicker> | ||
`, | ||
}) | ||
class MultiInputDatepicker { | ||
@ViewChild('d') datepicker: MdDatepicker; | ||
} | ||
|
||
|
||
@Component({ | ||
template: `<md-datepicker #d></md-datepicker>`, | ||
}) | ||
class NoInputDatepicker { | ||
@ViewChild('d') datepicker: MdDatepicker; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import { | ||
Component, | ||
TemplateRef, | ||
ViewChild, | ||
ViewEncapsulation, | ||
ChangeDetectionStrategy, | ||
ViewContainerRef, | ||
Optional, | ||
ElementRef, | ||
OnDestroy | ||
} from '@angular/core'; | ||
import {Overlay} from '../core/overlay/overlay'; | ||
import {OverlayRef} from '../core/overlay/overlay-ref'; | ||
import {TemplatePortal} from '../core/portal/portal'; | ||
import {OverlayState} from '../core/overlay/overlay-state'; | ||
import {Dir} from '../core/rtl/dir'; | ||
import {MdError} from '../core/errors/error'; | ||
import {MdDialog} from '../dialog/dialog'; | ||
import {MdDialogRef} from '../dialog/dialog-ref'; | ||
import {PositionStrategy} from '../core/overlay/position/position-strategy'; | ||
import { | ||
OriginConnectionPosition, | ||
OverlayConnectionPosition | ||
} from '../core/overlay/position/connected-position'; | ||
|
||
|
||
// TODO(mmalerba): Figure out what the real width should be. | ||
const CALENDAR_POPUP_WIDTH = 300; | ||
|
||
|
||
/** Component responsible for managing the datepicker popup/dialog. */ | ||
@Component({ | ||
moduleId: module.id, | ||
selector: 'md-datepicker, mat-datepicker', | ||
templateUrl: 'datepicker.html', | ||
encapsulation: ViewEncapsulation.None, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class MdDatepicker implements OnDestroy { | ||
/** | ||
* 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>; | ||
|
||
/** A reference to the overlay when the calendar is opened as a popup. */ | ||
private _popupRef: OverlayRef; | ||
|
||
/** A reference to the dialog when the calendar is opened as a dialog. */ | ||
private _dialogRef: MdDialogRef<any>; | ||
|
||
/** A portal containing the calendar for this datepicker. */ | ||
private _calendarPortal: TemplatePortal; | ||
|
||
/** The input element this datepicker is associated with. */ | ||
private _inputElementRef: ElementRef; | ||
|
||
constructor(private _dialog: MdDialog, private _overlay: Overlay, | ||
private _viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir) {} | ||
|
||
ngOnDestroy() { | ||
this.close(); | ||
if (this._popupRef) { | ||
this._popupRef.dispose(); | ||
} | ||
} | ||
|
||
/** | ||
* Register an input with this datepicker. | ||
* @param inputElementRef An ElementRef for the input. | ||
*/ | ||
_registerInput(inputElementRef: ElementRef): void { | ||
if (this._inputElementRef) { | ||
throw new MdError('An MdDatepicker can only be associated with a single input.'); | ||
} | ||
this._inputElementRef = inputElementRef; | ||
} | ||
|
||
/** 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 { | ||
if (!this._inputElementRef) { | ||
throw new MdError('Attempted to open an MdDatepicker with no associated input.'); | ||
} | ||
|
||
if (!this._calendarPortal) { | ||
this._calendarPortal = new TemplatePortal(this.calendarTemplate, this._viewContainerRef); | ||
} | ||
|
||
this.touchUi = touchUi; | ||
touchUi ? this._openAsDialog() : this._openAsPopup(); | ||
} | ||
|
||
/** Close the calendar. */ | ||
close(): void { | ||
if (this._popupRef && this._popupRef.hasAttached()) { | ||
this._popupRef.detach(); | ||
} | ||
if (this._dialogRef) { | ||
this._dialogRef.close(); | ||
this._dialogRef = null; | ||
} | ||
if (this._calendarPortal && this._calendarPortal.isAttached) { | ||
this._calendarPortal.detach(); | ||
} | ||
} | ||
|
||
/** Open the calendar as a dialog. */ | ||
private _openAsDialog(): void { | ||
this._dialogRef = this._dialog.open(this.calendarTemplate); | ||
} | ||
|
||
/** Open the calendar as a popup. */ | ||
private _openAsPopup(): void { | ||
if (!this._popupRef) { | ||
this._createPopup(); | ||
} | ||
|
||
if (!this._popupRef.hasAttached()) { | ||
this._popupRef.attach(this._calendarPortal); | ||
} | ||
|
||
this._popupRef.backdropClick().first().subscribe(() => this.close()); | ||
} | ||
|
||
/** Create the popup. */ | ||
private _createPopup(): void { | ||
const overlayState = new OverlayState(); | ||
overlayState.positionStrategy = this._createPopupPositionStrategy(); | ||
overlayState.width = CALENDAR_POPUP_WIDTH; | ||
overlayState.hasBackdrop = true; | ||
overlayState.backdropClass = 'md-overlay-transparent-backdrop'; | ||
overlayState.direction = this._dir ? this._dir.value : 'ltr'; | ||
|
||
this._popupRef = this._overlay.create(overlayState); | ||
} | ||
|
||
/** Create the popup PositionStrategy. */ | ||
private _createPopupPositionStrategy(): PositionStrategy { | ||
let origin = {originX: 'start', originY: 'bottom'} as OriginConnectionPosition; | ||
let overlay = {overlayX: 'start', overlayY: 'top'} as OverlayConnectionPosition; | ||
return this._overlay.position().connectedTo(this._inputElementRef, origin, overlay); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters