66 * found in the LICENSE file at https://angular.dev/license
77 */
88
9- import { AnimationEvent } from '@angular/animations' ;
109import { CdkAccordionItem } from '@angular/cdk/accordion' ;
1110import { UniqueSelectionDispatcher } from '@angular/cdk/collections' ;
1211import { CdkPortalOutlet , TemplatePortal } from '@angular/cdk/portal' ;
@@ -31,12 +30,12 @@ import {
3130 booleanAttribute ,
3231 ANIMATION_MODULE_TYPE ,
3332 inject ,
33+ NgZone ,
3434} from '@angular/core' ;
3535import { _IdGenerator } from '@angular/cdk/a11y' ;
3636import { Subject } from 'rxjs' ;
3737import { filter , startWith , take } from 'rxjs/operators' ;
3838import { MatAccordionBase , MatAccordionTogglePosition , MAT_ACCORDION } from './accordion-base' ;
39- import { matExpansionAnimations } from './expansion-animations' ;
4039import { MAT_EXPANSION_PANEL } from './expansion-panel-base' ;
4140import { MatExpansionPanelContent } from './expansion-panel-content' ;
4241
@@ -76,7 +75,6 @@ export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS =
7675 templateUrl : 'expansion-panel.html' ,
7776 encapsulation : ViewEncapsulation . None ,
7877 changeDetection : ChangeDetectionStrategy . OnPush ,
79- animations : [ matExpansionAnimations . bodyExpansion ] ,
8078 providers : [
8179 // Provide MatAccordion as undefined to prevent nested expansion panels from registering
8280 // to the same accordion.
@@ -86,7 +84,6 @@ export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS =
8684 host : {
8785 'class' : 'mat-expansion-panel' ,
8886 '[class.mat-expanded]' : 'expanded' ,
89- '[class._mat-animation-noopable]' : '_animationsDisabled' ,
9087 '[class.mat-expansion-panel-spacing]' : '_hasSpacing()' ,
9188 } ,
9289 imports : [ CdkPortalOutlet ] ,
@@ -96,10 +93,11 @@ export class MatExpansionPanel
9693 implements AfterContentInit , OnChanges , OnDestroy
9794{
9895 private _viewContainerRef = inject ( ViewContainerRef ) ;
99- _animationMode = inject ( ANIMATION_MODULE_TYPE , { optional : true } ) ;
100-
101- protected _animationsDisabled : boolean ;
96+ private readonly _animationsDisabled =
97+ inject ( ANIMATION_MODULE_TYPE , { optional : true } ) === 'NoopAnimations' ;
10298 private _document = inject ( DOCUMENT ) ;
99+ private _ngZone = inject ( NgZone ) ;
100+ private _elementRef = inject < ElementRef < HTMLElement > > ( ElementRef ) ;
103101
104102 /** Whether the toggle indicator should be hidden. */
105103 @Input ( { transform : booleanAttribute } )
@@ -139,6 +137,10 @@ export class MatExpansionPanel
139137 /** Element containing the panel's user-provided content. */
140138 @ViewChild ( 'body' ) _body : ElementRef < HTMLElement > ;
141139
140+ /** Element wrapping the panel body. */
141+ @ViewChild ( 'bodyWrapper' )
142+ protected _bodyWrapper : ElementRef < HTMLElement > | undefined ;
143+
142144 /** Portal holding the user's content. */
143145 _portal : TemplatePortal ;
144146
@@ -156,7 +158,6 @@ export class MatExpansionPanel
156158 ) ;
157159
158160 this . _expansionDispatcher = inject ( UniqueSelectionDispatcher ) ;
159- this . _animationsDisabled = this . _animationMode === 'NoopAnimations' ;
160161
161162 if ( defaultOptions ) {
162163 this . hideToggle = defaultOptions . hideToggle ;
@@ -204,6 +205,8 @@ export class MatExpansionPanel
204205 this . _portal = new TemplatePortal ( this . _lazyContent . _template , this . _viewContainerRef ) ;
205206 } ) ;
206207 }
208+
209+ this . _setupAnimationEvents ( ) ;
207210 }
208211
209212 ngOnChanges ( changes : SimpleChanges ) {
@@ -212,6 +215,10 @@ export class MatExpansionPanel
212215
213216 override ngOnDestroy ( ) {
214217 super . ngOnDestroy ( ) ;
218+ this . _bodyWrapper ?. nativeElement . removeEventListener (
219+ 'transitionend' ,
220+ this . _transitionEndListener ,
221+ ) ;
215222 this . _inputChanges . complete ( ) ;
216223 }
217224
@@ -226,38 +233,36 @@ export class MatExpansionPanel
226233 return false ;
227234 }
228235
229- /** Called when the expansion animation has started. */
230- protected _animationStarted ( event : AnimationEvent ) {
231- if ( ! isInitialAnimation ( event ) && ! this . _animationsDisabled && this . _body ) {
232- // Prevent the user from tabbing into the content while it's animating.
233- // TODO(crisbeto): maybe use `inert` to prevent focus from entering while closed as well
234- // instead of `visibility`? Will allow us to clean up some code but needs more testing.
235- this . _body ?. nativeElement . setAttribute ( 'inert' , '' ) ;
236+ private _transitionEndListener = ( { target, propertyName} : TransitionEvent ) => {
237+ if ( target === this . _bodyWrapper ?. nativeElement && propertyName === 'grid-template-rows' ) {
238+ this . _ngZone . run ( ( ) => {
239+ if ( this . expanded ) {
240+ this . afterExpand . emit ( ) ;
241+ } else {
242+ this . afterCollapse . emit ( ) ;
243+ }
244+ } ) ;
236245 }
237- }
238-
239- /** Called when the expansion animation has finished. */
240- protected _animationDone ( event : AnimationEvent ) {
241- if ( ! isInitialAnimation ( event ) ) {
242- if ( event . toState === 'expanded' ) {
243- this . afterExpand . emit ( ) ;
244- } else if ( event . toState === 'collapsed' ) {
245- this . afterCollapse . emit ( ) ;
246+ } ;
247+
248+ protected _setupAnimationEvents ( ) {
249+ // This method is defined separately, because we need to
250+ // disable this logic in some internal components.
251+ this . _ngZone . runOutsideAngular ( ( ) => {
252+ if ( this . _animationsDisabled ) {
253+ this . opened . subscribe ( ( ) => this . _ngZone . run ( ( ) => this . afterExpand . emit ( ) ) ) ;
254+ this . closed . subscribe ( ( ) => this . _ngZone . run ( ( ) => this . afterCollapse . emit ( ) ) ) ;
255+ } else {
256+ setTimeout ( ( ) => {
257+ const element = this . _elementRef . nativeElement ;
258+ element . addEventListener ( 'transitionend' , this . _transitionEndListener ) ;
259+ element . classList . add ( 'mat-expansion-panel-animations-enabled' ) ;
260+ } , 200 ) ;
246261 }
247-
248- // Re-enable tabbing once the animation is finished.
249- if ( ! this . _animationsDisabled && this . _body ) {
250- this . _body . nativeElement . removeAttribute ( 'inert' ) ;
251- }
252- }
262+ } ) ;
253263 }
254264}
255265
256- /** Checks whether an animation is the initial setup animation. */
257- function isInitialAnimation ( event : AnimationEvent ) : boolean {
258- return event . fromState === 'void' ;
259- }
260-
261266/**
262267 * Actions of a `<mat-expansion-panel>`.
263268 */
0 commit comments