From db3bf3d0308a9b1d8d62fceb586f33b1248147d6 Mon Sep 17 00:00:00 2001 From: Egor Volvachev Date: Thu, 21 Apr 2022 18:24:38 +0300 Subject: [PATCH] fix(material/autocomplete): outside click in Angular zone. Fixes a bug in Angular Material `autocomplete` when outside click doesn't trigger `changeDetection`. Fixes #24811 --- .../mdc-autocomplete/autocomplete.spec.ts | 26 +++++++++++++++++++ .../autocomplete/autocomplete-trigger.ts | 7 ++++- .../autocomplete/autocomplete.spec.ts | 26 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 47f40aab81a0..75c452880cd3 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -3365,6 +3365,32 @@ 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).toHaveBeenCalled(); + 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..bb335761fab0 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -3371,6 +3371,32 @@ 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).toHaveBeenCalled(); + expect(inZoneSpy).toHaveBeenCalledWith(true); + + subscription.unsubscribe(); + })); }); const SIMPLE_AUTOCOMPLETE_TEMPLATE = `