From 8584e85c9e5f0adcd602312625127d9c4f703041 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 24 Aug 2026 21:30:15 +0500 Subject: [PATCH 1/3] fix(material/sidenav): drawer stuck in animating state if transition does not start --- src/material/sidenav/drawer.spec.ts | 28 ++++++++++++++++++++++++++++ src/material/sidenav/drawer.ts | 15 ++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/material/sidenav/drawer.spec.ts b/src/material/sidenav/drawer.spec.ts index c16fa2e45ad0..f528f530b447 100644 --- a/src/material/sidenav/drawer.spec.ts +++ b/src/material/sidenav/drawer.spec.ts @@ -923,6 +923,34 @@ describe('MatDrawer', () => { return Array.from(fixture.nativeElement.querySelector('.mat-drawer-container').childNodes); } }); + + describe('with animations', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [{provide: MATERIAL_ANIMATIONS, useValue: {animationsDisabled: false}}], + }); + }); + + it('should not stay in the animating state if a transition does not start', async () => { + const fixture = TestBed.createComponent(BasicTestApp); + fixture.detectChanges(); + + const testComponent: BasicTestApp = fixture.debugElement.componentInstance; + const drawer = fixture.debugElement.query(By.directive(MatDrawer))!; + + // Wait for the container to enable the transitions. + await wait(250); + + drawer.componentInstance.open(); + drawer.componentInstance.close(); + fixture.detectChanges(); + await wait(100); + fixture.detectChanges(); + + expect(drawer.nativeElement.classList).not.toContain('mat-drawer-animating'); + expect(testComponent.closeCount).toBe(1); + }); + }); }); describe('MatDrawerContainer', () => { diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index 2702d1b8e10c..5837a25c518c 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -207,6 +207,9 @@ export class MatDrawer implements AfterViewInit, OnDestroy { /** Whether the view of the component has been attached. */ private _isAttached = false; + /** Whether the drawer is currently animating. */ + private _isAnimating = false; + /** Anchor node used to restore the drawer to its initial position. */ private _anchor: Comment | null = null; @@ -594,7 +597,16 @@ export class MatDrawer implements AfterViewInit, OnDestroy { // Previously we dispatched this in a `transitionrun` event, but it might not fire // if the element is hidden (see #32992). Since this event is load-bearing for the // margin calculations, we need it to fire consistently. - setTimeout(() => this._animationStarted.next()); + setTimeout(() => { + this._animationStarted.next(); + + // No transition will run if the drawer was toggled back to its previous state before the + // browser had a chance to render it, in which case we have to end the animation ourselves. + if (this._isAnimating && this._elementRef.nativeElement.getAnimations().length === 0) { + this._setIsAnimating(false); + this._animationEnd.next(); + } + }); } else { // Simulate the animation events if animations are disabled. setTimeout(() => { @@ -625,6 +637,7 @@ export class MatDrawer implements AfterViewInit, OnDestroy { /** Toggles whether the drawer is currently animating. */ private _setIsAnimating(isAnimating: boolean) { + this._isAnimating = isAnimating; this._elementRef.nativeElement.classList.toggle('mat-drawer-animating', isAnimating); } From fa9e36f3aaa117834dc9b01415e3e2141a20c158 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 24 Aug 2026 21:57:15 +0500 Subject: [PATCH 2/3] fix(material/sidenav): avoid ending the drawer animation prematurely --- src/material/sidenav/BUILD.bazel | 1 + src/material/sidenav/drawer.spec.ts | 12 +++++++++++- src/material/sidenav/drawer.ts | 17 ++++++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/material/sidenav/BUILD.bazel b/src/material/sidenav/BUILD.bazel index 12b246015cc9..57323b2e75de 100644 --- a/src/material/sidenav/BUILD.bazel +++ b/src/material/sidenav/BUILD.bazel @@ -105,6 +105,7 @@ ng_project( "//src/cdk/a11y", "//src/cdk/bidi", "//src/cdk/keycodes", + "//src/cdk/layout", "//src/cdk/platform", "//src/cdk/scrolling", "//src/cdk/testing", diff --git a/src/material/sidenav/drawer.spec.ts b/src/material/sidenav/drawer.spec.ts index f528f530b447..af97979f9ccb 100644 --- a/src/material/sidenav/drawer.spec.ts +++ b/src/material/sidenav/drawer.spec.ts @@ -1,6 +1,7 @@ import {A11yModule} from '@angular/cdk/a11y'; import {Direction} from '@angular/cdk/bidi'; import {ESCAPE} from '@angular/cdk/keycodes'; +import {MediaMatcher} from '@angular/cdk/layout'; import {CdkScrollable} from '@angular/cdk/scrolling'; import { createKeyboardEvent, @@ -927,7 +928,16 @@ describe('MatDrawer', () => { describe('with animations', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [{provide: MATERIAL_ANIMATIONS, useValue: {animationsDisabled: false}}], + providers: [ + {provide: MATERIAL_ANIMATIONS, useValue: {animationsDisabled: false}}, + // Ensure that the transitions aren't disabled if the machine has reduced motion enabled. + { + provide: MediaMatcher, + useValue: { + matchMedia: () => ({matches: false, addListener: () => {}, removeListener: () => {}}), + }, + }, + ], }); }); diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index 5837a25c518c..b296fef374ec 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -210,6 +210,9 @@ export class MatDrawer implements AfterViewInit, OnDestroy { /** Whether the drawer is currently animating. */ private _isAnimating = false; + /** Opened state that the drawer had when it started animating. */ + private _openedBeforeAnimating = false; + /** Anchor node used to restore the drawer to its initial position. */ private _anchor: Comment | null = null; @@ -590,6 +593,10 @@ export class MatDrawer implements AfterViewInit, OnDestroy { this._getContent()?._drawerToggled(this); if (this._container?._transitionsEnabled) { + if (!this._isAnimating) { + this._openedBeforeAnimating = !isOpen; + } + // Note: it's important to set this as early as possible, // otherwise the animation can look glitchy in some cases. this._setIsAnimating(true); @@ -600,9 +607,13 @@ export class MatDrawer implements AfterViewInit, OnDestroy { setTimeout(() => { this._animationStarted.next(); - // No transition will run if the drawer was toggled back to its previous state before the - // browser had a chance to render it, in which case we have to end the animation ourselves. - if (this._isAnimating && this._elementRef.nativeElement.getAnimations().length === 0) { + // No transition will run if the drawer ended up back in the state it started animating + // from, in which case we have to end the animation ourselves. + if ( + this._isAnimating && + this.opened === this._openedBeforeAnimating && + this._elementRef.nativeElement.getAnimations().length === 0 + ) { this._setIsAnimating(false); this._animationEnd.next(); } From 99f0b175cb4a9919b2bfe91497f6cac44b1add66 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 24 Aug 2026 22:07:09 +0500 Subject: [PATCH 3/3] test(material/sidenav): configure the test module inside the test --- src/material/sidenav/drawer.spec.ts | 70 ++++++++++++++--------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/src/material/sidenav/drawer.spec.ts b/src/material/sidenav/drawer.spec.ts index af97979f9ccb..e5ad56723cf8 100644 --- a/src/material/sidenav/drawer.spec.ts +++ b/src/material/sidenav/drawer.spec.ts @@ -790,6 +790,39 @@ describe('MatDrawer', () => { expect(scrollable.getElementRef().nativeElement).toBe(content.nativeElement); }); + it('should not stay in the animating state if a transition does not start', async () => { + TestBed.configureTestingModule({ + providers: [ + {provide: MATERIAL_ANIMATIONS, useValue: {animationsDisabled: false}}, + // Ensure that the transitions aren't disabled if the machine has reduced motion enabled. + { + provide: MediaMatcher, + useValue: { + matchMedia: () => ({matches: false, addListener: () => {}, removeListener: () => {}}), + }, + }, + ], + }); + + const fixture = TestBed.createComponent(BasicTestApp); + fixture.detectChanges(); + + const testComponent: BasicTestApp = fixture.debugElement.componentInstance; + const drawer = fixture.debugElement.query(By.directive(MatDrawer))!; + + // Wait for the container to enable the transitions. + await wait(250); + + drawer.componentInstance.open(); + drawer.componentInstance.close(); + fixture.detectChanges(); + await wait(100); + fixture.detectChanges(); + + expect(drawer.nativeElement.classList).not.toContain('mat-drawer-animating'); + expect(testComponent.closeCount).toBe(1); + }); + describe('DOM position', () => { it('should project start drawer before the content', () => { const fixture = TestBed.createComponent(BasicTestApp); @@ -924,43 +957,6 @@ describe('MatDrawer', () => { return Array.from(fixture.nativeElement.querySelector('.mat-drawer-container').childNodes); } }); - - describe('with animations', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - {provide: MATERIAL_ANIMATIONS, useValue: {animationsDisabled: false}}, - // Ensure that the transitions aren't disabled if the machine has reduced motion enabled. - { - provide: MediaMatcher, - useValue: { - matchMedia: () => ({matches: false, addListener: () => {}, removeListener: () => {}}), - }, - }, - ], - }); - }); - - it('should not stay in the animating state if a transition does not start', async () => { - const fixture = TestBed.createComponent(BasicTestApp); - fixture.detectChanges(); - - const testComponent: BasicTestApp = fixture.debugElement.componentInstance; - const drawer = fixture.debugElement.query(By.directive(MatDrawer))!; - - // Wait for the container to enable the transitions. - await wait(250); - - drawer.componentInstance.open(); - drawer.componentInstance.close(); - fixture.detectChanges(); - await wait(100); - fixture.detectChanges(); - - expect(drawer.nativeElement.classList).not.toContain('mat-drawer-animating'); - expect(testComponent.closeCount).toBe(1); - }); - }); }); describe('MatDrawerContainer', () => {