Skip to content

Commit

Permalink
feat(datepicker): allow for the dropdown position to be customized
Browse files Browse the repository at this point in the history
Allows the consumer to customize the primary position of the datepicker in dropdown mode.

Fixes #16550.
  • Loading branch information
crisbeto committed Aug 6, 2019
1 parent 8e321ae commit 605993f
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 40 deletions.
46 changes: 44 additions & 2 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/tes
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Subject} from 'rxjs';
import {MatInputModule} from '../input/index';
import {MatDatepicker} from './datepicker';
import {
MatDatepicker,
DatepickerDropdownPositionX,
DatepickerDropdownPositionY,
} from './datepicker';
import {MatDatepickerInput} from './datepicker-input';
import {MatDatepickerToggle} from './datepicker-toggle';
import {MAT_DATEPICKER_SCROLL_STRATEGY, MatDatepickerIntl, MatDatepickerModule} from './index';
Expand Down Expand Up @@ -1617,6 +1621,36 @@ describe('MatDatepicker', () => {
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
});

it('should be able to customize the calendar position along the X axis', () => {
input.style.top = input.style.left = '200px';
testComponent.xPosition = 'end';
fixture.detectChanges();

testComponent.datepicker.open();
fixture.detectChanges();

const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
const inputRect = input.getBoundingClientRect();

expect(Math.floor(overlayRect.right))
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
});

it('should be able to customize the calendar position along the Y axis', () => {
input.style.bottom = input.style.left = '200px';
testComponent.yPosition = 'above';
fixture.detectChanges();

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

});

