5
5
* Use of this source code is governed by an MIT-style license that can be
6
6
* found in the LICENSE file at https://angular.dev/license
7
7
*/
8
- import { AnimationEvent } from '@angular/animations' ;
9
8
import {
10
9
FocusMonitor ,
11
10
FocusOrigin ,
@@ -20,7 +19,6 @@ import {Platform} from '@angular/cdk/platform';
20
19
import { CdkScrollable , ScrollDispatcher , ViewportRuler } from '@angular/cdk/scrolling' ;
21
20
import { DOCUMENT } from '@angular/common' ;
22
21
import {
23
- AfterContentChecked ,
24
22
AfterContentInit ,
25
23
afterNextRender ,
26
24
AfterRenderPhase ,
@@ -48,7 +46,6 @@ import {
48
46
} from '@angular/core' ;
49
47
import { fromEvent , merge , Observable , Subject } from 'rxjs' ;
50
48
import { debounceTime , filter , map , mapTo , startWith , take , takeUntil } from 'rxjs/operators' ;
51
- import { matDrawerAnimations } from './drawer-animations' ;
52
49
53
50
/**
54
51
* Throws an exception when two MatDrawer are matching the same position.
@@ -152,7 +149,6 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit
152
149
selector : 'mat-drawer' ,
153
150
exportAs : 'matDrawer' ,
154
151
templateUrl : 'drawer.html' ,
155
- animations : [ matDrawerAnimations . transformDrawer ] ,
156
152
host : {
157
153
'class' : 'mat-drawer' ,
158
154
// must prevent the browser from aligning text based on value
@@ -161,17 +157,17 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit
161
157
'[class.mat-drawer-over]' : 'mode === "over"' ,
162
158
'[class.mat-drawer-push]' : 'mode === "push"' ,
163
159
'[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' ,
165
164
'tabIndex' : '-1' ,
166
- '[@transform]' : '_animationState' ,
167
- '(@transform.start)' : '_animationStarted.next($event)' ,
168
- '(@transform.done)' : '_animationEnd.next($event)' ,
169
165
} ,
170
166
changeDetection : ChangeDetectionStrategy . OnPush ,
171
167
encapsulation : ViewEncapsulation . None ,
172
168
imports : [ CdkScrollable ] ,
173
169
} )
174
- export class MatDrawer implements AfterViewInit , AfterContentChecked , OnDestroy {
170
+ export class MatDrawer implements AfterViewInit , OnDestroy {
175
171
private _elementRef = inject < ElementRef < HTMLElement > > ( ElementRef ) ;
176
172
private _focusTrapFactory = inject ( FocusTrapFactory ) ;
177
173
private _focusMonitor = inject ( FocusMonitor ) ;
@@ -184,9 +180,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
184
180
185
181
private _focusTrap : FocusTrap | null = null ;
186
182
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 ) [ ] ;
190
184
191
185
/** Whether the view of the component has been attached. */
192
186
private _isAttached : boolean ;
@@ -284,13 +278,10 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
284
278
private _openedVia : FocusOrigin | null ;
285
279
286
280
/** Emits whenever the drawer has started animating. */
287
- readonly _animationStarted = new Subject < AnimationEvent > ( ) ;
281
+ readonly _animationStarted = new Subject ( ) ;
288
282
289
283
/** 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' ;
284
+ readonly _animationEnd = new Subject ( ) ;
294
285
295
286
/** Event emitted when the drawer open state is changed. */
296
287
@Output ( ) readonly openedChange : EventEmitter < boolean > =
@@ -307,7 +298,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
307
298
/** Event emitted when the drawer has started opening. */
308
299
@Output ( )
309
300
readonly openedStart : Observable < void > = this . _animationStarted . pipe (
310
- filter ( e => e . fromState !== e . toState && e . toState . indexOf ( 'open' ) === 0 ) ,
301
+ filter ( ( ) => this . opened ) ,
311
302
mapTo ( undefined ) ,
312
303
) ;
313
304
@@ -321,7 +312,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
321
312
/** Event emitted when the drawer has started closing. */
322
313
@Output ( )
323
314
readonly closedStart : Observable < void > = this . _animationStarted . pipe (
324
- filter ( e => e . fromState !== e . toState && e . toState === 'void' ) ,
315
+ filter ( ( ) => ! this . opened ) ,
325
316
mapTo ( undefined ) ,
326
317
) ;
327
318
@@ -364,7 +355,8 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
364
355
* and we don't have close disabled.
365
356
*/
366
357
this . _ngZone . runOutsideAngular ( ( ) => {
367
- ( fromEvent ( this . _elementRef . nativeElement , 'keydown' ) as Observable < KeyboardEvent > )
358
+ const element = this . _elementRef . nativeElement ;
359
+ ( fromEvent ( element , 'keydown' ) as Observable < KeyboardEvent > )
368
360
. pipe (
369
361
filter ( event => {
370
362
return event . keyCode === ESCAPE && ! this . disableClose && ! hasModifierKey ( event ) ;
@@ -378,17 +370,16 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
378
370
event . preventDefault ( ) ;
379
371
} ) ,
380
372
) ;
381
- } ) ;
382
373
383
- this . _animationEnd . subscribe ( ( event : AnimationEvent ) => {
384
- const { fromState, toState} = event ;
374
+ this . _eventCleanups = [
375
+ this . _renderer . listen ( element , 'transitionrun' , this . _handleTransitionEvent ) ,
376
+ this . _renderer . listen ( element , 'transitionend' , this . _handleTransitionEvent ) ,
377
+ this . _renderer . listen ( element , 'transitioncancel' , this . _handleTransitionEvent ) ,
378
+ ] ;
379
+ } ) ;
385
380
386
- if (
387
- ( toState . indexOf ( 'open' ) === 0 && fromState === 'void' ) ||
388
- ( toState === 'void' && fromState . indexOf ( 'open' ) === 0 )
389
- ) {
390
- this . openedChange . emit ( this . _opened ) ;
391
- }
381
+ this . _animationEnd . subscribe ( ( ) => {
382
+ this . openedChange . emit ( this . _opened ) ;
392
383
} ) ;
393
384
}
394
385
@@ -508,17 +499,8 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
508
499
}
509
500
}
510
501
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
-
521
502
ngOnDestroy ( ) {
503
+ this . _eventCleanups . forEach ( cleanup => cleanup ( ) ) ;
522
504
this . _focusTrap ?. destroy ( ) ;
523
505
this . _anchor ?. remove ( ) ;
524
506
this . _anchor = null ;
@@ -588,15 +570,28 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
588
570
restoreFocus : boolean ,
589
571
focusOrigin : Exclude < FocusOrigin , null > ,
590
572
) : Promise < MatDrawerToggleResult > {
573
+ if ( isOpen === this . _opened ) {
574
+ return Promise . resolve ( isOpen ? 'open' : 'close' ) ;
575
+ }
576
+
591
577
this . _opened = isOpen ;
592
578
593
- if ( isOpen ) {
594
- this . _animationState = this . _enableAnimations ? 'open' : 'open-instant' ;
579
+ if ( this . _container ?. _transitionsEnabled ) {
580
+ // Note: it's importatnt to set this as early as possible,
581
+ // otherwise the animation can look glitchy in some cases.
582
+ this . _setIsAnimating ( true ) ;
595
583
} else {
596
- this . _animationState = 'void' ;
597
- if ( restoreFocus ) {
598
- this . _restoreFocus ( focusOrigin ) ;
599
- }
584
+ // Simulate the animation events if animations are disabled.
585
+ setTimeout ( ( ) => {
586
+ this . _animationStarted . next ( ) ;
587
+ this . _animationEnd . next ( ) ;
588
+ } ) ;
589
+ }
590
+
591
+ this . _elementRef . nativeElement . classList . toggle ( 'mat-drawer-opened' , isOpen ) ;
592
+
593
+ if ( ! isOpen && restoreFocus ) {
594
+ this . _restoreFocus ( focusOrigin ) ;
600
595
}
601
596
602
597
// Needed to ensure that the closing sequence fires off correctly.
@@ -608,8 +603,13 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
608
603
} ) ;
609
604
}
610
605
606
+ /** Toggles whether the drawer is currently animating. */
607
+ private _setIsAnimating ( isAnimating : boolean ) {
608
+ this . _elementRef . nativeElement . classList . toggle ( 'mat-drawer-animating' , isAnimating ) ;
609
+ }
610
+
611
611
_getWidth ( ) : number {
612
- return this . _elementRef . nativeElement ? this . _elementRef . nativeElement . offsetWidth || 0 : 0 ;
612
+ return this . _elementRef . nativeElement . offsetWidth || 0 ;
613
613
}
614
614
615
615
/** Updates the enabled state of the focus trap. */
@@ -647,6 +647,28 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
647
647
this . _anchor . parentNode ! . insertBefore ( element , this . _anchor ) ;
648
648
}
649
649
}
650
+
651
+ /** Event handler for animation events. */
652
+ private _handleTransitionEvent = ( event : TransitionEvent ) => {
653
+ const element = this . _elementRef . nativeElement ;
654
+ console . log ( event ) ;
655
+
656
+ if ( event . target === element ) {
657
+ this . _ngZone . run ( ( ) => {
658
+ if ( event . type === 'transitionrun' ) {
659
+ this . _animationStarted . next ( event ) ;
660
+ } else {
661
+ // Don't toggle the animating state on `transitioncancel` since another animation should
662
+ // start afterwards. This prevents the drawer from blinking if an animation is interrupted.
663
+ if ( event . type === 'transitionend' ) {
664
+ this . _setIsAnimating ( false ) ;
665
+ }
666
+
667
+ this . _animationEnd . next ( event ) ;
668
+ }
669
+ } ) ;
670
+ }
671
+ } ;
650
672
}
651
673
652
674
/**
@@ -680,6 +702,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
680
702
private _ngZone = inject ( NgZone ) ;
681
703
private _changeDetectorRef = inject ( ChangeDetectorRef ) ;
682
704
private _animationMode = inject ( ANIMATION_MODULE_TYPE , { optional : true } ) ;
705
+ _transitionsEnabled = false ;
683
706
684
707
/** All drawers in the container. Includes drawers from inside nested containers. */
685
708
@ContentChildren ( MatDrawer , {
@@ -777,6 +800,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
777
800
constructor ( ...args : unknown [ ] ) ;
778
801
779
802
constructor ( ) {
803
+ const platform = inject ( Platform ) ;
780
804
const viewportRuler = inject ( ViewportRuler ) ;
781
805
782
806
// If a `Dir` directive exists up the tree, listen direction changes
@@ -792,6 +816,17 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
792
816
. change ( )
793
817
. pipe ( takeUntil ( this . _destroyed ) )
794
818
. subscribe ( ( ) => this . updateContentMargins ( ) ) ;
819
+
820
+ if ( this . _animationMode !== 'NoopAnimations' && platform . isBrowser ) {
821
+ this . _ngZone . runOutsideAngular ( ( ) => {
822
+ // Enable the animations after a delay in order to skip
823
+ // the initial transition if a drawer is open by default.
824
+ setTimeout ( ( ) => {
825
+ this . _element . nativeElement . classList . add ( 'mat-drawer-transition' ) ;
826
+ this . _transitionsEnabled = true ;
827
+ } , 200 ) ;
828
+ } ) ;
829
+ }
795
830
}
796
831
797
832
ngAfterContentInit ( ) {
@@ -915,21 +950,10 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
915
950
* is properly hidden.
916
951
*/
917
952
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
- } ) ;
953
+ drawer . _animationStarted . pipe ( takeUntil ( this . _drawers . changes ) ) . subscribe ( ( ) => {
954
+ this . updateContentMargins ( ) ;
955
+ this . _changeDetectorRef . markForCheck ( ) ;
956
+ } ) ;
933
957
934
958
if ( drawer . mode !== 'side' ) {
935
959
drawer . openedChange
0 commit comments