From b53813c230ca9c19a8a85e92b797e8efec0cf9f0 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 7 Sep 2018 11:23:54 +0300 Subject: [PATCH] feat(datepicker): remove dependency on mat-dialog Reworks the datepicker to remove its dependency to `material/dialog`, avoiding bringing in all of the overhead of the dialog from which the datepicker is using a small fraction. --- src/lib/datepicker/BUILD.bazel | 1 - src/lib/datepicker/datepicker-animations.ts | 17 +-- src/lib/datepicker/datepicker-content.scss | 2 +- src/lib/datepicker/datepicker-module.ts | 2 - src/lib/datepicker/datepicker.spec.ts | 40 +++--- src/lib/datepicker/datepicker.ts | 152 ++++++++++---------- 6 files changed, 106 insertions(+), 108 deletions(-) diff --git a/src/lib/datepicker/BUILD.bazel b/src/lib/datepicker/BUILD.bazel index 75e64a820375..7dc11479f0a3 100644 --- a/src/lib/datepicker/BUILD.bazel +++ b/src/lib/datepicker/BUILD.bazel @@ -16,7 +16,6 @@ ng_module( deps = [ "//src/lib/core", "//src/lib/button", - "//src/lib/dialog", "//src/lib/input", "//src/cdk/a11y", "//src/cdk/bidi", diff --git a/src/lib/datepicker/datepicker-animations.ts b/src/lib/datepicker/datepicker-animations.ts index a84f8b4149cb..85cab0cdd56a 100644 --- a/src/lib/datepicker/datepicker-animations.ts +++ b/src/lib/datepicker/datepicker-animations.ts @@ -12,6 +12,7 @@ import { transition, trigger, AnimationTriggerMetadata, + keyframes, } from '@angular/animations'; /** Animations used by the Material datepicker. */ @@ -21,14 +22,14 @@ export const matDatepickerAnimations: { } = { /** Transforms the height of the datepicker's calendar. */ transformPanel: trigger('transformPanel', [ - state('void', style({ - opacity: 0, - transform: 'scale(1, 0.8)' - })), - transition('void => enter', animate('120ms cubic-bezier(0, 0, 0.2, 1)', style({ - opacity: 1, - transform: 'scale(1, 1)' - }))), + transition('void => enter-popup', animate('120ms cubic-bezier(0, 0, 0.2, 1)', keyframes([ + style({opacity: 0, transform: 'scale(1, 0.8)'}), + style({opacity: 1, transform: 'none'}) + ]))), + transition('void => enter-dialog', animate('150ms cubic-bezier(0, 0, 0.2, 1)', keyframes([ + style({opacity: 0, transform: 'scale(0.7)'}), + style({opacity: 1, transform: 'none'}) + ]))), transition('* => void', animate('100ms linear', style({opacity: 0}))) ]), diff --git a/src/lib/datepicker/datepicker-content.scss b/src/lib/datepicker/datepicker-content.scss index c3c4b03aafce..85ec424e8894 100644 --- a/src/lib/datepicker/datepicker-content.scss +++ b/src/lib/datepicker/datepicker-content.scss @@ -37,7 +37,7 @@ $mat-datepicker-touch-max-height: 788px; } .mat-datepicker-content-touch { - @include mat-elevation(0); + @include mat-elevation(24); display: block; // make sure the dialog scrolls rather than being cropped on ludicrously small screens diff --git a/src/lib/datepicker/datepicker-module.ts b/src/lib/datepicker/datepicker-module.ts index c0e08d6a75cf..8658cdd13ec8 100644 --- a/src/lib/datepicker/datepicker-module.ts +++ b/src/lib/datepicker/datepicker-module.ts @@ -12,7 +12,6 @@ import {PortalModule} from '@angular/cdk/portal'; import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; -import {MatDialogModule} from '@angular/material/dialog'; import {MatCalendar, MatCalendarHeader} from './calendar'; import {MatCalendarBody} from './calendar-body'; import { @@ -32,7 +31,6 @@ import {MatYearView} from './year-view'; imports: [ CommonModule, MatButtonModule, - MatDialogModule, OverlayModule, A11yModule, PortalModule, diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index a69becfce949..e9621bcf4588 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -106,12 +106,12 @@ describe('MatDatepicker', () => { testComponent.touch = true; fixture.detectChanges(); - expect(document.querySelector('.mat-datepicker-dialog mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); testComponent.datepicker.open(); fixture.detectChanges(); - expect(document.querySelector('.mat-datepicker-dialog mat-dialog-container')) + expect(document.querySelector('.mat-datepicker-dialog')) .not.toBeNull(); }); @@ -156,13 +156,13 @@ describe('MatDatepicker', () => { fixture.detectChanges(); expect(document.querySelector('.cdk-overlay-pane')).toBeNull(); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('.cdk-overlay-pane')).toBeNull(); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); }); it('disabled datepicker input should open the calendar if datepicker is enabled', () => { @@ -224,13 +224,13 @@ describe('MatDatepicker', () => { testComponent.datepicker.open(); fixture.detectChanges(); - expect(document.querySelector('mat-dialog-container')).not.toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); testComponent.datepicker.close(); fixture.detectChanges(); flush(); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); })); it('setting selected via click should update input and close calendar', fakeAsync(() => { @@ -241,7 +241,7 @@ describe('MatDatepicker', () => { fixture.detectChanges(); flush(); - expect(document.querySelector('mat-dialog-container')).not.toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1)); let cells = document.querySelectorAll('.mat-calendar-body-cell'); @@ -249,7 +249,7 @@ describe('MatDatepicker', () => { fixture.detectChanges(); flush(); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2)); })); @@ -262,7 +262,7 @@ describe('MatDatepicker', () => { fixture.detectChanges(); flush(); - expect(document.querySelector('mat-dialog-container')).not.toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1)); let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement; @@ -274,7 +274,7 @@ describe('MatDatepicker', () => { fixture.detectChanges(); flush(); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2)); })); @@ -298,7 +298,7 @@ describe('MatDatepicker', () => { } expect(selectedChangedSpy.calls.count()).toEqual(1); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2)); })); @@ -319,7 +319,7 @@ describe('MatDatepicker', () => { fixture.whenStable().then(() => { expect(selectedChangedSpy.calls.count()).toEqual(0); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1)); }); }); @@ -909,13 +909,13 @@ describe('MatDatepicker', () => { }); it('should open calendar when toggle clicked', () => { - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); let toggle = fixture.debugElement.query(By.css('button')); dispatchMouseEvent(toggle.nativeElement, 'click'); fixture.detectChanges(); - expect(document.querySelector('mat-dialog-container')).not.toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); }); it('should not open calendar when toggle clicked if datepicker is disabled', () => { @@ -924,12 +924,12 @@ describe('MatDatepicker', () => { const toggle = fixture.debugElement.query(By.css('button')).nativeElement; expect(toggle.hasAttribute('disabled')).toBe(true); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); dispatchMouseEvent(toggle, 'click'); fixture.detectChanges(); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); }); it('should not open calendar when toggle clicked if input is disabled', () => { @@ -940,12 +940,12 @@ describe('MatDatepicker', () => { const toggle = fixture.debugElement.query(By.css('button')).nativeElement; expect(toggle.hasAttribute('disabled')).toBe(true); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); dispatchMouseEvent(toggle, 'click'); fixture.detectChanges(); - expect(document.querySelector('mat-dialog-container')).toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); }); it('should set the `button` type on the trigger to prevent form submissions', () => { @@ -1224,7 +1224,7 @@ describe('MatDatepicker', () => { testComponent.datepicker.open(); fixture.detectChanges(); - expect(document.querySelector('mat-dialog-container')).not.toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); let cells = document.querySelectorAll('.mat-calendar-body-cell'); expect(cells[0].classList).toContain('mat-calendar-body-disabled'); @@ -1296,7 +1296,7 @@ describe('MatDatepicker', () => { testComponent.datepicker.open(); fixture.detectChanges(); - expect(document.querySelector('mat-dialog-container')).not.toBeNull(); + expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); const cells = document.querySelectorAll('.mat-calendar-body-cell'); dispatchMouseEvent(cells[0], 'click'); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index cd3875e69140..7e67fde027ae 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -43,7 +43,6 @@ import { mixinColor, ThemePalette, } from '@angular/material/core'; -import {MatDialog, MatDialogRef} from '@angular/material/dialog'; import {merge, Subject, Subscription} from 'rxjs'; import {filter, take} from 'rxjs/operators'; import {MatCalendar} from './calendar'; @@ -92,7 +91,7 @@ export const _MatDatepickerContentMixinBase: CanColorCtor & typeof MatDatepicker styleUrls: ['datepicker-content.css'], host: { 'class': 'mat-datepicker-content', - '[@transformPanel]': '"enter"', + '[@transformPanel]': 'datepicker.touchUi ? "enter-dialog" : "enter-popup"', '[class.mat-datepicker-content-touch]': 'datepicker.touchUi', }, animations: [ @@ -113,9 +112,6 @@ export class MatDatepickerContent extends _MatDatepickerContentMixinBase /** Reference to the datepicker that created the overlay. */ datepicker: MatDatepicker; - /** Whether the datepicker is above or below the input. */ - _isAbove: boolean; - constructor(elementRef: ElementRef) { super(elementRef); } @@ -249,13 +245,13 @@ export class MatDatepicker implements OnDestroy, CanColor { _popupRef: OverlayRef; /** A reference to the dialog when the calendar is opened as a dialog. */ - private _dialogRef: MatDialogRef> | null; + private _dialogRef: OverlayRef; /** A portal containing the calendar for this datepicker. */ private _calendarPortal: ComponentPortal>; - /** Reference to the component instantiated in popup mode. */ - private _popupComponentRef: ComponentRef> | null; + /** Reference to the component instantiated inside the overlay */ + private _componentRef: ComponentRef> | null; /** The element that was focused before the datepicker was opened. */ private _focusedElementBeforeOpen: HTMLElement | null = null; @@ -272,14 +268,14 @@ export class MatDatepicker implements OnDestroy, CanColor { /** Emits new selected date when selected date changes. */ readonly _selectedChanged = new Subject(); - constructor(private _dialog: MatDialog, - private _overlay: Overlay, + constructor(private _overlay: Overlay, private _ngZone: NgZone, private _viewContainerRef: ViewContainerRef, @Inject(MAT_DATEPICKER_SCROLL_STRATEGY) private _scrollStrategy, @Optional() private _dateAdapter: DateAdapter, @Optional() private _dir: Directionality, @Optional() @Inject(DOCUMENT) private _document: any) { + if (!this._dateAdapter) { throw createMissingDateImplError('DateAdapter'); } @@ -292,8 +288,13 @@ export class MatDatepicker implements OnDestroy, CanColor { if (this._popupRef) { this._popupRef.dispose(); - this._popupComponentRef = null; } + + if (this._dialogRef) { + this._dialogRef.dispose(); + } + + this._componentRef = null; } /** Selects the given date */ @@ -340,7 +341,7 @@ export class MatDatepicker implements OnDestroy, CanColor { this._focusedElementBeforeOpen = this._document.activeElement; } - this.touchUi ? this._openAsDialog() : this._openAsPopup(); + this._openOverlay(); this._opened = true; this.openedStream.emit(); } @@ -353,9 +354,8 @@ export class MatDatepicker implements OnDestroy, CanColor { if (this._popupRef && this._popupRef.hasAttached()) { this._popupRef.detach(); } - if (this._dialogRef) { - this._dialogRef.close(); - this._dialogRef = null; + if (this._dialogRef && this._dialogRef.hasAttached()) { + this._dialogRef.detach(); } if (this._calendarPortal && this._calendarPortal.isAttached) { this._calendarPortal.detach(); @@ -385,73 +385,84 @@ export class MatDatepicker implements OnDestroy, CanColor { } } - /** Open the calendar as a dialog. */ - private _openAsDialog(): void { - // Usually this would be handled by `open` which ensures that we can only have one overlay - // open at a time, however since we reset the variables in async handlers some overlays - // may slip through if the user opens and closes multiple times in quick succession (e.g. - // by holding down the enter key). - if (this._dialogRef) { - this._dialogRef.close(); + /** Opens the overlay with the calendar. */ + private _openOverlay(): void { + const overlayRef = this.touchUi ? this._getDialogRef() : this._getPopupRef(); + + // Don't do anything if the relevant overlay is already attached. + if (overlayRef.hasAttached()) { + return; } - this._dialogRef = this._dialog.open>(MatDatepickerContent, { - direction: this._dir ? this._dir.value : 'ltr', - viewContainerRef: this._viewContainerRef, - panelClass: 'mat-datepicker-dialog', - }); + this._componentRef = overlayRef.attach(this._getCalendarPortal()); + this._componentRef.instance.datepicker = this; + this._componentRef.instance.color = this.color; + + if (!this.touchUi) { + // Update the position once the calendar has rendered. + this._ngZone.onStable.asObservable() + .pipe(take(1)) + .subscribe(() => overlayRef.updatePosition()); + } - this._dialogRef.afterClosed().subscribe(() => this.close()); - this._dialogRef.componentInstance.datepicker = this; - this._setColor(); + merge( + overlayRef.backdropClick(), + overlayRef.detachments(), + overlayRef.keydownEvents().pipe(filter(event => { + return event.keyCode === ESCAPE || ( + // Closing on alt + up is only valid when there's an input associated with the datepicker. + !this.touchUi && this._datepickerInput && event.altKey && event.keyCode === UP_ARROW + ); + })) + ).subscribe(() => this.close()); } - /** Open the calendar as a popup. */ - private _openAsPopup(): void { + /** Creates the portal that will be used to render the calendar. */ + private _getCalendarPortal() { if (!this._calendarPortal) { this._calendarPortal = new ComponentPortal>(MatDatepickerContent, this._viewContainerRef); } - if (!this._popupRef) { - this._createPopup(); - } - - if (!this._popupRef.hasAttached()) { - this._popupComponentRef = this._popupRef.attach(this._calendarPortal); - this._popupComponentRef.instance.datepicker = this; - this._setColor(); + return this._calendarPortal; + } - // Update the position once the calendar has rendered. - this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => { - this._popupRef.updatePosition(); + /** Gets a reference to the overlay that will be used in touch UI mode. */ + private _getDialogRef(): OverlayRef { + if (!this._dialogRef) { + const overlayConfig = new OverlayConfig({ + positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(), + hasBackdrop: true, + direction: this._dir, + scrollStrategy: this._overlay.scrollStrategies.block(), + panelClass: 'mat-datepicker-dialog', }); + + this._dialogRef = this._overlay.create(overlayConfig); + this._dialogRef.overlayElement.setAttribute('role', 'dialog'); + this._dialogRef.overlayElement.setAttribute('aria-modal', 'true'); } + + return this._dialogRef; } - /** Create the popup. */ - private _createPopup(): void { - const overlayConfig = new OverlayConfig({ - positionStrategy: this._createPopupPositionStrategy(), - hasBackdrop: true, - backdropClass: 'mat-overlay-transparent-backdrop', - direction: this._dir, - scrollStrategy: this._scrollStrategy(), - panelClass: 'mat-datepicker-popup', - }); + /** Gets a reference to the overlay that will be used in popup mode. */ + private _getPopupRef(): OverlayRef { + if (!this._popupRef) { + const overlayConfig = new OverlayConfig({ + positionStrategy: this._createPopupPositionStrategy(), + hasBackdrop: true, + backdropClass: 'mat-overlay-transparent-backdrop', + direction: this._dir, + scrollStrategy: this._scrollStrategy(), + panelClass: 'mat-datepicker-popup', + }); - this._popupRef = this._overlay.create(overlayConfig); - this._popupRef.overlayElement.setAttribute('role', 'dialog'); + this._popupRef = this._overlay.create(overlayConfig); + this._popupRef.overlayElement.setAttribute('role', 'dialog'); + } - merge( - this._popupRef.backdropClick(), - this._popupRef.detachments(), - this._popupRef.keydownEvents().pipe(filter(event => { - // Closing on alt + up is only valid when there's an input associated with the datepicker. - return event.keyCode === ESCAPE || - (this._datepickerInput && event.altKey && event.keyCode === UP_ARROW); - })) - ).subscribe(() => this.close()); + return this._popupRef; } /** Create the popup PositionStrategy. */ @@ -497,15 +508,4 @@ export class MatDatepicker implements OnDestroy, CanColor { private _getValidDateOrNull(obj: any): D | null { return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null; } - - /** Passes the current theme color along to the calendar overlay. */ - private _setColor(): void { - const color = this.color; - if (this._popupComponentRef) { - this._popupComponentRef.instance.color = color; - } - if (this._dialogRef) { - this._dialogRef.componentInstance.color = color; - } - } }