Skip to content

feat(datepicker): popup positioning improvements #4696

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/lib/datepicker/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
</div>

<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
cdkMonitorSubtreeFocus>
[ngSwitch]="_monthView" cdkMonitorSubtreeFocus>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we switch to ngSwitch (I'm fine with it, just curious) is it just a style thing to avoid repeating the condition?

Copy link
Member Author

@crisbeto crisbeto May 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just one less watcher in the template. It'll also be simpler to change if we add more views.

<md-month-view
*ngIf="_monthView"
*ngSwitchCase="true"
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
(selectedChange)="_dateSelected($event)">
</md-month-view>

<md-year-view
*ngIf="!_monthView"
*ngSwitchDefault
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
Expand Down
76 changes: 76 additions & 0 deletions src/lib/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nifty way of testing this, I like it :)

}));

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

});
});


Expand Down
40 changes: 29 additions & 11 deletions src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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. */
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand All @@ -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' }
);
}
}