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