Skip to content

Commit eadccc2

Browse files
crisbetotinayuangao
authored andcommitted
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
1 parent b2c3ed0 commit eadccc2

File tree

3 files changed

+108
-14
lines changed

3 files changed

+108
-14
lines changed

src/lib/datepicker/calendar.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,17 @@
4242
</div>
4343

4444
<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
45-
cdkMonitorSubtreeFocus>
45+
[ngSwitch]="_monthView" cdkMonitorSubtreeFocus>
4646
<md-month-view
47-
*ngIf="_monthView"
47+
*ngSwitchCase="true"
4848
[activeDate]="_activeDate"
4949
[selected]="selected"
5050
[dateFilter]="_dateFilterForViews"
5151
(selectedChange)="_dateSelected($event)">
5252
</md-month-view>
5353

5454
<md-year-view
55-
*ngIf="!_monthView"
55+
*ngSwitchDefault
5656
[activeDate]="_activeDate"
5757
[selected]="selected"
5858
[dateFilter]="_dateFilterForViews"

src/lib/datepicker/datepicker.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,82 @@ describe('MdDatepicker', () => {
569569
.toThrowError(/MdDatepicker: No provider found for .*/);
570570
});
571571
});
572+
573+
describe('popup positioning', () => {
574+
let fixture: ComponentFixture<StandardDatepicker>;
575+
let testComponent: StandardDatepicker;
576+
let input: HTMLElement;
577+
578+
beforeEach(async(() => {
579+
TestBed.configureTestingModule({
580+
imports: [MdDatepickerModule, MdInputModule, MdNativeDateModule, NoopAnimationsModule],
581+
declarations: [StandardDatepicker],
582+
}).compileComponents();
583+
584+
fixture = TestBed.createComponent(StandardDatepicker);
585+
fixture.detectChanges();
586+
testComponent = fixture.componentInstance;
587+
input = fixture.debugElement.query(By.css('input')).nativeElement;
588+
input.style.position = 'fixed';
589+
}));
590+
591+
it('should be below and to the right when there is plenty of space', () => {
592+
input.style.top = input.style.left = '20px';
593+
testComponent.datepicker.open();
594+
fixture.detectChanges();
595+
596+
const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect();
597+
const inputRect = input.getBoundingClientRect();
598+
599+
expect(Math.floor(overlayRect.top))
600+
.toBe(Math.floor(inputRect.bottom), 'Expected popup to align to input bottom.');
601+
expect(Math.floor(overlayRect.left))
602+
.toBe(Math.floor(inputRect.left), 'Expected popup to align to input left.');
603+
});
604+
605+
it('should be above and to the right when there is no space below', () => {
606+
input.style.bottom = input.style.left = '20px';
607+
testComponent.datepicker.open();
608+
fixture.detectChanges();
609+
610+
const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect();
611+
const inputRect = input.getBoundingClientRect();
612+
613+
expect(Math.floor(overlayRect.bottom))
614+
.toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.');
615+
expect(Math.floor(overlayRect.left))
616+
.toBe(Math.floor(inputRect.left), 'Expected popup to align to input left.');
617+
});
618+
619+
it('should be below and to the left when there is no space on the right', () => {
620+
input.style.top = input.style.right = '20px';
621+
testComponent.datepicker.open();
622+
fixture.detectChanges();
623+
624+
const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect();
625+
const inputRect = input.getBoundingClientRect();
626+
627+
expect(Math.floor(overlayRect.top))
628+
.toBe(Math.floor(inputRect.bottom), 'Expected popup to align to input bottom.');
629+
expect(Math.floor(overlayRect.right))
630+
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
631+
});
632+
633+
it('should be above and to the left when there is no space on the bottom', () => {
634+
input.style.bottom = input.style.right = '20px';
635+
testComponent.datepicker.open();
636+
fixture.detectChanges();
637+
638+
const overlayRect = document.querySelector('.cdk-overlay-pane').getBoundingClientRect();
639+
const inputRect = input.getBoundingClientRect();
640+
641+
expect(Math.floor(overlayRect.bottom))
642+
.toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.');
643+
expect(Math.floor(overlayRect.right))
644+
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
645+
});
646+
647+
});
572648
});
573649

574650