describe('internationalization', () => {
Expand Down Expand Up @@ -1696,7 +1730,13 @@ describe('MatDatepicker', () => {
@Component({
template: `
<input [matDatepicker]="d" [value]="date">
<mat-datepicker #d [touchUi]="touch" [disabled]="disabled" [opened]="opened"></mat-datepicker>
<mat-datepicker
#d
[touchUi]="touch"
[disabled]="disabled"
[opened]="opened"
[xPosition]="xPosition"
[yPosition]="yPosition"></mat-datepicker>
`,
})
class StandardDatepicker {
Expand All @@ -1706,6 +1746,8 @@ class StandardDatepicker {
date: Date | null = new Date(2020, JAN, 1);
@ViewChild('d', {static: false}) datepicker: MatDatepicker<Date>;
@ViewChild(MatDatepickerInput, {static: false}) datepickerInput: MatDatepickerInput<Date>;
xPosition: DatepickerDropdownPositionX;
yPosition: DatepickerDropdownPositionY;
}


Expand Down
109 changes: 72 additions & 37 deletions src/material/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
Overlay,
OverlayConfig,
OverlayRef,
PositionStrategy,
ScrollStrategy,
FlexibleConnectedPositionStrategy,
} from '@angular/cdk/overlay';
import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
Expand All @@ -35,6 +35,8 @@ import {
ViewChild,
ViewContainerRef,
ViewEncapsulation,
OnChanges,
SimpleChanges,
} from '@angular/core';
import {
CanColor,
Expand Down Expand Up @@ -71,6 +73,12 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER = {
useFactory: MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY,
};

/** Possible positions for the datepicker dropdown along the X axis. */
export type DatepickerDropdownPositionX = 'start' | 'end';

/** Possible positions for the datepicker dropdown along the Y axis. */
export type DatepickerDropdownPositionY = 'above' | 'below';

// Boilerplate for applying mixins to MatDatepickerContent.
/** @docs-private */
class MatDatepickerContentBase {
Expand Down Expand Up @@ -139,7 +147,7 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class MatDatepicker<D> implements OnDestroy, CanColor {
export class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
private _scrollStrategy: () => ScrollStrategy;

/** An input indicating the type of the custom header component for the calendar, if set. */
Expand Down Expand Up @@ -198,6 +206,14 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
}
private _disabled: boolean;

/** Position of the datepicker in the X axis. */
@Input()
xPosition: DatepickerDropdownPositionX = 'start';

/** Position of the datepicker in the Y axis. */
@Input()
yPosition: DatepickerDropdownPositionY = 'below';

/**
* Emits selected year in multiyear view.
* This doesn't imply a change on the selected date.
Expand Down Expand Up @@ -293,6 +309,19 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
this._scrollStrategy = scrollStrategy;
}

ngOnChanges(changes: SimpleChanges) {
const positionChange = changes['xPosition'] || changes['yPosition'];

if (positionChange && !positionChange.firstChange && this._popupRef) {
this._setConnectedPositions(
this._popupRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy);

if (this.opened) {
this._popupRef.updatePosition();
}
}
}

ngOnDestroy() {
this.close();
this._inputSubscription.unsubscribe();
Expand Down Expand Up @@ -439,8 +468,15 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {

/** Create the popup. */
private _createPopup(): void {
const positionStrategy = this._overlay.position()
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
.withTransformOriginOn('.mat-datepicker-content')
.withFlexibleDimensions(false)
.withViewportMargin(8)
.withLockedPosition();

const overlayConfig = new OverlayConfig({
positionStrategy: this._createPopupPositionStrategy(),
positionStrategy: this._setConnectedPositions(positionStrategy),
hasBackdrop: true,
backdropClass: 'mat-overlay-transparent-backdrop',
direction: this._dir,
Expand Down Expand Up @@ -468,40 +504,39 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
});
}

/** Create the popup PositionStrategy. */
private _createPopupPositionStrategy(): PositionStrategy {
return this._overlay.position()
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
.withTransformOriginOn('.mat-datepicker-content')
.withFlexibleDimensions(false)
.withViewportMargin(8)
.withLockedPosition()
.withPositions([
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom'
},
{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
},
{
originX: 'end',
originY: 'top',
overlayX: 'end',
overlayY: 'bottom'
}
]);
/** Sets the positions of the datepicker in dropdown mode based on the current configuration. */
private _setConnectedPositions(strategy: FlexibleConnectedPositionStrategy) {
const primaryX = this.xPosition === 'end' ? 'end' : 'start';
const secondaryX = primaryX === 'start' ? 'end' : 'start';
const primaryY = this.yPosition === 'above' ? 'bottom' : 'top';
const secondaryY = primaryY === 'top' ? 'bottom' : 'top';

return strategy.withPositions([
{
originX: primaryX,
originY: secondaryY,
overlayX: primaryX,
overlayY: primaryY
},
{
originX: primaryX,
originY: primaryY,
overlayX: primaryX,
overlayY: secondaryY
},
{
originX: secondaryX,
originY: secondaryY,
overlayX: secondaryX,
overlayY: primaryY
},
{
originX: secondaryX,
originY: primaryY,
overlayX: secondaryX,
overlayY: secondaryY
}
]);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion tools/public_api_guard/material/datepicker.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export declare type DatepickerDropdownPositionX = 'start' | 'end';

export declare type DatepickerDropdownPositionY = 'above' | 'below';

export declare const MAT_DATEPICKER_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;

export declare function MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy;
Expand Down Expand Up @@ -99,7 +103,7 @@ export declare class MatCalendarHeader<D> {

export declare type MatCalendarView = 'month' | 'year' | 'multi-year';

export declare class MatDatepicker<D> implements OnDestroy, CanColor {
export declare class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
_color: ThemePalette;
readonly _dateFilter: (date: D | null) => boolean;
_datepickerInput: MatDatepickerInput<D>;
Expand All @@ -122,12 +126,15 @@ export declare class MatDatepicker<D> implements OnDestroy, CanColor {
startAt: D | null;
startView: 'month' | 'year' | 'multi-year';
touchUi: boolean;
xPosition: DatepickerDropdownPositionX;
yPosition: DatepickerDropdownPositionY;
readonly yearSelected: EventEmitter<D>;
constructor(_dialog: MatDialog, _overlay: Overlay, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, scrollStrategy: any, _dateAdapter: DateAdapter<D>, _dir: Directionality, _document: any);
_registerInput(input: MatDatepickerInput<D>): void;
_selectMonth(normalizedMonth: D): void;
_selectYear(normalizedYear: D): void;
close(): void;
ngOnChanges(changes: SimpleChanges): void;
ngOnDestroy(): void;
open(): void;
select(date: D): void;
Expand Down

0 comments on commit 605993f

Please sign in to comment.