Skip to content

Commit 7ae7919

Browse files
committed
feat(datepicker): allow for the dropdown position to be customized
Allows the consumer to customize the primary position of the datepicker in dropdown mode. Fixes #16550.
1 parent 29e74eb commit 7ae7919

File tree

3 files changed

+125
-41
lines changed

3 files changed

+125
-41
lines changed

src/material/datepicker/datepicker.spec.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
2121
import {MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig} from '@angular/material/dialog';
2222
import {Subject} from 'rxjs';
2323
import {MatInputModule} from '../input/index';
24-
import {MatDatepicker} from './datepicker';
24+
import {
25+
MatDatepicker,
26+
DatepickerDropdownPositionX,
27+
DatepickerDropdownPositionY,
28+
} from './datepicker';
2529
import {MatDatepickerInput} from './datepicker-input';
2630
import {MatDatepickerToggle} from './datepicker-toggle';
2731
import {MAT_DATEPICKER_SCROLL_STRATEGY, MatDatepickerIntl, MatDatepickerModule} from './index';
@@ -1689,6 +1693,36 @@ describe('MatDatepicker', () => {
16891693
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
16901694
});
16911695

1696+
it('should be able to customize the calendar position along the X axis', () => {
1697+
input.style.top = input.style.left = '200px';
1698+
testComponent.xPosition = 'end';
1699+
fixture.detectChanges();
1700+
1701+
testComponent.datepicker.open();
1702+
fixture.detectChanges();
1703+
1704+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1705+
const inputRect = input.getBoundingClientRect();
1706+
1707+
expect(Math.floor(overlayRect.right))
1708+
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
1709+
});
1710+
1711+
it('should be able to customize the calendar position along the Y axis', () => {
1712+
input.style.bottom = input.style.left = '100px';
1713+
testComponent.yPosition = 'above';
1714+
fixture.detectChanges();
1715+
1716+
testComponent.datepicker.open();
1717+
fixture.detectChanges();
1718+
1719+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1720+
const inputRect = input.getBoundingClientRect();
1721+
1722+
expect(Math.floor(overlayRect.bottom))
1723+
.toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.');
1724+
});
1725+
16921726
});
16931727

