mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
tinayuangao
parent
b2c3ed030d
commit
eadccc2f76
@@ -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"
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user