diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 244a6640bda5..1bb26abe14df 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -16,9 +16,9 @@
+ [ngSwitch]="_monthView" cdkMonitorSubtreeFocus> { .toThrowError(/MdDatepicker: No provider found for .*/); }); }); + + describe('popup positioning', () => { + let fixture: ComponentFixture; + let testComponent: StandardDatepicker; + let input: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MdDatepickerModule, MdInputModule, MdNativeDateModule, NoopAnimationsModule], + declarations: [StandardDatepicker], + }).compileComponents(); + + fixture = TestBed.createComponent(StandardDatepicker); + fixture.detectChanges(); + testComponent = fixture.componentInstance; + input = fixture.debugElement.query(By.css('input')).nativeElement; + input.style.position = 'fixed'; + })); + + it('should be below and to the right when there is plenty of space', () => { + input.style.top = input.style.left = '20px'; + testComponent.datepicker.open(); + fixture.detectChanges(); + + const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect(); + const inputRect = input.getBoundingClientRect(); + + expect(Math.floor(overlayRect.top)) + .toBe(Math.floor(inputRect.bottom), 'Expected popup to align to input bottom.'); + expect(Math.floor(overlayRect.left)) + .toBe(Math.floor(inputRect.left), 'Expected popup to align to input left.'); + }); + + it('should be above and to the right when there is no space below', () => { + input.style.bottom = input.style.left = '20px'; + testComponent.datepicker.open(); + fixture.detectChanges(); + + const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect(); + const inputRect = input.getBoundingClientRect(); + + expect(Math.floor(overlayRect.bottom)) + .toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.'); + expect(Math.floor(overlayRect.left)) + .toBe(Math.floor(inputRect.left), 'Expected popup to align to input left.'); + }); + + it('should be below and to the left when there is no space on the right', () => { + input.style.top = input.style.right = '20px'; + testComponent.datepicker.open(); + fixture.detectChanges(); + + const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect(); + const inputRect = input.getBoundingClientRect(); + + expect(Math.floor(overlayRect.top)) + .toBe(Math.floor(inputRect.bottom), 'Expected popup to align to input bottom.'); + expect(Math.floor(overlayRect.right)) + .toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.'); + }); + + it('should be above and to the left when there is no space on the bottom', () => { + input.style.bottom = input.style.right = '20px'; + testComponent.datepicker.open(); + fixture.detectChanges(); + + const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect(); + const inputRect = input.getBoundingClientRect(); + + expect(Math.floor(overlayRect.bottom)) + .toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.'); + expect(Math.floor(overlayRect.right)) + .toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.'); + }); + + }); }); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 2748b75059d3..bddb7c701adb 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -10,7 +10,8 @@ import { Output, ViewChild, ViewContainerRef, - ViewEncapsulation + ViewEncapsulation, + NgZone, } from '@angular/core'; import {Overlay} from '../core/overlay/overlay'; import {OverlayRef} from '../core/overlay/overlay-ref'; @@ -20,18 +21,15 @@ import {Dir} from '../core/rtl/dir'; 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'; +import {RepositionScrollStrategy, ScrollDispatcher} from '../core/overlay/index'; import {MdDatepickerInput} from './datepicker-input'; -import 'rxjs/add/operator/first'; import {Subscription} from 'rxjs/Subscription'; import {MdDialogConfig} from '../dialog/dialog-config'; import {DateAdapter} from '../core/datetime/index'; import {createMissingDateImplError} from './datepicker-errors'; import {ESCAPE} from '../core/keyboard/keycodes'; import {MdCalendar} from './calendar'; +import 'rxjs/add/operator/first'; /** Used to generate a unique ID for each datepicker instance. */ @@ -155,8 +153,11 @@ export class MdDatepicker implements OnDestroy { private _inputSubscription: Subscription; - constructor(private _dialog: MdDialog, private _overlay: Overlay, + constructor(private _dialog: MdDialog, + private _overlay: Overlay, + private _ngZone: NgZone, private _viewContainerRef: ViewContainerRef, + private _scrollDispatcher: ScrollDispatcher, @Optional() private _dateAdapter: DateAdapter, @Optional() private _dir: Dir) { if (!this._dateAdapter) { @@ -253,6 +254,9 @@ export class MdDatepicker implements OnDestroy { let componentRef: ComponentRef> = this._popupRef.attach(this._calendarPortal); componentRef.instance.datepicker = this; + + // Update the position once the calendar has rendered. + this._ngZone.onStable.first().subscribe(() => this._popupRef.updatePosition()); } this._popupRef.backdropClick().first().subscribe(() => this.close()); @@ -265,15 +269,29 @@ export class MdDatepicker implements OnDestroy { overlayState.hasBackdrop = true; overlayState.backdropClass = 'md-overlay-transparent-backdrop'; overlayState.direction = this._dir ? this._dir.value : 'ltr'; + overlayState.scrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher); 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._datepickerInput.getPopupConnectionElementRef(), origin, overlay); + return this._overlay.position() + .connectedTo(this._datepickerInput.getPopupConnectionElementRef(), + {originX: 'start', originY: 'bottom'}, + {overlayX: 'start', overlayY: 'top'} + ) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, + { overlayX: 'start', overlayY: 'bottom' } + ) + .withFallbackPosition( + {originX: 'end', originY: 'bottom'}, + {overlayX: 'end', overlayY: 'top'} + ) + .withFallbackPosition( + { originX: 'end', originY: 'top' }, + { overlayX: 'end', overlayY: 'bottom' } + ); } }