Skip to content

Commit 64992fd

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 09dc459 commit 64992fd

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
@@ -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';
@@ -1678,6 +1682,36 @@ describe('MatDatepicker', () => {
16781682
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
16791683
});
16801684

1685+
it('should be able to customize the calendar position along the X axis', () => {
1686+
input.style.top = input.style.left = '200px';
1687+
testComponent.xPosition = 'end';
1688+
fixture.detectChanges();
1689+
1690+
testComponent.datepicker.open();
1691+
fixture.detectChanges();
1692+
1693+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1694+
const inputRect = input.getBoundingClientRect();
1695+
1696+
expect(Math.floor(overlayRect.right))
1697+
.toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.');
1698+
});
1699+
1700+
it('should be able to customize the calendar position along the Y axis', () => {
1701+
input.style.bottom = input.style.left = '100px';
1702+
testComponent.yPosition = 'above';
1703+
fixture.detectChanges();
1704+
1705+
testComponent.datepicker.open();
1706+
fixture.detectChanges();
1707+
1708+
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
1709+
const inputRect = input.getBoundingClientRect();
1710+
1711+
expect(Math.floor(overlayRect.bottom))
1712+
.toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.');
1713+
});
1714+
16811715
});
16821716

16831717
describe('internationalization', () => {
@@ -1757,7 +1791,13 @@ describe('MatDatepicker', () => {
17571791
@Component({
17581792
template: `
17591793
<input [matDatepicker]="d" [value]="date">
1760-
<mat-datepicker #d [touchUi]="touch" [disabled]="disabled" [opened]="opened"></mat-datepicker>
1794+
<mat-datepicker
1795+
#d
1796+
[touchUi]="touch"
1797+
[disabled]="disabled"
1798+
[opened]="opened"
1799+
[xPosition]="xPosition"
1800+
[yPosition]="yPosition"></mat-datepicker>
17611801
`,
17621802
})
17631803
class StandardDatepicker {
@@ -1767,6 +1807,8 @@ class StandardDatepicker {
17671807
date: Date | null = new Date(2020, JAN, 1);
17681808
@ViewChild('d') datepicker: MatDatepicker<Date>;
17691809
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
1810+
xPosition: DatepickerDropdownPositionX;
1811+
yPosition: DatepickerDropdownPositionY;
17701812
}
17711813

17721814

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: 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;
@@ -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,18 +128,21 @@ 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;
136143
static ngAcceptInputType_disabled: BooleanInput;
137144
static ngAcceptInputType_touchUi: BooleanInput;
138-
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>;
145+
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>;
139146
static ɵfac: i0.ɵɵFactoryDef<MatDatepicker<any>>;
140147
}
141148

0 commit comments

Comments
 (0)