55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.dev/license
77 */
8- import { AnimationEvent } from '@angular/animations' ;
98import {
109 FocusMonitor ,
1110 FocusOrigin ,
@@ -20,7 +19,6 @@ import {Platform} from '@angular/cdk/platform';
2019import { CdkScrollable , ScrollDispatcher , ViewportRuler } from '@angular/cdk/scrolling' ;
2120import { DOCUMENT } from '@angular/common' ;
2221import {
23- AfterContentChecked ,
2422 AfterContentInit ,
2523 afterNextRender ,
2624 AfterRenderPhase ,
@@ -48,7 +46,6 @@ import {
4846} from '@angular/core' ;
4947import { fromEvent , merge , Observable , Subject } from 'rxjs' ;
5048import { debounceTime , filter , map , mapTo , startWith , take , takeUntil } from 'rxjs/operators' ;
51- import { matDrawerAnimations } from './drawer-animations' ;
5249
5350/**
5451 * Throws an exception when two MatDrawer are matching the same position.
@@ -152,7 +149,6 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit
152149 selector : 'mat-drawer' ,
153150 exportAs : 'matDrawer' ,
154151 templateUrl : 'drawer.html' ,
155- animations : [ matDrawerAnimations . transformDrawer ] ,
156152 host : {
157153 'class' : 'mat-drawer' ,
158154 // must prevent the browser from aligning text based on value
@@ -161,17 +157,17 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit
161157 '[class.mat-drawer-over]' : 'mode === "over"' ,
162158 '[class.mat-drawer-push]' : 'mode === "push"' ,
163159 '[class.mat-drawer-side]' : 'mode === "side"' ,
164- '[class.mat-drawer-opened]' : 'opened' ,
160+ // The styles that render the sidenav off-screen come from the drawer container. Prior to #30235
161+ // this was also done by the animations module which some internal tests seem to depend on.
162+ // Simulate it by toggling the `hidden` attribute instead.
163+ '[style.visibility]' : '(!_container && !opened) ? "hidden" : null' ,
165164 'tabIndex' : '-1' ,
166- '[@transform]' : '_animationState' ,
167- '(@transform.start)' : '_animationStarted.next($event)' ,
168- '(@transform.done)' : '_animationEnd.next($event)' ,
169165 } ,
170166 changeDetection : ChangeDetectionStrategy . OnPush ,
171167 encapsulation : ViewEncapsulation . None ,
172168 imports : [ CdkScrollable ] ,
173169} )
174- export class MatDrawer implements AfterViewInit , AfterContentChecked , OnDestroy {
170+ export class MatDrawer implements AfterViewInit , OnDestroy {
175171 private _elementRef = inject < ElementRef < HTMLElement > > ( ElementRef ) ;
176172 private _focusTrapFactory = inject ( FocusTrapFactory ) ;
177173 private _focusMonitor = inject ( FocusMonitor ) ;
@@ -184,9 +180,8 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
184180
185181 private _focusTrap : FocusTrap | null = null ;
186182 private _elementFocusedBeforeDrawerWasOpened : HTMLElement | null = null ;
187-
188- /** Whether the drawer is initialized. Used for disabling the initial animation. */
189- private _enableAnimations = false ;
183+ private _eventCleanups : ( ( ) => void ) [ ] ;
184+ private _fallbackTimer : ReturnType < typeof setTimeout > | undefined ;
190185
191186 /** Whether the view of the component has been attached. */
192187 private _isAttached : boolean ;
@@ -284,13 +279,10 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
284279 private _openedVia : FocusOrigin | null ;
285280
286281 /** Emits whenever the drawer has started animating. */
287- readonly _animationStarted = new Subject < AnimationEvent > ( ) ;
282+ readonly _animationStarted = new Subject ( ) ;
288283
289284 /** Emits whenever the drawer is done animating. */
290- readonly _animationEnd = new Subject < AnimationEvent > ( ) ;
291-
292- /** Current state of the sidenav animation. */
293- _animationState : 'open-instant' | 'open' | 'void' = 'void' ;
285+ readonly _animationEnd = new Subject ( ) ;
294286
295287 /** Event emitted when the drawer open state is changed. */
296288 @Output ( ) readonly openedChange : EventEmitter < boolean > =
@@ -307,7 +299,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
307299 /** Event emitted when the drawer has started opening. */
308300 @Output ( )
309301 readonly openedStart : Observable < void > = this . _animationStarted . pipe (
310- filter ( e => e . fromState !== e . toState && e . toState . indexOf ( 'open' ) === 0 ) ,
302+ filter ( ( ) => this . opened ) ,
311303 mapTo ( undefined ) ,
312304 ) ;
313305
@@ -321,7 +313,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
321313 /** Event emitted when the drawer has started closing. */
322314 @Output ( )
323315 readonly closedStart : Observable < void > = this . _animationStarted . pipe (
324- filter ( e => e . fromState !== e . toState && e . toState === 'void' ) ,
316+ filter ( ( ) => ! this . opened ) ,
325317 mapTo ( undefined ) ,
326318 ) ;
327319
@@ -364,7 +356,8 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
364356 * and we don't have close disabled.
365357 */
366358 this . _ngZone . runOutsideAngular ( ( ) => {
367- ( fromEvent ( this . _elementRef . nativeElement , 'keydown' ) as Observable < KeyboardEvent > )
359+ const element = this . _elementRef . nativeElement ;
360+ ( fromEvent ( element , 'keydown' ) as Observable < KeyboardEvent > )
368361 . pipe (
369362 filter ( event => {
370363 return event . keyCode === ESCAPE && ! this . disableClose && ! hasModifierKey ( event ) ;
@@ -378,17 +371,16 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
378371 event . preventDefault ( ) ;
379372 } ) ,
380373 ) ;
381- } ) ;
382374
383- this . _animationEnd . subscribe ( ( event : AnimationEvent ) => {
384- const { fromState, toState} = event ;
375+ this . _eventCleanups = [
376+ this . _renderer . listen ( element , 'transitionrun' , this . _handleTransitionEvent ) ,
377+ this . _renderer . listen ( element , 'transitionend' , this . _handleTransitionEvent ) ,
378+ this . _renderer . listen ( element , 'transitioncancel' , this . _handleTransitionEvent ) ,
379+ ] ;
380+ } ) ;
385381
386- if (
387- ( toState . indexOf ( 'open' ) === 0 && fromState === 'void' ) ||
388- ( toState === 'void' && fromState . indexOf ( 'open' ) === 0 )
389- ) {
390- this . openedChange . emit ( this . _opened ) ;
391- }
382+ this . _animationEnd . subscribe ( ( ) => {
383+ this . openedChange . emit ( this . _opened ) ;
392384 } ) ;
393385 }
394386
@@ -508,17 +500,9 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
508500 }
509501 }
510502
511- ngAfterContentChecked ( ) {
512- // Enable the animations after the lifecycle hooks have run, in order to avoid animating
513- // drawers that are open by default. When we're on the server, we shouldn't enable the
514- // animations, because we don't want the drawer to animate the first time the user sees
515- // the page.
516- if ( this . _platform . isBrowser ) {
517- this . _enableAnimations = true ;
518- }
519- }
520-
521503 ngOnDestroy ( ) {
504+ clearTimeout ( this . _fallbackTimer ) ;
505+ this . _eventCleanups . forEach ( cleanup => cleanup ( ) ) ;
522506 this . _focusTrap ?. destroy ( ) ;
523507 this . _anchor ?. remove ( ) ;
524508 this . _anchor = null ;
@@ -588,15 +572,28 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
588572 restoreFocus : boolean ,
589573 focusOrigin : Exclude < FocusOrigin , null > ,
590574 ) : Promise < MatDrawerToggleResult > {
575+ if ( isOpen === this . _opened ) {
576+ return Promise . resolve ( isOpen ? 'open' : 'close' ) ;
577+ }
578+
591579 this . _opened = isOpen ;
592580
593- if ( isOpen ) {
594- this . _animationState = this . _enableAnimations ? 'open' : 'open-instant' ;
581+ if ( this . _container ?. _transitionsEnabled ) {
582+ // Note: it's importatnt to set this as early as possible,
583+ // otherwise the animation can look glitchy in some cases.
584+ this . _setIsAnimating ( true ) ;
595585 } else {
596- this . _animationState = 'void' ;
597- if ( restoreFocus ) {
598- this . _restoreFocus ( focusOrigin ) ;
599- }
586+ // Simulate the animation events if animations are disabled.
587+ setTimeout ( ( ) => {
588+ this . _animationStarted . next ( ) ;
589+ this . _animationEnd . next ( ) ;
590+ } ) ;
591+ }
592+
593+ this . _elementRef . nativeElement . classList . toggle ( 'mat-drawer-opened' , isOpen ) ;
594+
595+ if ( ! isOpen && restoreFocus ) {
596+ this . _restoreFocus ( focusOrigin ) ;
600597 }
601598
602599 // Needed to ensure that the closing sequence fires off correctly.
@@ -608,8 +605,23 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
608605 } ) ;
609606 }
610607
608+ /** Toggles whether the drawer is currently animating. */
609+ private _setIsAnimating ( isAnimating : boolean ) {
610+ clearTimeout ( this . _fallbackTimer ) ;
611+ this . _elementRef . nativeElement . classList . toggle ( 'mat-drawer-animating' , isAnimating ) ;
612+
613+ // Some internal integration tests disable animations by setting `* {transition: none}`. This
614+ // will stop transition events from firing and prevent this class from being removed. Since
615+ // it's somewhat load-bearing we need a fallback for such cases.
616+ if ( isAnimating ) {
617+ this . _fallbackTimer = this . _ngZone . runOutsideAngular ( ( ) =>
618+ setTimeout ( ( ) => this . _setIsAnimating ( false ) , 500 ) ,
619+ ) ;
620+ }
621+ }
622+
611623 _getWidth ( ) : number {
612- return this . _elementRef . nativeElement ? this . _elementRef . nativeElement . offsetWidth || 0 : 0 ;
624+ return this . _elementRef . nativeElement . offsetWidth || 0 ;
613625 }
614626
615627 /** Updates the enabled state of the focus trap. */
@@ -647,6 +659,28 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
647659 this . _anchor . parentNode ! . insertBefore ( element , this . _anchor ) ;
648660 }
649661 }
662+
663+ /** Event handler for animation events. */
664+ private _handleTransitionEvent = ( event : TransitionEvent ) => {
665+ const element = this . _elementRef . nativeElement ;
666+ console . log ( event ) ;
667+
668+ if ( event . target === element ) {
669+ this . _ngZone . run ( ( ) => {
670+ if ( event . type === 'transitionrun' ) {
671+ this . _animationStarted . next ( event ) ;
672+ } else {
673+ // Don't toggle the animating state on `transitioncancel` since another animation should
674+ // start afterwards. This prevents the drawer from blinking if an animation is interrupted.
675+ if ( event . type === 'transitionend' ) {
676+ this . _setIsAnimating ( false ) ;
677+ }
678+
679+ this . _animationEnd . next ( event ) ;
680+ }
681+ } ) ;
682+ }
683+ } ;
650684}
651685
652686/**
@@ -680,6 +714,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
680714 private _ngZone = inject ( NgZone ) ;
681715 private _changeDetectorRef = inject ( ChangeDetectorRef ) ;
682716 private _animationMode = inject ( ANIMATION_MODULE_TYPE , { optional : true } ) ;
717+ _transitionsEnabled = false ;
683718
684719 /** All drawers in the container. Includes drawers from inside nested containers. */
685720 @ContentChildren ( MatDrawer , {
@@ -777,6 +812,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
777812 constructor ( ...args : unknown [ ] ) ;
778813
779814 constructor ( ) {
815+ const platform = inject ( Platform ) ;
780816 const viewportRuler = inject ( ViewportRuler ) ;
781817
782818 // If a `Dir` directive exists up the tree, listen direction changes
@@ -792,6 +828,17 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
792828 . change ( )
793829 . pipe ( takeUntil ( this . _destroyed ) )
794830 . subscribe ( ( ) => this . updateContentMargins ( ) ) ;
831+
832+ if ( this . _animationMode !== 'NoopAnimations' && platform . isBrowser ) {
833+ this . _ngZone . runOutsideAngular ( ( ) => {
834+ // Enable the animations after a delay in order to skip
835+ // the initial transition if a drawer is open by default.
836+ setTimeout ( ( ) => {
837+ this . _element . nativeElement . classList . add ( 'mat-drawer-transition' ) ;
838+ this . _transitionsEnabled = true ;
839+ } , 200 ) ;
840+ } ) ;
841+ }
795842 }
796843
797844 ngAfterContentInit ( ) {
@@ -915,21 +962,10 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
915962 * is properly hidden.
916963 */
917964 private _watchDrawerToggle ( drawer : MatDrawer ) : void {
918- drawer . _animationStarted
919- . pipe (
920- filter ( ( event : AnimationEvent ) => event . fromState !== event . toState ) ,
921- takeUntil ( this . _drawers . changes ) ,
922- )
923- . subscribe ( ( event : AnimationEvent ) => {
924- // Set the transition class on the container so that the animations occur. This should not
925- // be set initially because animations should only be triggered via a change in state.
926- if ( event . toState !== 'open-instant' && this . _animationMode !== 'NoopAnimations' ) {
927- this . _element . nativeElement . classList . add ( 'mat-drawer-transition' ) ;
928- }
929-
930- this . updateContentMargins ( ) ;
931- this . _changeDetectorRef . markForCheck ( ) ;
932- } ) ;
965+ drawer . _animationStarted . pipe ( takeUntil ( this . _drawers . changes ) ) . subscribe ( ( ) => {
966+ this . updateContentMargins ( ) ;
967+ this . _changeDetectorRef . markForCheck ( ) ;
968+ } ) ;
933969
934970 if ( drawer . mode !== 'side' ) {
935971 drawer . openedChange
0 commit comments