Skip to content

Commit 659fb5a

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 8e321ae commit 659fb5a

File tree

3 files changed

+124
-40
lines changed

3 files changed

+124
-40
lines changed

src/material/datepicker/datepicker.spec.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/tes
2020
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
2121
import {Subject} from 'rxjs';
2222
import {MatInputModule} from '../input/index';
23-
import {MatDatepicker} from './datepicker';
23+
import {
24+
MatDatepicker,
25+
DatepickerDropdownPositionX,
26+
DatepickerDropdownPositionY,
27+
} from './datepicker';
2428
import {MatDatepickerInput} from './datepicker-input';
2529
import {MatDatepickerToggle} from './datepicker-toggle';
2630
import {MAT_DATEPICKER_SCROLL_STRATEGY, MatDatepickerIntl, MatDatepickerModule} from './index';
@@ -1617,6 +1621,36 @@ describe('MatDatepicker', () => {
16171621
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
16181622
});
16191623

1624+
it('should be able to customize the calendar position along the X axis', () => {
1625+
input.style.top = input.style.left = '200px';
1626+
testComponent.xPosition = 'end';
1627+
fixture.detectChanges();
1628+
1629+
testComponent.datepicker.open();
1630+
fixture.detectChanges();
1631+
1632+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1633+
const inputRect = input.getBoundingClientRect();
1634+
1635+
expect(Math.floor(overlayRect.right))
1636+
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
1637+
});
1638+
1639+
it('should be able to customize the calendar position along the Y axis', () => {
1640+
input.style.bottom = input.style.left = '100px';
1641+
testComponent.yPosition = 'above';
1642+
fixture.detectChanges();
1643+
1644+
testComponent.datepicker.open();
1645+
fixture.detectChanges();
1646+
1647+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1648+
const inputRect = input.getBoundingClientRect();
1649+
1650+
expect(Math.floor(overlayRect.bottom))
1651+
.toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.');
1652+
});
1653+
16201654
});
16211655

