Skip to content

Commit 77a3387

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 93dc69f commit 77a3387

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';
@@ -1640,6 +1644,36 @@ describe('MatDatepicker', () => {
16401644
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
16411645
});
16421646

1647+
it('should be able to customize the calendar position along the X axis', () => {
1648+
input.style.top = input.style.left = '200px';
1649+
testComponent.xPosition = 'end';
1650+
fixture.detectChanges();
1651+
1652+
testComponent.datepicker.open();
1653+
fixture.detectChanges();
1654+
1655+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1656+
const inputRect = input.getBoundingClientRect();
1657+
1658+
expect(Math.floor(overlayRect.right))
1659+
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
1660+
});
1661+
1662+
it('should be able to customize the calendar position along the Y axis', () => {
1663+
input.style.bottom = input.style.left = '100px';
1664+
testComponent.yPosition = 'above';
1665+
fixture.detectChanges();
1666+
1667+
testComponent.datepicker.open();
1668+
fixture.detectChanges();
1669+
1670+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1671+
const inputRect = input.getBoundingClientRect();
1672+
1673+
expect(Math.floor(overlayRect.bottom))
1674+
.toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.');
1675+
});
1676+
16431677
});
16441678

16451679
describe('internationalization', () => {
@@ -1719,7 +1753,13 @@ describe('MatDatepicker', () => {
17191753
@Component({
17201754
template: `
17211755
<input [matDatepicker]="d" [value]="date">
1722-
<mat-datepicker #d [touchUi]="touch" [disabled]="disabled" [opened]="opened"></mat-datepicker>
1756+
<mat-datepicker
1757+
#d
1758+
[touchUi]="touch"
1759+
[disabled]="disabled"
1760+
[opened]="opened"
1761+
[xPosition]="xPosition"
1762+
[yPosition]="yPosition"></mat-datepicker>
17231763
`,
17241764
})
17251765
class StandardDatepicker {
@@ -1729,6 +1769,8 @@ class StandardDatepicker {
17291769
date: Date | null = new Date(2020, JAN, 1);
17301770
@ViewChild('d') datepicker: MatDatepicker<Date>;
17311771
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
1772+
xPosition: DatepickerDropdownPositionX;
1773+
yPosition: DatepickerDropdownPositionY;
17321774
}
17331775

17341776

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 {
@@ -137,7 +145,7 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
137145
changeDetection: ChangeDetectionStrategy.OnPush,
138146
encapsulation: ViewEncapsulation.None,
139147
})
140-
export class MatDatepicker<D> implements OnDestroy, CanColor {
148+
export class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
141149
private _scrollStrategy: () => ScrollStrategy;
142150

143151
/** An input indicating the type of the custom header component for the calendar, if set. */
@@ -196,6 +204,14 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
196204
}
197205
private _disabled: boolean;
198206

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

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

438467
/** Create the popup. */
439468
private _createPopup(): void {
469+
const positionStrategy = this._overlay.position()
470+
.flexibleConnectedTo(this._datepickerInput.getConnectedOverlayOrigin())
471+
.withTransformOriginOn('.mat-datepicker-content')
472+
.withFlexibleDimensions(false)
473+
.withViewportMargin(8)
474+
.withLockedPosition();
475+
440476
const overlayConfig = new OverlayConfig({
441-
positionStrategy: this._createPopupPositionStrategy(),
477+
positionStrategy: this._setConnectedPositions(positionStrategy),
442478
hasBackdrop: true,
443479
backdropClass: 'mat-overlay-transparent-backdrop',
444480
direction: this._dir,
@@ -466,40 +502,39 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
466502
});
467503
}
468504

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

505540
/**

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;
@@ -101,7 +105,7 @@ export declare class MatCalendarHeader<D> {
101105

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

104-
export declare class MatDatepicker<D> implements OnDestroy, CanColor {
108+
export declare class MatDatepicker<D> implements OnDestroy, CanColor, OnChanges {
105109
_color: ThemePalette;
106110
readonly _dateFilter: (date: D | null) => boolean;
107111
_datepickerInput: MatDatepickerInput<D>;
@@ -124,12 +128,15 @@ export declare class MatDatepicker<D> implements OnDestroy, CanColor {
124128
startAt: D | null;
125129
startView: 'month' | 'year' | 'multi-year';
126130
touchUi: boolean;
131+
xPosition: DatepickerDropdownPositionX;
132+
yPosition: DatepickerDropdownPositionY;
127133
readonly yearSelected: EventEmitter<D>;
128134
constructor(_dialog: MatDialog, _overlay: Overlay, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, scrollStrategy: any, _dateAdapter: DateAdapter<D>, _dir: Directionality, _document: any);
129135
_registerInput(input: MatDatepickerInput<D>): void;
130136
_selectMonth(normalizedMonth: D): void;
131137
_selectYear(normalizedYear: D): void;
132138
close(): void;
139+
ngOnChanges(changes: SimpleChanges): void;
133140
ngOnDestroy(): void;
134141
open(): void;
135142
select(date: D): void;

0 commit comments

Comments
 (0)