From 169ad6a4b3b2ff6c0a8d012d0d7369213db900ba Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 17 May 2017 23:52:48 +0200 Subject: [PATCH 1/2] feat(datepicker): popup positioning improvements * Adds fallback positions to the datepicker popup, allowing it to reposition in the cases where it would've gone outside the viewport. * Fixes an issue that prevented the datepicker from the being positioned properly on the first open due to the calendar not being rendered yet. * Adds a scroll strategy to reposition the datepicker when the user scrolls away. * Switches to using `ngSwitch` instead of `ngIf` when determining which view to render. Fixes #4406. --- src/lib/datepicker/calendar.html | 6 +-- src/lib/datepicker/datepicker.spec.ts | 76 +++++++++++++++++++++++++++ src/lib/datepicker/datepicker.ts | 42 +++++++++++---- 3 files changed, 112 insertions(+), 12 deletions(-) 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..fd5d86fd6efe 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'; @@ -22,16 +23,19 @@ import {MdDialogRef} from '../dialog/dialog-ref'; import {PositionStrategy} from '../core/overlay/position/position-strategy'; import { OriginConnectionPosition, - OverlayConnectionPosition -} from '../core/overlay/position/connected-position'; + OverlayConnectionPosition, + RepositionScrollStrategy, + BlockScrollStrategy, + 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 +159,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 +260,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 +275,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' } + ); } } From 4a47f8907c147b340a2cd8084775ad5449cfe308 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 22 May 2017 19:27:13 +0200 Subject: [PATCH 2/2] chore: unused imports --- src/lib/datepicker/datepicker.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index fd5d86fd6efe..bddb7c701adb 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -21,13 +21,7 @@ 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, - RepositionScrollStrategy, - BlockScrollStrategy, - ScrollDispatcher, -} from '../core/overlay/index'; +import {RepositionScrollStrategy, ScrollDispatcher} from '../core/overlay/index'; import {MdDatepickerInput} from './datepicker-input'; import {Subscription} from 'rxjs/Subscription'; import {MdDialogConfig} from '../dialog/dialog-config';