Skip to content

Commit 9104a0b

Browse files
authored
feat(datepicker): allow for the dropdown position to be customized (#16698)
Allows the consumer to customize the primary position of the datepicker in dropdown mode. Fixes #16550.
1 parent 6a64130 commit 9104a0b

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';
@@ -1728,6 +1732,36 @@ describe('MatDatepicker', () => {
17281732
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
17291733
});
17301734

1735+
it('should be able to customize the calendar position along the X axis', () => {
1736+
input.style.top = input.style.left = '200px';
1737+
testComponent.xPosition = 'end';
1738+
fixture.detectChanges();
1739+
1740+
testComponent.datepicker.open();
1741+
fixture.detectChanges();
1742+
1743+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1744+
const inputRect = input.getBoundingClientRect();
1745+
1746+
expect(Math.floor(overlayRect.right))
1747+
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
1748+
});
1749+
1750+
it('should be able to customize the calendar position along the Y axis', () => {
1751+
input.style.bottom = input.style.left = '100px';
1752+
testComponent.yPosition = 'above';
1753+
fixture.detectChanges();
1754+
1755+
testComponent.datepicker.open();
1756+
fixture.detectChanges();
1757+
1758+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1759+
const inputRect = input.getBoundingClientRect();
1760+
1761+
expect(Math.floor(overlayRect.bottom))
1762+
.toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.');
1763+
});
1764+
17311765
});
17321766

17331767
describe('internationalization', () => {
@@ -1825,7 +1859,13 @@ describe('MatDatepicker', () => {
18251859
@Component({
18261860
template: `
18271861
<input [matDatepicker]="d" [value]="date">
1828-
<mat-datepicker #d [touchUi]="touch" [disabled]="disabled" [opened]="opened"></mat-datepicker>
1862+
<mat-datepicker
1863+
#d
1864+
[touchUi]="touch"
1865+
[disabled]="disabled"
1866+
[opened]="opened"
1867+
[xPosition]="xPosition"
1868+
[yPosition]="yPosition"></mat-datepicker>
18291869
`,
18301870
})
18311871
class StandardDatepicker {
@@ -1835,6 +1875,8 @@ class StandardDatepicker {
18351875
date: Date | null = new Date(2020, JAN, 1);
18361876
@ViewChild('d') datepicker: MatDatepicker<Date>;
18371877
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
1878+
xPosition: DatepickerDropdownPositionX;
1879+
yPosition: DatepickerDropdownPositionY;
18381880
}
18391881

18401882

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();
@@ -471,8 +500,15 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
471500

472501
/** Create the popup. */
473502
private _createPopup(): void {
503+
const positionStrategy = this._overlay.position()
504+
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
505+
.withTransformOriginOn('.mat-datepicker-content')
506+
.withFlexibleDimensions(false)
507+
.withViewportMargin(8)
508+
.withLockedPosition();
509+
474510
const overlayConfig = new OverlayConfig({
475-
positionStrategy: this._createPopupPositionStrategy(),
511+
positionStrategy: this._setConnectedPositions(positionStrategy),
476512
hasBackdrop: true,
477513
backdropClass: 'mat-overlay-transparent-backdrop',
478514
direction: this._dir,
@@ -508,40 +544,39 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
508544
}
509545
}
510546

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

547582
/**

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)