src/lib/datepicker/datepicker.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
Output,
1111
ViewChild,
1212
ViewContainerRef,
13-
ViewEncapsulation
13+
ViewEncapsulation,
14+
NgZone,
1415
} from '@angular/core';
1516
import {Overlay} from '../core/overlay/overlay';
1617
import {OverlayRef} from '../core/overlay/overlay-ref';
@@ -20,18 +21,15 @@ import {Dir} from '../core/rtl/dir';
2021
import {MdDialog} from '../dialog/dialog';
2122
import {MdDialogRef} from '../dialog/dialog-ref';
2223
import {PositionStrategy} from '../core/overlay/position/position-strategy';
23-
import {
24-
OriginConnectionPosition,
25-
OverlayConnectionPosition
26-
} from '../core/overlay/position/connected-position';
24+
import {RepositionScrollStrategy, ScrollDispatcher} from '../core/overlay/index';
2725
import {MdDatepickerInput} from './datepicker-input';
28-
import 'rxjs/add/operator/first';
2926
import {Subscription} from 'rxjs/Subscription';
3027
import {MdDialogConfig} from '../dialog/dialog-config';
3128
import {DateAdapter} from '../core/datetime/index';
3229
import {createMissingDateImplError} from './datepicker-errors';
3330
import {ESCAPE} from '../core/keyboard/keycodes';
3431
import {MdCalendar} from './calendar';
32+
import 'rxjs/add/operator/first';
3533

3634

3735
/** Used to generate a unique ID for each datepicker instance. */
@@ -155,8 +153,11 @@ export class MdDatepicker<D> implements OnDestroy {
155153

156154
private _inputSubscription: Subscription;
157155

158-
constructor(private _dialog: MdDialog, private _overlay: Overlay,
156+
constructor(private _dialog: MdDialog,
157+
private _overlay: Overlay,
158+
private _ngZone: NgZone,
159159
private _viewContainerRef: ViewContainerRef,
160+
private _scrollDispatcher: ScrollDispatcher,
160161
@Optional() private _dateAdapter: DateAdapter<D>,
161162
@Optional() private _dir: Dir) {
162163
if (!this._dateAdapter) {
@@ -253,6 +254,9 @@ export class MdDatepicker<D> implements OnDestroy {
253254
let componentRef: ComponentRef<MdDatepickerContent<D>> =
254255
this._popupRef.attach(this._calendarPortal);
255256
componentRef.instance.datepicker = this;
257+
258+
// Update the position once the calendar has rendered.
259+
this._ngZone.onStable.first().subscribe(() => this._popupRef.updatePosition());
256260
}
257261

258262
this._popupRef.backdropClick().first().subscribe(() => this.close());
@@ -265,15 +269,29 @@ export class MdDatepicker<D> implements OnDestroy {
265269
overlayState.hasBackdrop = true;
266270
overlayState.backdropClass = 'md-overlay-transparent-backdrop';
267271
overlayState.direction = this._dir ? this._dir.value : 'ltr';
272+
overlayState.scrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher);
268273

269274
this._popupRef = this._overlay.create(overlayState);
270275
}
271276

272277
/** Create the popup PositionStrategy. */
273278
private _createPopupPositionStrategy(): PositionStrategy {
274-
let origin = {originX: 'start', originY: 'bottom'} as OriginConnectionPosition;
275-
let overlay = {overlayX: 'start', overlayY: 'top'} as OverlayConnectionPosition;
276-
return this._overlay.position().connectedTo(
277-
this._datepickerInput.getPopupConnectionElementRef(), origin, overlay);
279+
return this._overlay.position()
280+
.connectedTo(this._datepickerInput.getPopupConnectionElementRef(),
281+
{originX: 'start', originY: 'bottom'},
282+
{overlayX: 'start', overlayY: 'top'}
283+
)
284+
.withFallbackPosition(
285+
{ originX: 'start', originY: 'top' },
286+
{ overlayX: 'start', overlayY: 'bottom' }
287+
)
288+
.withFallbackPosition(
289+
{originX: 'end', originY: 'bottom'},
290+
{overlayX: 'end', overlayY: 'top'}
291+
)
292+
.withFallbackPosition(
293+
{ originX: 'end', originY: 'top' },
294+
{ overlayX: 'end', overlayY: 'bottom' }
295+
);
278296
}
279297
}

0 commit comments

Comments
 (0)