sass-references/angular-material/material/datepicker/datepicker-base.ts

867 lines
28 KiB
TypeScript
Raw Normal View History

2024-12-06 10:42:08 +08:00
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {AnimationEvent} from '@angular/animations';
import {_IdGenerator, CdkTrapFocus} from '@angular/cdk/a11y';
import {Directionality} from '@angular/cdk/bidi';
import {coerceStringArray} from '@angular/cdk/coercion';
import {
DOWN_ARROW,
ESCAPE,
hasModifierKey,
LEFT_ARROW,
ModifierKey,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {
FlexibleConnectedPositionStrategy,
Overlay,
OverlayConfig,
OverlayRef,
ScrollStrategy,
} from '@angular/cdk/overlay';
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
import {CdkPortalOutlet, ComponentPortal, ComponentType, TemplatePortal} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
import {
afterNextRender,
AfterViewInit,
booleanAttribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ComponentRef,
Directive,
ElementRef,
EventEmitter,
inject,
InjectionToken,
Injector,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
} from '@angular/core';
import {MatButton} from '@angular/material/button';
import {DateAdapter, ThemePalette} from '@angular/material/core';
import {merge, Observable, Subject, Subscription} from 'rxjs';
import {filter, take} from 'rxjs/operators';
import {MatCalendar, MatCalendarView} from './calendar';
import {MatCalendarCellClassFunction, MatCalendarUserEvent} from './calendar-body';
import {
MAT_DATE_RANGE_SELECTION_STRATEGY,
MatDateRangeSelectionStrategy,
} from './date-range-selection-strategy';
import {
DateRange,
ExtractDateTypeFromSelection,
MatDateSelectionModel,
} from './date-selection-model';
import {matDatepickerAnimations} from './datepicker-animations';
import {createMissingDateImplError} from './datepicker-errors';
import {DateFilterFn} from './datepicker-input-base';
import {MatDatepickerIntl} from './datepicker-intl';
import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private';
/** Injection token that determines the scroll handling while the calendar is open. */
export const MAT_DATEPICKER_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>(
'mat-datepicker-scroll-strategy',
{
providedIn: 'root',
factory: () => {
const overlay = inject(Overlay);
return () => overlay.scrollStrategies.reposition();
},
},
);
/** @docs-private */
export function MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy {
return () => overlay.scrollStrategies.reposition();
}
/** Possible positions for the datepicker dropdown along the X axis. */
export type DatepickerDropdownPositionX = 'start' | 'end';
/** Possible positions for the datepicker dropdown along the Y axis. */
export type DatepickerDropdownPositionY = 'above' | 'below';
/** @docs-private */
export const MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER = {
provide: MAT_DATEPICKER_SCROLL_STRATEGY,
deps: [Overlay],
useFactory: MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY,
};
/**
* Component used as the content for the datepicker overlay. We use this instead of using
* MatCalendar directly as the content so we can control the initial focus. This also gives us a
* place to put additional features of the overlay that are not part of the calendar itself in the
* future. (e.g. confirmation buttons).
* @docs-private
*/
@Component({
selector: 'mat-datepicker-content',
templateUrl: 'datepicker-content.html',
styleUrl: 'datepicker-content.css',
host: {
'class': 'mat-datepicker-content',
'[class]': 'color ? "mat-" + color : ""',
'[@transformPanel]': '_animationState',
'(@transformPanel.start)': '_handleAnimationEvent($event)',
'(@transformPanel.done)': '_handleAnimationEvent($event)',
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
},
animations: [matDatepickerAnimations.transformPanel, matDatepickerAnimations.fadeInCalendar],
exportAs: 'matDatepickerContent',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CdkTrapFocus, MatCalendar, CdkPortalOutlet, MatButton],
})
export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
implements OnInit, AfterViewInit, OnDestroy
{
protected _elementRef = inject(ElementRef);
private _changeDetectorRef = inject(ChangeDetectorRef);
private _globalModel = inject<MatDateSelectionModel<S, D>>(MatDateSelectionModel);
private _dateAdapter = inject<DateAdapter<D>>(DateAdapter)!;
private _rangeSelectionStrategy = inject<MatDateRangeSelectionStrategy<D>>(
MAT_DATE_RANGE_SELECTION_STRATEGY,
{optional: true},
);
private _subscriptions = new Subscription();
private _model: MatDateSelectionModel<S, D>;
/** Reference to the internal calendar component. */
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;
/**
* Theme color of the internal calendar. This API is supported in M2 themes
* only, it has no effect in M3 themes.
*
* For information on applying color variants in M3, see
* https://material.angular.io/guide/theming#using-component-color-variants.
*/
@Input() color: ThemePalette;
/** Reference to the datepicker that created the overlay. */
datepicker: MatDatepickerBase<any, S, D>;
/** Start of the comparison range. */
comparisonStart: D | null;
/** End of the comparison range. */
comparisonEnd: D | null;
/** ARIA Accessible name of the `<input matStartDate/>` */
startDateAccessibleName: string | null;
/** ARIA Accessible name of the `<input matEndDate/>` */
endDateAccessibleName: string | null;
/** Whether the datepicker is above or below the input. */
_isAbove: boolean;
/** Current state of the animation. */
_animationState: 'enter-dropdown' | 'enter-dialog' | 'void';
/** Emits when an animation has finished. */
readonly _animationDone = new Subject<void>();
/** Whether there is an in-progress animation. */
_isAnimating = false;
/** Text for the close button. */
_closeButtonText: string;
/** Whether the close button currently has focus. */
_closeButtonFocused: boolean;
/** Portal with projected action buttons. */
_actionsPortal: TemplatePortal | null = null;
/** Id of the label for the `role="dialog"` element. */
_dialogLabelId: string | null;
constructor(...args: unknown[]);
constructor() {
inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader);
const intl = inject(MatDatepickerIntl);
this._closeButtonText = intl.closeCalendarLabel;
}
ngOnInit() {
this._animationState = this.datepicker.touchUi ? 'enter-dialog' : 'enter-dropdown';
}
ngAfterViewInit() {
this._subscriptions.add(
this.datepicker.stateChanges.subscribe(() => {
this._changeDetectorRef.markForCheck();
}),
);
this._calendar.focusActiveCell();
}
ngOnDestroy() {
this._subscriptions.unsubscribe();
this._animationDone.complete();
}
_handleUserSelection(event: MatCalendarUserEvent<D | null>) {
const selection = this._model.selection;
const value = event.value;
const isRange = selection instanceof DateRange;
// If we're selecting a range and we have a selection strategy, always pass the value through
// there. Otherwise don't assign null values to the model, unless we're selecting a range.
// A null value when picking a range means that the user cancelled the selection (e.g. by
// pressing escape), whereas when selecting a single value it means that the value didn't
// change. This isn't very intuitive, but it's here for backwards-compatibility.
if (isRange && this._rangeSelectionStrategy) {
const newSelection = this._rangeSelectionStrategy.selectionFinished(
value,
selection as unknown as DateRange<D>,
event.event,
);
this._model.updateSelection(newSelection as unknown as S, this);
} else if (
value &&
(isRange || !this._dateAdapter.sameDate(value, selection as unknown as D))
) {
this._model.add(value);
}
// Delegate closing the overlay to the actions.
if ((!this._model || this._model.isComplete()) && !this._actionsPortal) {
this.datepicker.close();
}
}
_handleUserDragDrop(event: MatCalendarUserEvent<DateRange<D>>) {
this._model.updateSelection(event.value as unknown as S, this);
}
_startExitAnimation() {
this._animationState = 'void';
this._changeDetectorRef.markForCheck();
}
_handleAnimationEvent(event: AnimationEvent) {
this._isAnimating = event.phaseName === 'start';
if (!this._isAnimating) {
this._animationDone.next();
}
}
_getSelected() {
return this._model.selection as unknown as D | DateRange<D> | null;
}
/** Applies the current pending selection to the global model. */
_applyPendingSelection() {
if (this._model !== this._globalModel) {
this._globalModel.updateSelection(this._model.selection, this);
}
}
/**
* Assigns a new portal containing the datepicker actions.
* @param portal Portal with the actions to be assigned.
* @param forceRerender Whether a re-render of the portal should be triggered. This isn't
* necessary if the portal is assigned during initialization, but it may be required if it's
* added at a later point.
*/
_assignActions(portal: TemplatePortal<any> | null, forceRerender: boolean) {
// If we have actions, clone the model so that we have the ability to cancel the selection,
// otherwise update the global model directly. Note that we want to assign this as soon as
// possible, but `_actionsPortal` isn't available in the constructor so we do it in `ngOnInit`.
this._model = portal ? this._globalModel.clone() : this._globalModel;
this._actionsPortal = portal;
if (forceRerender) {
this._changeDetectorRef.detectChanges();
}
}
}
/** Form control that can be associated with a datepicker. */
export interface MatDatepickerControl<D> {
getStartValue(): D | null;
getThemePalette(): ThemePalette;
min: D | null;
max: D | null;
disabled: boolean;
dateFilter: DateFilterFn<D>;
getConnectedOverlayOrigin(): ElementRef;
getOverlayLabelId(): string | null;
stateChanges: Observable<void>;
}
/** A datepicker that can be attached to a {@link MatDatepickerControl}. */
export interface MatDatepickerPanel<
C extends MatDatepickerControl<D>,
S,
D = ExtractDateTypeFromSelection<S>,
> {
/** Stream that emits whenever the date picker is closed. */
closedStream: EventEmitter<void>;
/**
* Color palette to use on the datepicker's calendar. This API is supported in M2 themes only, it
* has no effect in M3 themes.
*
* For information on applying color variants in M3, see
* https://material.angular.io/guide/theming#using-component-color-variants
*/
color: ThemePalette;
/** The input element the datepicker is associated with. */
datepickerInput: C;
/** Whether the datepicker pop-up should be disabled. */
disabled: boolean;
/** The id for the datepicker's calendar. */
id: string;
/** Whether the datepicker is open. */
opened: boolean;
/** Stream that emits whenever the date picker is opened. */
openedStream: EventEmitter<void>;
/** Emits when the datepicker's state changes. */
stateChanges: Subject<void>;
/** Opens the datepicker. */
open(): void;
/** Register an input with the datepicker. */
registerInput(input: C): MatDateSelectionModel<S, D>;
}
/** Base class for a datepicker. */
@Directive()
export abstract class MatDatepickerBase<
C extends MatDatepickerControl<D>,
S,
D = ExtractDateTypeFromSelection<S>,
>
implements MatDatepickerPanel<C, S, D>, OnDestroy, OnChanges
{
private _overlay = inject(Overlay);
private _viewContainerRef = inject(ViewContainerRef);
private _dateAdapter = inject<DateAdapter<D>>(DateAdapter, {optional: true})!;
private _dir = inject(Directionality, {optional: true});
private _model = inject<MatDateSelectionModel<S, D>>(MatDateSelectionModel);
private _scrollStrategy = inject(MAT_DATEPICKER_SCROLL_STRATEGY);
private _inputStateChanges = Subscription.EMPTY;
private _document = inject(DOCUMENT);
/** An input indicating the type of the custom header component for the calendar, if set. */
@Input() calendarHeaderComponent: ComponentType<any>;
/** The date to open the calendar to initially. */
@Input()
get startAt(): D | null {
// If an explicit startAt is set we start there, otherwise we start at whatever the currently
// selected value is.
return this._startAt || (this.datepickerInput ? this.datepickerInput.getStartValue() : null);
}
set startAt(value: D | null) {
this._startAt = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}
private _startAt: D | null;
/** The view that the calendar should start in. */
@Input() startView: 'month' | 'year' | 'multi-year' = 'month';
/**
* Theme color of the datepicker's calendar. This API is supported in M2 themes only, it
* has no effect in M3 themes.
*
* For information on applying color variants in M3, see
* https://material.angular.io/guide/theming#using-component-color-variants.
*/
@Input()
get color(): ThemePalette {
return (
this._color || (this.datepickerInput ? this.datepickerInput.getThemePalette() : undefined)
);
}
set color(value: ThemePalette) {
this._color = value;
}
_color: ThemePalette;
/**
* Whether the calendar UI is in touch mode. In touch mode the calendar opens in a dialog rather
* than a dropdown and elements have more padding to allow for bigger touch targets.
*/
@Input({transform: booleanAttribute})
touchUi: boolean = false;
/** Whether the datepicker pop-up should be disabled. */
@Input({transform: booleanAttribute})
get disabled(): boolean {
return this._disabled === undefined && this.datepickerInput
? this.datepickerInput.disabled
: !!this._disabled;
}
set disabled(value: boolean) {
if (value !== this._disabled) {
this._disabled = value;
this.stateChanges.next(undefined);
}
}
private _disabled: boolean;
/** Preferred position of the datepicker in the X axis. */
@Input()
xPosition: DatepickerDropdownPositionX = 'start';
/** Preferred position of the datepicker in the Y axis. */
@Input()
yPosition: DatepickerDropdownPositionY = 'below';
/**
* Whether to restore focus to the previously-focused element when the calendar is closed.
* Note that automatic focus restoration is an accessibility feature and it is recommended that
* you provide your own equivalent, if you decide to turn it off.
*/
@Input({transform: booleanAttribute})
restoreFocus: boolean = true;
/**
* Emits selected year in multiyear view.
* This doesn't imply a change on the selected date.
*/
@Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();
/**
* Emits selected month in year view.
* This doesn't imply a change on the selected date.
*/
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();
/**
* Emits when the current view changes.
*/
@Output() readonly viewChanged: EventEmitter<MatCalendarView> = new EventEmitter<MatCalendarView>(
true,
);
/** Function that can be used to add custom CSS classes to dates. */
@Input() dateClass: MatCalendarCellClassFunction<D>;
/** Emits when the datepicker has been opened. */
@Output('opened') readonly openedStream = new EventEmitter<void>();
/** Emits when the datepicker has been closed. */
@Output('closed') readonly closedStream = new EventEmitter<void>();
/** Classes to be passed to the date picker panel. */
@Input()
get panelClass(): string | string[] {
return this._panelClass;
}
set panelClass(value: string | string[]) {
this._panelClass = coerceStringArray(value);
}
private _panelClass: string[];
/** Whether the calendar is open. */
@Input({transform: booleanAttribute})
get opened(): boolean {
return this._opened;
}
set opened(value: boolean) {
if (value) {
this.open();
} else {
this.close();
}
}
private _opened = false;
/** The id for the datepicker calendar. */
id: string = inject(_IdGenerator).getId('mat-datepicker-');
/** The minimum selectable date. */
_getMinDate(): D | null {
return this.datepickerInput && this.datepickerInput.min;
}
/** The maximum selectable date. */
_getMaxDate(): D | null {
return this.datepickerInput && this.datepickerInput.max;
}
_getDateFilter(): DateFilterFn<D> {
return this.datepickerInput && this.datepickerInput.dateFilter;
}
/** A reference to the overlay into which we've rendered the calendar. */
private _overlayRef: OverlayRef | null;
/** Reference to the component instance rendered in the overlay. */
private _componentRef: ComponentRef<MatDatepickerContent<S, D>> | null;
/** The element that was focused before the datepicker was opened. */
private _focusedElementBeforeOpen: HTMLElement | null = null;
/** Unique class that will be added to the backdrop so that the test harnesses can look it up. */
private _backdropHarnessClass = `${this.id}-backdrop`;
/** Currently-registered actions portal. */
private _actionsPortal: TemplatePortal | null;
/** The input element this datepicker is associated with. */
datepickerInput: C;
/** Emits when the datepicker's state changes. */
readonly stateChanges = new Subject<void>();
private _injector = inject(Injector);
private readonly _changeDetectorRef = inject(ChangeDetectorRef);
constructor(...args: unknown[]);
constructor() {
if (!this._dateAdapter && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw createMissingDateImplError('DateAdapter');
}
this._model.selectionChanged.subscribe(() => {
this._changeDetectorRef.markForCheck();
});
}
ngOnChanges(changes: SimpleChanges) {
const positionChange = changes['xPosition'] || changes['yPosition'];
if (positionChange && !positionChange.firstChange && this._overlayRef) {
const positionStrategy = this._overlayRef.getConfig().positionStrategy;
if (positionStrategy instanceof FlexibleConnectedPositionStrategy) {
this._setConnectedPositions(positionStrategy);
if (this.opened) {
this._overlayRef.updatePosition();
}
}
}
this.stateChanges.next(undefined);
}
ngOnDestroy() {
this._destroyOverlay();
this.close();
this._inputStateChanges.unsubscribe();
this.stateChanges.complete();
}
/** Selects the given date */
select(date: D): void {
this._model.add(date);
}
/** Emits the selected year in multiyear view */
_selectYear(normalizedYear: D): void {
this.yearSelected.emit(normalizedYear);
}
/** Emits selected month in year view */
_selectMonth(normalizedMonth: D): void {
this.monthSelected.emit(normalizedMonth);
}
/** Emits changed view */
_viewChanged(view: MatCalendarView): void {
this.viewChanged.emit(view);
}
/**
* Register an input with this datepicker.
* @param input The datepicker input to register with this datepicker.
* @returns Selection model that the input should hook itself up to.
*/
registerInput(input: C): MatDateSelectionModel<S, D> {
if (this.datepickerInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('A MatDatepicker can only be associated with a single input.');
}
this._inputStateChanges.unsubscribe();
this.datepickerInput = input;
this._inputStateChanges = input.stateChanges.subscribe(() => this.stateChanges.next(undefined));
return this._model;
}
/**
* Registers a portal containing action buttons with the datepicker.
* @param portal Portal to be registered.
*/
registerActions(portal: TemplatePortal): void {
if (this._actionsPortal && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('A MatDatepicker can only be associated with a single actions row.');
}
this._actionsPortal = portal;
this._componentRef?.instance._assignActions(portal, true);
}
/**
* Removes a portal containing action buttons from the datepicker.
* @param portal Portal to be removed.
*/
removeActions(portal: TemplatePortal): void {
if (portal === this._actionsPortal) {
this._actionsPortal = null;
this._componentRef?.instance._assignActions(null, true);
}
}
/** Open the calendar. */
open(): void {
// Skip reopening if there's an in-progress animation to avoid overlapping
// sequences which can cause "changed after checked" errors. See #25837.
if (this._opened || this.disabled || this._componentRef?.instance._isAnimating) {
return;
}
if (!this.datepickerInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Attempted to open an MatDatepicker with no associated input.');
}
this._focusedElementBeforeOpen = _getFocusedElementPierceShadowDom();
this._openOverlay();
this._opened = true;
this.openedStream.emit();
}
/** Close the calendar. */
close(): void {
// Skip reopening if there's an in-progress animation to avoid overlapping
// sequences which can cause "changed after checked" errors. See #25837.
if (!this._opened || this._componentRef?.instance._isAnimating) {
return;
}
const canRestoreFocus =
this.restoreFocus &&
this._focusedElementBeforeOpen &&
typeof this._focusedElementBeforeOpen.focus === 'function';
const completeClose = () => {
// The `_opened` could've been reset already if
// we got two events in quick succession.
if (this._opened) {
this._opened = false;
this.closedStream.emit();
}
};
if (this._componentRef) {
const {instance, location} = this._componentRef;
instance._startExitAnimation();
instance._animationDone.pipe(take(1)).subscribe(() => {
const activeElement = this._document.activeElement;
// Since we restore focus after the exit animation, we have to check that
// the user didn't move focus themselves inside the `close` handler.
if (
canRestoreFocus &&
(!activeElement ||
activeElement === this._document.activeElement ||
location.nativeElement.contains(activeElement))
) {
this._focusedElementBeforeOpen!.focus();
}
this._focusedElementBeforeOpen = null;
this._destroyOverlay();
});
}
if (canRestoreFocus) {
// Because IE moves focus asynchronously, we can't count on it being restored before we've
// marked the datepicker as closed. If the event fires out of sequence and the element that
// we're refocusing opens the datepicker on focus, the user could be stuck with not being
// able to close the calendar at all. We work around it by making the logic, that marks
// the datepicker as closed, async as well.
setTimeout(completeClose);
} else {
completeClose();
}
}
/** Applies the current pending selection on the overlay to the model. */
_applyPendingSelection() {
this._componentRef?.instance?._applyPendingSelection();
}
/** Forwards relevant values from the datepicker to the datepicker content inside the overlay. */
protected _forwardContentValues(instance: MatDatepickerContent<S, D>) {
instance.datepicker = this;
instance.color = this.color;
instance._dialogLabelId = this.datepickerInput.getOverlayLabelId();
instance._assignActions(this._actionsPortal, false);
}
/** Opens the overlay with the calendar. */
private _openOverlay(): void {
this._destroyOverlay();
const isDialog = this.touchUi;
const portal = new ComponentPortal<MatDatepickerContent<S, D>>(
MatDatepickerContent,
this._viewContainerRef,
);
const overlayRef = (this._overlayRef = this._overlay.create(
new OverlayConfig({
positionStrategy: isDialog ? this._getDialogStrategy() : this._getDropdownStrategy(),
hasBackdrop: true,
backdropClass: [
isDialog ? 'cdk-overlay-dark-backdrop' : 'mat-overlay-transparent-backdrop',
this._backdropHarnessClass,
],
direction: this._dir || 'ltr',
scrollStrategy: isDialog ? this._overlay.scrollStrategies.block() : this._scrollStrategy(),
panelClass: `mat-datepicker-${isDialog ? 'dialog' : 'popup'}`,
}),
));
this._getCloseStream(overlayRef).subscribe(event => {
if (event) {
event.preventDefault();
}
this.close();
});
// The `preventDefault` call happens inside the calendar as well, however focus moves into
// it inside a timeout which can give browsers a chance to fire off a keyboard event in-between
// that can scroll the page (see #24969). Always block default actions of arrow keys for the
// entire overlay so the page doesn't get scrolled by accident.
overlayRef.keydownEvents().subscribe(event => {
const keyCode = event.keyCode;
if (
keyCode === UP_ARROW ||
keyCode === DOWN_ARROW ||
keyCode === LEFT_ARROW ||
keyCode === RIGHT_ARROW ||
keyCode === PAGE_UP ||
keyCode === PAGE_DOWN
) {
event.preventDefault();
}
});
this._componentRef = overlayRef.attach(portal);
this._forwardContentValues(this._componentRef.instance);
// Update the position once the calendar has rendered. Only relevant in dropdown mode.
if (!isDialog) {
afterNextRender(
() => {
overlayRef.updatePosition();
},
{injector: this._injector},
);
}
}
/** Destroys the current overlay. */
private _destroyOverlay() {
if (this._overlayRef) {
this._overlayRef.dispose();
this._overlayRef = this._componentRef = null;
}
}
/** Gets a position strategy that will open the calendar as a dropdown. */
private _getDialogStrategy() {
return this._overlay.position().global().centerHorizontally().centerVertically();
}
/** Gets a position strategy that will open the calendar as a dropdown. */
private _getDropdownStrategy() {
const strategy = this._overlay
.position()
.flexibleConnectedTo(this.datepickerInput.getConnectedOverlayOrigin())
.withTransformOriginOn('.mat-datepicker-content')
.withFlexibleDimensions(false)
.withViewportMargin(8)
.withLockedPosition();
return this._setConnectedPositions(strategy);
}
/** Sets the positions of the datepicker in dropdown mode based on the current configuration. */
private _setConnectedPositions(strategy: FlexibleConnectedPositionStrategy) {
const primaryX = this.xPosition === 'end' ? 'end' : 'start';
const secondaryX = primaryX === 'start' ? 'end' : 'start';
const primaryY = this.yPosition === 'above' ? 'bottom' : 'top';
const secondaryY = primaryY === 'top' ? 'bottom' : 'top';
return strategy.withPositions([
{
originX: primaryX,
originY: secondaryY,
overlayX: primaryX,
overlayY: primaryY,
},
{
originX: primaryX,
originY: primaryY,
overlayX: primaryX,
overlayY: secondaryY,
},
{
originX: secondaryX,
originY: secondaryY,
overlayX: secondaryX,
overlayY: primaryY,
},
{
originX: secondaryX,
originY: primaryY,
overlayX: secondaryX,
overlayY: secondaryY,
},
]);
}
/** Gets an observable that will emit when the overlay is supposed to be closed. */
private _getCloseStream(overlayRef: OverlayRef) {
const ctrlShiftMetaModifiers: ModifierKey[] = ['ctrlKey', 'shiftKey', 'metaKey'];
return merge(
overlayRef.backdropClick(),
overlayRef.detachments(),
overlayRef.keydownEvents().pipe(
filter(event => {
// Closing on alt + up is only valid when there's an input associated with the datepicker.
return (
(event.keyCode === ESCAPE && !hasModifierKey(event)) ||
(this.datepickerInput &&
hasModifierKey(event, 'altKey') &&
event.keyCode === UP_ARROW &&
ctrlShiftMetaModifiers.every(
(modifier: ModifierKey) => !hasModifierKey(event, modifier),
))
);
}),
),
);
}
}