feat(datepicker): popup positioning improvements (#4696)

* 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.

* chore: unused imports
This commit is contained in:
Kristiyan Kostadinov
2017-05-26 20:34:29 +02:00
committed by tinayuangao
parent b2c3ed030d
commit eadccc2f76
3 changed files with 108 additions and 14 deletions
+3 -3
View File
@@ -42,9 +42,9 @@
</div>
<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
cdkMonitorSubtreeFocus>
[ngSwitch]="_monthView" cdkMonitorSubtreeFocus>
<md-month-view
*ngIf="_monthView"
*ngSwitchCase="true"
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
@@ -52,7 +52,7 @@
</md-month-view>
<md-year-view
*ngIf="!_monthView"
*ngSwitchDefault
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
+76
View File
@@ -569,6 +569,82 @@ describe('MdDatepicker', () => {
.toThrowError(/MdDatepicker: No provider found for .*/);
});
});
describe('popup positioning', () => {
let fixture: ComponentFixture<StandardDatepicker>;
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.');
});
});
});
+29 -11
View File
@@ -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<D> 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<D>,
@Optional() private _dir: Dir) {
if (!this._dateAdapter) {
@@ -253,6 +254,9 @@ export class MdDatepicker<D> implements OnDestroy {
let componentRef: ComponentRef<MdDatepickerContent<D>> =
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<D> 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' }
);
}
}