diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index 1bc33c6b512c..3852150c44c3 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -8,6 +8,7 @@ import {addAriaReferencedId, removeAriaReferencedId} from '@angular/cdk/a11y'; import { + afterNextRender, AfterViewInit, booleanAttribute, ChangeDetectorRef, @@ -18,6 +19,7 @@ import { inject, Inject, InjectionToken, + Injector, Input, NgZone, OnChanges, @@ -227,6 +229,10 @@ export class MatAutocompleteTrigger @Input({alias: 'matAutocompleteDisabled', transform: booleanAttribute}) autocompleteDisabled: boolean; + private _initialized = new Subject(); + + private _injector = inject(Injector); + constructor( private _element: ElementRef, private _overlay: Overlay, @@ -249,6 +255,9 @@ export class MatAutocompleteTrigger private _aboveClass = 'mat-mdc-autocomplete-panel-above'; ngAfterViewInit() { + this._initialized.next(); + this._initialized.complete(); + const window = this._getWindow(); if (typeof window !== 'undefined') { @@ -301,12 +310,7 @@ export class MatAutocompleteTrigger if (this.panelOpen) { // Only emit if the panel was visible. - // The `NgZone.onStable` always emits outside of the Angular zone, - // so all the subscriptions from `_subscribeToClosingActions()` are also outside of the Angular zone. - // We should manually run in Angular zone to update UI after panel closing. - this._zone.run(() => { - this.autocomplete.closed.emit(); - }); + this.autocomplete.closed.emit(); } this.autocomplete._isOpen = this._overlayAttached = false; @@ -378,10 +382,7 @@ export class MatAutocompleteTrigger // If there are any subscribers before `ngAfterViewInit`, the `autocomplete` will be undefined. // Return a stream that we'll replace with the real one once everything is in place. - return this._zone.onStable.pipe( - take(1), - switchMap(() => this.optionSelections), - ); + return this._initialized.pipe(switchMap(() => this.optionSelections)); }) as Observable; /** The currently active option, coerced to MatOption type. */ @@ -592,7 +593,14 @@ export class MatAutocompleteTrigger * stream every time the option list changes. */ private _subscribeToClosingActions(): Subscription { - const firstStable = this._zone.onStable.pipe(take(1)); + const initialRender = new Observable(subscriber => { + afterNextRender( + () => { + subscriber.next(); + }, + {injector: this._injector}, + ); + }); const optionChanges = this.autocomplete.options.changes.pipe( tap(() => this._positionStrategy.reapplyLastPosition()), // Defer emitting to the stream until the next tick, because changing @@ -600,41 +608,36 @@ export class MatAutocompleteTrigger delay(0), ); - // When the zone is stable initially, and when the option list changes... + // When the options are initially rendered, and when the option list changes... return ( - merge(firstStable, optionChanges) + merge(initialRender, optionChanges) .pipe( // create a new stream of panelClosingActions, replacing any previous streams // that were created, and flatten it so our stream only emits closing events... switchMap(() => { - // The `NgZone.onStable` always emits outside of the Angular zone, thus we have to re-enter - // the Angular zone. This will lead to change detection being called outside of the Angular - // zone and the `autocomplete.opened` will also emit outside of the Angular. - this._zone.run(() => { - const wasOpen = this.panelOpen; - this._resetActiveItem(); - this._updatePanelState(); - this._changeDetectorRef.detectChanges(); - + const wasOpen = this.panelOpen; + this._resetActiveItem(); + this._updatePanelState(); + this._changeDetectorRef.detectChanges(); + + if (this.panelOpen) { + this._overlayRef!.updatePosition(); + } + + if (wasOpen !== this.panelOpen) { + // If the `panelOpen` state changed, we need to make sure to emit the `opened` or + // `closed` event, because we may not have emitted it. This can happen + // - if the users opens the panel and there are no options, but the + // options come in slightly later or as a result of the value changing, + // - if the panel is closed after the user entered a string that did not match any + // of the available options, + // - if a valid string is entered after an invalid one. if (this.panelOpen) { - this._overlayRef!.updatePosition(); - } - - if (wasOpen !== this.panelOpen) { - // If the `panelOpen` state changed, we need to make sure to emit the `opened` or - // `closed` event, because we may not have emitted it. This can happen - // - if the users opens the panel and there are no options, but the - // options come in slightly later or as a result of the value changing, - // - if the panel is closed after the user entered a string that did not match any - // of the available options, - // - if a valid string is entered after an invalid one. - if (this.panelOpen) { - this._emitOpened(); - } else { - this.autocomplete.closed.emit(); - } + this._emitOpened(); + } else { + this.autocomplete.closed.emit(); } - }); + } return this.panelClosingActions; }),