16941728
describe('internationalization', () => {
@@ -1786,7 +1820,13 @@ describe('MatDatepicker', () => {
17861820
@Component({
17871821
template: `
17881822
<input [matDatepicker]="d" [value]="date">
1789-
<mat-datepicker #d [touchUi]="touch" [disabled]="disabled" [opened]="opened"></mat-datepicker>
1823+
<mat-datepicker
1824+
#d
1825+
[touchUi]="touch"
1826+
[disabled]="disabled"
1827+
[opened]="opened"
1828+
[xPosition]="xPosition"
1829+
[yPosition]="yPosition"></mat-datepicker>
17901830
`,
17911831
})
17921832
class StandardDatepicker {
@@ -1796,6 +1836,8 @@ class StandardDatepicker {
17961836
date: Date | null = new Date(2020, JAN, 1);
17971837
@ViewChild('d') datepicker: MatDatepicker<Date>;
17981838
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
1839+
xPosition: DatepickerDropdownPositionX;
1840+
yPosition: DatepickerDropdownPositionY;
17991841
}
18001842

18011843

src/material/datepicker/datepicker.ts

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
Overlay,
1414
OverlayConfig,
1515
OverlayRef,
16-
PositionStrategy,
1716
ScrollStrategy,
17+
FlexibleConnectedPositionStrategy,
1818
} from '@angular/cdk/overlay';
1919
import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
2020
import {DOCUMENT} from '@angular/common';
@@ -36,6 +36,8 @@ import {
3636
ViewContainerRef,
3737
ViewEncapsulation,
3838
ChangeDetectorRef,
39+
OnChanges,
40+
SimpleChanges,
3941
} from '@angular/core';
4042
import {
4143
CanColor,
@@ -72,6 +74,12 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER = {
7274
useFactory: MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY,
7375
};
7476

77+
/** Possible positions for the datepicker dropdown along the X axis. */
78+
export type DatepickerDropdownPositionX = 'start' | 'end';
79+
80+
/** Possible positions for the datepicker dropdown along the Y axis. */
81+
export type DatepickerDropdownPositionY = 'above' | 'below';
82+
7583
// Boilerplate for applying mixins to MatDatepickerContent.
7684
/** @docs-private */
7785
class MatDatepickerContentBase {
@@ -164,7 +172,7 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
164172
changeDetection: ChangeDetectionStrategy.OnPush,
165173
encapsulation: ViewEncapsulation.None,
166174
})
167-
export class MatDatepicker<D> implements OnDestroy, CanColor {
175+
export class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
168176
private _scrollStrategy: () => ScrollStrategy;
169177

170178
/** An input indicating the type of the custom header component for the calendar, if set. */
@@ -223,6 +231,14 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
223231
}
224232
private _disabled: boolean;
225233

234+
/** Preferred position of the datepicker in the X axis. */
235+
@Input()
236+
xPosition: DatepickerDropdownPositionX = 'start';
237+
238+
/** Preferred position of the datepicker in the Y axis. */
239+
@Input()
240+
yPosition: DatepickerDropdownPositionY = 'below';
241+
226242
/**
227243
* Emits selected year in multiyear view.
228244
* This doesn't imply a change on the selected date.
@@ -315,6 +331,19 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
315331
this._scrollStrategy = scrollStrategy;
316332
}
317333

334+
ngOnChanges(changes: SimpleChanges) {
335+
const positionChange = changes['xPosition'] || changes['yPosition'];
336+
337+
if (positionChange && !positionChange.firstChange && this._popupRef) {
338+
this._setConnectedPositions(
339+
this._popupRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy);
340+
341+
if (this.opened) {
342+
this._popupRef.updatePosition();
343+
}
344+
}
345+
}
346+
318347
ngOnDestroy() {
319348
this._destroyPopup();
320349
this.close();
@@ -464,8 +493,15 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
464493

465494
/** Create the popup. */
466495
private _createPopup(): void {
496+
const positionStrategy = this._overlay.position()
497+
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
498+
.withTransformOriginOn('.mat-datepicker-content')
499+
.withFlexibleDimensions(false)
500+
.withViewportMargin(8)
501+
.withLockedPosition();
502+
467503
const overlayConfig = new OverlayConfig({
468-
positionStrategy: this._createPopupPositionStrategy(),
504+
positionStrategy: this._setConnectedPositions(positionStrategy),
469505
hasBackdrop: true,
470506
backdropClass: 'mat-overlay-transparent-backdrop',
471507
direction: this._dir,
@@ -501,40 +537,39 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
501537
}
502538
}
503539

504-
/** Create the popup PositionStrategy. */
505-
private _createPopupPositionStrategy(): PositionStrategy {
506-
return this._overlay.position()
507-
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
508-
.withTransformOriginOn('.mat-datepicker-content')
509-
.withFlexibleDimensions(false)
510-
.withViewportMargin(8)
511-
.withLockedPosition()
512-
.withPositions([
513-
{
514-
originX: 'start',
515-
originY: 'bottom',
516-
overlayX: 'start',
517-
overlayY: 'top'
518-
},
519-
{
520-
originX: 'start',
521-
originY: 'top',
522-
overlayX: 'start',
523-
overlayY: 'bottom'
524-
},
525-
{
526-
originX: 'end',
527-
originY: 'bottom',
528-
overlayX: 'end',
529-
overlayY: 'top'
530-
},
531-
{
532-
originX: 'end',
533-
originY: 'top',
534-
overlayX: 'end',
535-
overlayY: 'bottom'
536-
}
537-
]);
540+
/** Sets the positions of the datepicker in dropdown mode based on the current configuration. */
541+
private _setConnectedPositions(strategy: FlexibleConnectedPositionStrategy) {
542+
const primaryX = this.xPosition === 'end' ? 'end' : 'start';
543+
const secondaryX = primaryX === 'start' ? 'end' : 'start';
544+
const primaryY = this.yPosition === 'above' ? 'bottom' : 'top';
545+
const secondaryY = primaryY === 'top' ? 'bottom' : 'top';
546+
547+
return strategy.withPositions([
548+
{
549+
originX: primaryX,
550+
originY: secondaryY,
551+
overlayX: primaryX,
552+
overlayY: primaryY
553+
},
554+
{
555+
originX: primaryX,
556+
originY: primaryY,
557+
overlayX: primaryX,
558+
overlayY: secondaryY
559+
},
560+
{
561+
originX: secondaryX,
562+
originY: secondaryY,
563+
overlayX: secondaryX,
564+
overlayY: primaryY
565+
},
566+
{
567+
originX: secondaryX,
568+
originY: primaryY,
569+
overlayX: secondaryX,
570+
overlayY: secondaryY
571+
}
572+
]);
538573
}
539574

540575
/**

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export declare type DatepickerDropdownPositionX = 'start' | 'end';
2+
3+
export declare type DatepickerDropdownPositionY = 'above' | 'below';
4+
15
export declare const MAT_DATEPICKER_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;
26

37
export declare function MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy;
@@ -107,7 +111,7 @@ export declare class MatCalendarHeader<D> {
107111

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

110-
export declare class MatDatepicker<D> implements OnDestroy, CanColor {
114+
export declare class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
111115
_color: ThemePalette;
112116
get _dateFilter(): (date: D | null) => boolean;
113117
_datepickerInput: MatDatepickerInput<D>;
@@ -135,18 +139,21 @@ export declare class MatDatepicker<D> implements OnDestroy, CanColor {
135139
startView: 'month' | 'year' | 'multi-year';
136140
get touchUi(): boolean;
137141
set touchUi(value: boolean);
142+
xPosition: DatepickerDropdownPositionX;
143+
yPosition: DatepickerDropdownPositionY;
138144
readonly yearSelected: EventEmitter<D>;
139145
constructor(_dialog: MatDialog, _overlay: Overlay, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, scrollStrategy: any, _dateAdapter: DateAdapter<D>, _dir: Directionality, _document: any);
140146
_registerInput(input: MatDatepickerInput<D>): void;
141147
_selectMonth(normalizedMonth: D): void;
142148
_selectYear(normalizedYear: D): void;
143149
close(): void;
150+
ngOnChanges(changes: SimpleChanges): void;
144151
ngOnDestroy(): void;
145152
open(): void;
146153
select(date: D): void;
147154
static ngAcceptInputType_disabled: BooleanInput;
148155
static ngAcceptInputType_touchUi: BooleanInput;
149-
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatDatepicker<any>, "mat-datepicker", ["matDatepicker"], { "calendarHeaderComponent": "calendarHeaderComponent"; "startAt": "startAt"; "startView": "startView"; "color": "color"; "touchUi": "touchUi"; "disabled": "disabled"; "panelClass": "panelClass"; "dateClass": "dateClass"; "opened": "opened"; }, { "yearSelected": "yearSelected"; "monthSelected": "monthSelected"; "openedStream": "opened"; "closedStream": "closed"; }, never, never>;
156+
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatDatepicker<any>, "mat-datepicker", ["matDatepicker"], { "calendarHeaderComponent": "calendarHeaderComponent"; "startAt": "startAt"; "startView": "startView"; "color": "color"; "touchUi": "touchUi"; "disabled": "disabled"; "xPosition": "xPosition"; "yPosition": "yPosition"; "panelClass": "panelClass"; "dateClass": "dateClass"; "opened": "opened"; }, { "yearSelected": "yearSelected"; "monthSelected": "monthSelected"; "openedStream": "opened"; "closedStream": "closed"; }, never, never>;
150157
static ɵfac: i0.ɵɵFactoryDef<MatDatepicker<any>, [null, null, null, null, null, { optional: true; }, { optional: true; }, { optional: true; }]>;
151158
}
152159

0 commit comments

Comments
 (0)