From 62f46b0a8766cac192a2ae399fadfa1a3524ffdf Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 27 Feb 2024 23:28:29 +0000 Subject: [PATCH] refactor(material/datepicker): Remove use of zone onStable for focus and dropdown positioning --- src/material/datepicker/calendar-body.ts | 14 ++++++---- src/material/datepicker/calendar.spec.ts | 31 ++++++---------------- src/material/datepicker/datepicker-base.ts | 17 ++++++++++-- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts index 78f0ad2465e3..63032f3f3dc0 100644 --- a/src/material/datepicker/calendar-body.ts +++ b/src/material/datepicker/calendar-body.ts @@ -21,8 +21,9 @@ import { OnDestroy, AfterViewChecked, inject, + afterNextRender, + Injector, } from '@angular/core'; -import {take} from 'rxjs/operators'; import {NgClass} from '@angular/common'; /** Extra CSS classes that can be associated with a calendar cell. */ @@ -189,6 +190,8 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView private _didDragSinceMouseDown = false; + private _injector = inject(Injector); + constructor( private _elementRef: ElementRef, private _ngZone: NgZone, @@ -309,8 +312,8 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView * Adding delay also complicates writing tests. */ _focusActiveCell(movePreview = true) { - this._ngZone.runOutsideAngular(() => { - this._ngZone.onStable.pipe(take(1)).subscribe(() => { + afterNextRender( + () => { setTimeout(() => { const activeCell: HTMLElement | null = this._elementRef.nativeElement.querySelector( '.mat-calendar-body-active', @@ -324,8 +327,9 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView activeCell.focus(); } }); - }); - }); + }, + {injector: this._injector}, + ); } /** Focuses the active cell after change detection has run and the microtask queue is empty. */ diff --git a/src/material/datepicker/calendar.spec.ts b/src/material/datepicker/calendar.spec.ts index e23809995d58..70978c363ee9 100644 --- a/src/material/datepicker/calendar.spec.ts +++ b/src/material/datepicker/calendar.spec.ts @@ -4,17 +4,9 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, - MockNgZone, } from '@angular/cdk/testing/private'; -import {Component, NgZone} from '@angular/core'; -import { - fakeAsync, - waitForAsync, - ComponentFixture, - inject, - TestBed, - tick, -} from '@angular/core/testing'; +import {Component} from '@angular/core'; +import {waitForAsync, ComponentFixture, inject, TestBed} from '@angular/core/testing'; import {DateAdapter, MatNativeDateModule} from '@angular/material/core'; import {DEC, FEB, JAN, JUL, NOV} from '../testing'; import {By} from '@angular/platform-browser'; @@ -23,16 +15,10 @@ import {MatDatepickerIntl} from './datepicker-intl'; import {MatDatepickerModule} from './datepicker-module'; describe('MatCalendar', () => { - let zone: MockNgZone; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [MatNativeDateModule, MatDatepickerModule], - providers: [ - MatDatepickerIntl, - {provide: NgZone, useFactory: () => (zone = new MockNgZone())}, - {provide: Directionality, useFactory: () => ({value: 'ltr'})}, - ], + providers: [MatDatepickerIntl, {provide: Directionality, useFactory: () => ({value: 'ltr'})}], declarations: [ // Test components. StandardCalendar, @@ -183,19 +169,19 @@ describe('MatCalendar', () => { expect(calendarBodyEl.getAttribute('tabindex')).toBe('-1'); }); - it('should not move focus to the active cell on init', () => { + it('should not move focus to the active cell on init', waitForAsync(async () => { const activeCell = calendarBodyEl.querySelector( '.mat-calendar-body-active', )! as HTMLElement; spyOn(activeCell, 'focus').and.callThrough(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(resolve => setTimeout(resolve)); expect(activeCell.focus).not.toHaveBeenCalled(); - }); + })); - it('should move focus to the active cell when the view changes', fakeAsync(() => { + it('should move focus to the active cell when the view changes', waitForAsync(async () => { calendarInstance.currentView = 'multi-year'; fixture.detectChanges(); @@ -204,8 +190,7 @@ describe('MatCalendar', () => { )! as HTMLElement; spyOn(activeCell, 'focus').and.callThrough(); - zone.simulateZoneExit(); - tick(); + await new Promise(resolve => setTimeout(resolve)); expect(activeCell.focus).toHaveBeenCalled(); })); diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index e9b30cfbd487..7eb9689e50b5 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -51,6 +51,8 @@ import { OnInit, inject, booleanAttribute, + afterNextRender, + Injector, } from '@angular/core'; import {DateAdapter, ThemePalette} from '@angular/material/core'; import {AnimationEvent} from '@angular/animations'; @@ -509,9 +511,15 @@ export abstract class MatDatepickerBase< /** Emits when the datepicker's state changes. */ readonly stateChanges = new Subject(); + private _injector = inject(Injector); + constructor( private _overlay: Overlay, - private _ngZone: NgZone, + /** + * @deprecated parameter is unused and will be removed + * @breaking-change 19.0.0 + */ + _unusedNgZone: NgZone, private _viewContainerRef: ViewContainerRef, @Inject(MAT_DATEPICKER_SCROLL_STRATEGY) scrollStrategy: any, @Optional() private _dateAdapter: DateAdapter, @@ -749,7 +757,12 @@ export abstract class MatDatepickerBase< // Update the position once the calendar has rendered. Only relevant in dropdown mode. if (!isDialog) { - this._ngZone.onStable.pipe(take(1)).subscribe(() => overlayRef.updatePosition()); + afterNextRender( + () => { + overlayRef.updatePosition(); + }, + {injector: this._injector}, + ); } }