16221656
describe('internationalization', () => {
@@ -1696,7 +1730,13 @@ describe('MatDatepicker', () => {
16961730
@Component({
16971731
template: `
16981732
<input [matDatepicker]="d" [value]="date">
1699-
<mat-datepicker #d [touchUi]="touch" [disabled]="disabled" [opened]="opened"></mat-datepicker>
1733+
<mat-datepicker
1734+
#d
1735+
[touchUi]="touch"
1736+
[disabled]="disabled"
1737+
[opened]="opened"
1738+
[xPosition]="xPosition"
1739+
[yPosition]="yPosition"></mat-datepicker>
17001740
`,
17011741
})
17021742
class StandardDatepicker {
@@ -1706,6 +1746,8 @@ class StandardDatepicker {
17061746
date: Date | null = new Date(2020, JAN, 1);
17071747
@ViewChild('d', {static: false}) datepicker: MatDatepicker<Date>;
17081748
@ViewChild(MatDatepickerInput, {static: false}) datepickerInput: MatDatepickerInput<Date>;
1749+
xPosition: DatepickerDropdownPositionX;
1750+
yPosition: DatepickerDropdownPositionY;
17091751
}
17101752

17111753

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';
@@ -35,6 +35,8 @@ import {
3535
ViewChild,
3636
ViewContainerRef,
3737
ViewEncapsulation,
38+
OnChanges,
39+
SimpleChanges,
3840
} from '@angular/core';
3941
import {
4042
CanColor,
@@ -71,6 +73,12 @@ export const MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER = {
7173
useFactory: MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY,
7274
};
7375

76+
/** Possible positions for the datepicker dropdown along the X axis. */
77+
export type DatepickerDropdownPositionX = 'start' | 'end';
78+
79+
/** Possible positions for the datepicker dropdown along the Y axis. */
80+
export type DatepickerDropdownPositionY = 'above' | 'below';
81+
7482
// Boilerplate for applying mixins to MatDatepickerContent.
7583
/** @docs-private */
7684
class MatDatepickerContentBase {
@@ -139,7 +147,7 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
139147
changeDetection: ChangeDetectionStrategy.OnPush,
140148
encapsulation: ViewEncapsulation.None,
141149
})
142-
export class MatDatepicker<D> implements OnDestroy, CanColor {
150+
export class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
143151
private _scrollStrategy: () => ScrollStrategy;
144152

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

209+
/** Position of the datepicker in the X axis. */
210+
@Input()
211+
xPosition: DatepickerDropdownPositionX = 'start';
212+
213+
/** Position of the datepicker in the Y axis. */
214+
@Input()
215+
yPosition: DatepickerDropdownPositionY = 'below';
216+
201217
/**
202218
* Emits selected year in multiyear view.
203219
* This doesn't imply a change on the selected date.
@@ -293,6 +309,19 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
293309
this._scrollStrategy = scrollStrategy;
294310
}
295311

312+
ngOnChanges(changes: SimpleChanges) {
313+
const positionChange = changes['xPosition'] || changes['yPosition'];
314+
315+
if (positionChange && !positionChange.firstChange && this._popupRef) {
316+
this._setConnectedPositions(
317+
this._popupRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy);
318+
319+
if (this.opened) {
320+
this._popupRef.updatePosition();
321+
}
322+
}
323+
}
324+
296325
ngOnDestroy() {
297326
this.close();
298327
this._inputSubscription.unsubscribe();
@@ -439,8 +468,15 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
439468

440469
/** Create the popup. */
441470
private _createPopup(): void {
471+
const positionStrategy = this._overlay.position()
472+
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
473+
.withTransformOriginOn('.mat-datepicker-content')
474+
.withFlexibleDimensions(false)
475+
.withViewportMargin(8)
476+
.withLockedPosition();
477+
442478
const overlayConfig = new OverlayConfig({
443-
positionStrategy: this._createPopupPositionStrategy(),
479+
positionStrategy: this._setConnectedPositions(positionStrategy),
444480
hasBackdrop: true,
445481
backdropClass: 'mat-overlay-transparent-backdrop',
446482
direction: this._dir,
@@ -468,40 +504,39 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
468504
});
469505
}
470506

471-
/** Create the popup PositionStrategy. */
472-
private _createPopupPositionStrategy(): PositionStrategy {
473-
return this._overlay.position()
474-
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
475-
.withTransformOriginOn('.mat-datepicker-content')
476-
.withFlexibleDimensions(false)
477-
.withViewportMargin(8)
478-
.withLockedPosition()
479-
.withPositions([
480-
{
481-
originX: 'start',
482-
originY: 'bottom',
483-
overlayX: 'start',
484-
overlayY: 'top'
485-
},
486-
{
487-
originX: 'start',
488-
originY: 'top',
489-
overlayX: 'start',
490-
overlayY: 'bottom'
491-
},
492-
{
493-
originX: 'end',
494-
originY: 'bottom',
495-
overlayX: 'end',
496-
overlayY: 'top'
497-
},
498-
{
499-
originX: 'end',
500-
originY: 'top',
501-
overlayX: 'end',
502-
overlayY: 'bottom'
503-
}
504-
]);
507+
/** Sets the positions of the datepicker in dropdown mode based on the current configuration. */
508+
private _setConnectedPositions(strategy: FlexibleConnectedPositionStrategy) {
509+
const primaryX = this.xPosition === 'end' ? 'end' : 'start';
510+
const secondaryX = primaryX === 'start' ? 'end' : 'start';
511+
const primaryY = this.yPosition === 'above' ? 'bottom' : 'top';
512+
const secondaryY = primaryY === 'top' ? 'bottom' : 'top';
513+
514+
return strategy.withPositions([
515+
{
516+
originX: primaryX,
517+
originY: secondaryY,
518+
overlayX: primaryX,
519+
overlayY: primaryY
520+
},
521+
{
522+
originX: primaryX,
523+
originY: primaryY,
524+
overlayX: primaryX,
525+
overlayY: secondaryY
526+
},
527+
{
528+
originX: secondaryX,
529+
originY: secondaryY,
530+
overlayX: secondaryX,
531+
overlayY: primaryY
532+
},
533+
{
534+
originX: secondaryX,
535+
originY: primaryY,
536+
overlayX: secondaryX,
537+
overlayY: secondaryY
538+
}
539+
]);
505540
}
506541

507542
/**

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 8 additions & 1 deletion
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;
@@ -99,7 +103,7 @@ export declare class MatCalendarHeader<D> {
99103

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

102-
export declare class MatDatepicker<D> implements OnDestroy, CanColor {
106+
export declare class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
103107
_color: ThemePalette;
104108
readonly _dateFilter: (date: D | null) => boolean;
105109
_datepickerInput: MatDatepickerInput<D>;
@@ -122,12 +126,15 @@ export declare class MatDatepicker<D> implements OnDestroy, CanColor {
122126
startAt: D | null;
123127
startView: 'month' | 'year' | 'multi-year';
124128
touchUi: boolean;
129+
xPosition: DatepickerDropdownPositionX;
130+
yPosition: DatepickerDropdownPositionY;
125131
readonly yearSelected: EventEmitter<D>;
126132
constructor(_dialog: MatDialog, _overlay: Overlay, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, scrollStrategy: any, _dateAdapter: DateAdapter<D>, _dir: Directionality, _document: any);
127133
_registerInput(input: MatDatepickerInput<D>): void;
128134
_selectMonth(normalizedMonth: D): void;
129135
_selectYear(normalizedYear: D): void;
130136
close(): void;
137+
ngOnChanges(changes: SimpleChanges): void;
131138
ngOnDestroy(): void;
132139
open(): void;
133140
select(date: D): void;

0 commit comments

Comments
 (0)