diff --git a/src/lib/slide-toggle/slide-toggle.spec.ts b/src/lib/slide-toggle/slide-toggle.spec.ts index f9e512118463..84eaee5e0d72 100644 --- a/src/lib/slide-toggle/slide-toggle.spec.ts +++ b/src/lib/slide-toggle/slide-toggle.spec.ts @@ -1,7 +1,14 @@ import {MutationObserverFactory} from '@angular/cdk/observers'; import {dispatchFakeEvent} from '@angular/cdk/testing'; import {Component} from '@angular/core'; -import {ComponentFixture, fakeAsync, flushMicrotasks, TestBed, tick} from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + flush, + flushMicrotasks, + TestBed, + tick, +} from '@angular/core/testing'; import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms'; import {defaultRippleAnimationConfig} from '@angular/material/core'; import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; @@ -773,7 +780,7 @@ describe('MatSlideToggle with forms', () => { expect(slideToggleElement.classList).toContain('mat-checked'); })); - it('should have the correct control state initially and after interaction', () => { + it('should have the correct control state initially and after interaction', fakeAsync(() => { // The control should start off valid, pristine, and untouched. expect(slideToggleModel.valid).toBe(true); expect(slideToggleModel.pristine).toBe(true); @@ -795,13 +802,31 @@ describe('MatSlideToggle with forms', () => { // also turn touched. dispatchFakeEvent(inputElement, 'blur'); fixture.detectChanges(); + flushMicrotasks(); expect(slideToggleModel.valid).toBe(true); expect(slideToggleModel.pristine).toBe(false); expect(slideToggleModel.touched).toBe(true); - }); + })); + + it('should not throw an error when disabling while focused', fakeAsync(() => { + expect(() => { + // Focus the input element because after disabling, the `blur` event should automatically + // fire and not result in a changed after checked exception. Related: #12323 + inputElement.focus(); + + // Flush the two nested timeouts from the FocusMonitor that are being created on `focus`. + flush(); + + slideToggle.disabled = true; + fixture.detectChanges(); + flushMicrotasks(); + }).not.toThrow(); + })); + + it('should not set the control to touched when changing the state programmatically', + fakeAsync(() => { - it('should not set the control to touched when changing the state programmatically', () => { // The control should start off with being untouched. expect(slideToggleModel.touched).toBe(false); @@ -815,10 +840,11 @@ describe('MatSlideToggle with forms', () => { // also turn touched. dispatchFakeEvent(inputElement, 'blur'); fixture.detectChanges(); + flushMicrotasks(); expect(slideToggleModel.touched).toBe(true); expect(slideToggleElement.classList).toContain('mat-checked'); - }); + })); it('should not set the control to touched when changing the model', fakeAsync(() => { // The control should start off with being untouched. diff --git a/src/lib/slide-toggle/slide-toggle.ts b/src/lib/slide-toggle/slide-toggle.ts index bb4e02cded50..f82ac0a3cdb8 100644 --- a/src/lib/slide-toggle/slide-toggle.ts +++ b/src/lib/slide-toggle/slide-toggle.ts @@ -289,7 +289,10 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro // For keyboard focus show a persistent ripple as focus indicator. this._focusRipple = this._ripple.launch(0, 0, {persistent: true}); } else if (!focusOrigin) { - this.onTouched(); + // Do not immediately mark the component as touched because it can happen that the `blur` + // event from `FocusMonitor` fires, while the component is checked after a change detection. + // Immediately updating would then result in a changed after checked exception. Rel: #12323 + Promise.resolve().then(() => this.onTouched()); // Fade out and clear the focus ripple if one is currently present. if (this._focusRipple) {