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 c16fa2e45ad0..e5ad56723cf8 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, @@ -789,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); diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index 2702d1b8e10c..b296fef374ec 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -207,6 +207,12 @@ 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; + + /** 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; @@ -587,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); @@ -594,7 +604,20 @@ 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 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(); + } + }); } else { // Simulate the animation events if animations are disabled. setTimeout(() => { @@ -625,6 +648,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); }