diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 47f40aab81a0..3622f7c341b3 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -3365,6 +3365,31 @@ describe('MDC-based MatAutocomplete', () => { expect(fixture.componentInstance.trigger.panelOpen).toBe(true); }); + + it('should emit from `autocomplete.closed` after click outside inside the NgZone', fakeAsync(() => { + const inZoneSpy = jasmine.createSpy('in zone spy'); + + const fixture = createComponent(SimpleAutocomplete, [ + {provide: NgZone, useFactory: () => new NgZone({enableLongStackTrace: false})}, + ]); + const zone = TestBed.inject(NgZone); + fixture.detectChanges(); + + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + flush(); + + const subscription = fixture.componentInstance.trigger.autocomplete.closed.subscribe(() => + inZoneSpy(NgZone.isInAngularZone()), + ); + zone.onStable.emit(null); + + dispatchFakeEvent(document, 'click'); + + expect(inZoneSpy).toHaveBeenCalledWith(true); + + subscription.unsubscribe(); + })); }); const SIMPLE_AUTOCOMPLETE_TEMPLATE = ` diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index 1ea94334949e..77c6f4702941 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -268,7 +268,12 @@ export abstract class _MatAutocompleteTriggerBase if (this.panelOpen) { // Only emit if the panel was visible. - this.autocomplete.closed.emit(); + // 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._isOpen = this._overlayAttached = false; diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 60b96d6eebc1..daec4b061137 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -3371,6 +3371,31 @@ describe('MatAutocomplete', () => { expect(fixture.componentInstance.trigger.panelOpen).toBe(true); }); + + it('should emit from `autocomplete.closed` after click outside inside the NgZone', fakeAsync(() => { + const inZoneSpy = jasmine.createSpy('in zone spy'); + + const fixture = createComponent(SimpleAutocomplete, [ + {provide: NgZone, useFactory: () => new NgZone({enableLongStackTrace: false})}, + ]); + const zone = TestBed.inject(NgZone); + fixture.detectChanges(); + + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + flush(); + + const subscription = fixture.componentInstance.trigger.autocomplete.closed.subscribe(() => + inZoneSpy(NgZone.isInAngularZone()), + ); + zone.onStable.emit(null); + + dispatchFakeEvent(document, 'click'); + + expect(inZoneSpy).toHaveBeenCalledWith(true); + + subscription.unsubscribe(); + })); }); const SIMPLE_AUTOCOMPLETE_TEMPLATE = `