angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.33k stars 6.73k forks source link

mat-expansion-panel "opens" with angular enter-leave animations, although current state is closed. #11765

Open bencbradshaw opened 6 years ago

bencbradshaw commented 6 years ago

Bug, feature request, or proposal:

Bug

What is the expected behavior?

Mat-expansion-panel, when animating in or out, will stay in current state of opened or closed and render as such.

What is the current behavior?

mat-expansion-panel, when animating out, will open and display all hidden panel content, while still showing the current state as closed.

What are the steps to reproduce?

https://stackblitz.com/edit/angular-ykxmcr?file=src%2Fapp%2Fhero-list-multistep.component.ts

What is the use-case or motivation for changing an existing behavior?

Keeping the panel closed when animating out provides a much cleaner experience for the UI by preventing jumpiness and inconsistencies.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

@angular/core 6.0.4 @angular/material 6.2.1 @angular/cdk 6.2.1 Chrome 67.0.3396.79 Firefox 60.0.2 Safari 11.1

Is there anything else we should know?

This happens with all animations I have tested, not just the one provided in the stackblitz example.

aeslinger0 commented 5 years ago

Possibly related... I'm seeing something similar where the expansion panel is visible in a dialog as the dialog animates open. So it may appear that the animation does not have to applied directly to the expansion panel to see this behavior.

kiwikern commented 5 years ago

I have created an example for a router transition which illustrates the same problem:

https://stackblitz.com/edit/angular-expansion-panel-with-route-transition-animation?file=app%2Fapp%2Fapp.component.html

checkin-eurowings

thw0rted commented 5 years ago

Any updates on this? I thought maybe I had to specify something in my transition animations to cause everything to trigger in the right order, but if that's the case, I can't figure out what. I'd like to see an example of transitions that don't cause this behavior, if anybody has it working.

virtuoushub commented 5 years ago

@aeslinger0

Possibly related... I'm seeing something similar where the expansion panel is visible in a dialog as the dialog animates open. So it may appear that the animation does not have to applied directly to the expansion panel to see this behavior.

See #13870. I am not sure they are related either, but what you describe seems similar to that issue.

thw0rted commented 5 years ago

That sounds likely, yeah. For now I have a workaround in my global styles. I'll keep an eye out for the linked patch landing and see if that fixes things.

AshurovRustam commented 5 years ago

That sounds likely, yeah. For now I have a workaround in my global styles. I'll keep an eye out for the linked patch landing and see if that fixes things.

Can you please tell how you fixed it? I partially fixed it in my code by to putting the content of expansion-panel to ng-template and fortunately it helped with collapsing it during transition animation, but instead that behavior, now initial size of expansion-panel is smaller then it should be (i believe because it contains totally nothing), but after animation it became normal.

thw0rted commented 5 years ago

I happen to only have this problem when a custom component, that contains a mat-card with a few mat-expansion-panels on it, slides into view with an enter-animation. So, I made these rules:

.ng-animating mat-card mat-expansion-panel-header {
    height: 48px;
}
.ng-animating mat-card div.mat-expansion-panel-content {
    height: 0px;
    visibility: hidden;
}

The expansion panel header, in its closed state, is normally 48px. If you don't force it to this height during the animation, but try to remove the expansion panel content DIV by settings its height to 0, it renders at the height of its content instead (the panel-title element, probably) then jumps to the 48px height after the animation completes.

Hope this helps!

AshurovRustam commented 5 years ago

.ng-animating mat-card mat-expansion-panel-header { height: 48px; } .ng-animating mat-card div.mat-expansion-panel-content { height: 0px; visibility: hidden; }

With little changes it works, thanks) And you are right, I have the same situation, with a little bit different HTML structure: custom component with expansion-panel inside and during enter-animation it is opened by default. If I put content of each panel in ng-content (to make it lazy-loaded) then content become invisible but height of the panel become too small. Only sad thing in my case, to make it work it requires to put these styles rules in the the global styles.css. When I put them in the specific component style sheet, the first rule (for header) applies perfectly but the second (for content) just doesn't work. Also I found that all these behavior worked fine even without hacks before I updated ng-material and angular libraries from version 6 to 7.

thw0rted commented 5 years ago

I think for your second rule you'd need a piercing selector, e.g. .ng-animating mat-card ::ng-deep div.mat-expansion-panel-content -- the mat-expansion-panel-content is part of the mat-expansion-panel component so you can't style it without the ::ng-deep. I find it easier to make the fix globally.

AshurovRustam commented 5 years ago

Piercing selector helped a lot:

::ng-deep .ng-animating div mat-accordion mat-expansion-panel mat-expansion-panel-header {
    height: 48px;
  }
::ng-deep .ng-animating div mat-accordion mat-expansion-panel div.mat-expansion-panel-content {
    height: 0px;
    visibility: hidden;
}

To say the truth, I totally forgot that styles are encapsulated by default in components not only from child to parent but also in opposite way. Thanks)

tfalvo commented 5 years ago

Hi every body, have you got some news about this issue ? Do you know if this fix will be integrated soon ? Thanks a lot!

stefanmatar commented 5 years ago

I solved it temporarily until a fix arrives via a global style in style.scss:

// temporary fix for expansion panels until Angular Material Issue is fixed,
// see https://github.com/angular/material2/issues/13870
mat-accordion mat-expansion-panel {
  mat-expansion-panel-header {
    height: 40px; // height may be different for you
  }
  .mat-expansion-panel-content {
    height: 0;
  }
  &.mat-expanded {
    mat-expansion-panel-header {
      height: 64px; // height may be different for you
    }
    .mat-expansion-panel-content {
      height: auto;
    }
  }
}
simeyla commented 5 years ago

So are Angular animations fundamentally broken if we get issues like this?

To visualize at least what's going on I found it helpful to add the following to global styles:

.ng-animating
{
    outline: 1px solid red;
}

During a simple slide-in animation, the view of my accordion is like this:

image

So the .ng-animating class is being inherited by the children of accordion and anything that is possible to be animatable is being highlighted as such. This happens even if my accordion is in a completely separate component of its own.

I've concluded the only way to get on with my day right now - is to just disable animations completely on the accordion.

<mat-accordion [@.disabled]="true">

None of the css solutions worked for me. The problem being they won't work for animating an accordion offscreen when you quite likely do want the panel to remain open in its current state. In addition if I click on an accordion that initially animates on screen (with a 30s animation) then I can't open any panels once the animation is complete. That seems like a much more serious issue, which I don't even understand.

In any case the problem seems to be that the .ng-animate class is being inherited by the accordion control.

If there's no way to prevent this then surely animations are broken, and the fix needs to be of broader scope than a nasty css hack?

am-awais commented 5 years ago

I solved it temporarily until a fix arrives via a global style in style.scss:

// temporary fix for expansion panels until Angular Material Issue is fixed,
// see https://github.com/angular/material2/issues/13870
mat-accordion mat-expansion-panel {
  mat-expansion-panel-header {
    height: 40px; // height may be different for you
  }
  .mat-expansion-panel-content {
    height: 0;
  }
  &.mat-expanded {
    mat-expansion-panel-header {
      height: 64px; // height may be different for you
    }
    .mat-expansion-panel-content {
      height: auto;
    }
  }
}

it solves the problem but it causes mat-panel content to jump for a while :(

alexsanqp commented 5 years ago

Я решил это временно, пока не пришло исправление с помощью глобального стиля в style.scss:

// temporary fix for expansion panels until Angular Material Issue is fixed,
// see https://github.com/angular/material2/issues/13870
mat-accordion mat-expansion-panel {
  mat-expansion-panel-header {
    height: 40px; // height may be different for you
  }
  .mat-expansion-panel-content {
    height: 0;
  }
  &.mat-expanded {
    mat-expansion-panel-header {
      height: 64px; // height may be different for you
    }
    .mat-expansion-panel-content {
      height: auto;
    }
  }
}

это решает проблему, но некоторое время заставляет содержимое панели мата прыгать :(

I think that if you set these options to mat-expansion-panel-header, the jump in the content of the mat panel will be fixed. Replace with your height. <mat-expansion-panel-header [collapsedHeight]="'66px'" [expandedHeight]="'66px'">

pburkindine commented 5 years ago

I wanted to add another repro that may be illustrative

https://stackblitz.com/edit/sidenav-accordion-expansion

Here an accordion is in a component which is factoried at runtime into a sidenav; you can see the accordion is expanded during sidenav enter.

thw0rted commented 5 years ago

I applied my fixes from upthread and it resolves your issue. In styles.scss, just add

mat-sidenav.ng-animating {
  mat-expansion-panel-header {
    height: 48px;
  }
  div.mat-expansion-panel-content {
    height: 0px;
    visibility: hidden;
  }
}

Of course this should work out of the box, but you can use it as an interim fix.

am-awais commented 5 years ago

Finally solved this by using these attributes <mat-expansion-panel-header expandedHeight="46px" collapsedHeight="46px"> your text </mat-expansion-panel-header>

you can change the height as per requirement

TY me LATER :) ;) ;p

chipicow commented 4 years ago

So are Angular animations fundamentally broken if we get issues like this?

To visualize at least what's going on I found it helpful to add the following to global styles:

.ng-animating
{
    outline: 1px solid red;
}

During a simple slide-in animation, the view of my accordion is like this:

image

So the .ng-animating class is being inherited by the children of accordion and anything that is possible to be animatable is being highlighted as such. This happens even if my accordion is in a completely separate component of its own.

I've concluded the only way to get on with my day right now - is to just disable animations completely on the accordion.

<mat-accordion [@.disabled]="true">

None of the css solutions worked for me. The problem being they won't work for animating an accordion offscreen when you quite likely do want the panel to remain open in its current state. In addition if I click on an accordion that initially animates on screen (with a 30s animation) then I can't open any panels once the animation is complete. That seems like a much more serious issue, which I don't even understand.

In any case the problem seems to be that the .ng-animate class is being inherited by the accordion control.

If there's no way to prevent this then surely animations are broken, and the fix needs to be of broader scope than a nasty css hack?

I was able to implement it with your approach of disabling animations instead this css's workarounds, but added a little twist , I instead setting the animations fully off I binded that field [@.disabled] with a variable from my component that would be true by default but on click that that opens my ng-template via a Mat-Dialog I change the variable value after a timeout fixing the visual bug of the animation and allowing animations for the mat-expansion-panel like so:

this.dialog.open(accountRef).afterClosed().subscribe(() => { this.disableAnimations = true; }); setTimeout(() => { this.disableAnimations = false; });

And on my html <mat-accordion [multi]="true" [@.disabled]="disableAnimations">

Just make sure to turn disableAnimations to true when this accordion leaves the screen so it fixes the next time the mat-expansion-panel shows on the screen, for me this accordion is inside a mat dialog so i achieved this with the afterClosed event

meblum commented 4 years ago

Having this issue when expansion panel is used in a mat-dialog. Expands for a second when dialog is opened.

Diralytic commented 4 years ago

any updates?

TheParad0X commented 4 years ago

The only workaround that worked for me is:

<mat-accordion [@.disabled]="true">

Thanks @chipicow

ALGDB commented 3 years ago

So are Angular animations fundamentally broken if we get issues like this? To visualize at least what's going on I found it helpful to add the following to global styles:

.ng-animating
{
    outline: 1px solid red;
}

During a simple slide-in animation, the view of my accordion is like this: image So the .ng-animating class is being inherited by the children of accordion and anything that is possible to be animatable is being highlighted as such. This happens even if my accordion is in a completely separate component of its own. I've concluded the only way to get on with my day right now - is to just disable animations completely on the accordion.

<mat-accordion [@.disabled]="true">

None of the css solutions worked for me. The problem being they won't work for animating an accordion offscreen when you quite likely do want the panel to remain open in its current state. In addition if I click on an accordion that initially animates on screen (with a 30s animation) then I can't open any panels once the animation is complete. That seems like a much more serious issue, which I don't even understand. In any case the problem seems to be that the .ng-animate class is being inherited by the accordion control. If there's no way to prevent this then surely animations are broken, and the fix needs to be of broader scope than a nasty css hack?

I was able to implement it with your approach of disabling animations instead this css's workarounds, but added a little twist , I instead setting the animations fully off I binded that field [@.disabled] with a variable from my component that would be true by default but on click that that opens my ng-template via a Mat-Dialog I change the variable value after a timeout fixing the visual bug of the animation and allowing animations for the mat-expansion-panel like so:

this.dialog.open(accountRef).afterClosed().subscribe(() => { this.disableAnimations = true; }); setTimeout(() => { this.disableAnimations = false; });

And on my html <mat-accordion [multi]="true" [@.disabled]="disableAnimations">

Just make sure to turn disableAnimations to true when this accordion leaves the screen so it fixes the next time the mat-expansion-panel shows on the screen, for me this accordion is inside a mat dialog so i achieved this with the afterClosed event

this worked for me, but I insted of the timeout I used

 ngAfterViewChecked(): void {
    this.disableAnimations = false;
  }
manuelfuchs commented 3 years ago

@ALGDB Angular will throw you the error NG0100: ExpressionChangedAfterItHasBeenCheckedError if you perform changes directly after a check.

image

A possible solution is to queue the operation to the next change detection check like this:

  ngAfterViewChecked(): void {
    setTimeout(() => {
      this.disableAnimations = false;
    }, 0)
  }

A more detailed explanation and different solutions can be found here in this video from the Angular docs.

kbsh commented 2 years ago

I solved only this

::ng-deep .ng-animating mat-accordion mat-expansion-panel div.mat-expansion-panel-content {
  height: 0px;
  visibility: hidden;
}
OlliHolli commented 2 years ago

I've found another workaround that might help a few people. See my comment here.

antoci-alin commented 1 year ago

I have also fixed using:

.ng-animating .mat-accordion .mat-expansion-panel .mat-expansion-panel-content {
  height: 0px;
  visibility: hidden;
}
Vestelth commented 11 months ago

similar to above, works for me:

.mat-expansion-panel .mat-expansion-panel-content {
    &.ng-animating {
       height: 0;
    }
}
Nividica commented 11 months ago

TL;DR

Changing

to

fixed my issues.


Also ran into this issue today, and stumbled into a solution that seems to fix my issues.

My Setup

Dependencies:

"@angular/core": "16.2.0",
"@angular/material": "16.2.4",

Two router animations

  1. Fade in the first page load.
  2. Slide/Swipe when navigating from any page to any other page.

    <mat-expansion-panel [expanded]="false" on the target page.

All components use OnPush change detection.

Issue

For both situations the expansion panel starts as expanded, the page fades/slides in, then a moment later the expansion panel closes. I also sometimes have to click the header twice to get the panel to expand.

Research

I tried some of the solutions given above, with [@.disabled]="true" being the simplest fix for me, but I was left unsatisfied as it all felt like work-arounds for whatever the root problems was, and I was hoping for a solution that only changed the animation sequence/config.

After spending some time debugging in and out of the animation code and the components, I came to the conclusion that Angular wasn't animating the components on-page until some time after the router animation had finished. In other words the animation triggers were not getting triggered until the page was done animating and what appeared to be a new change detection cycle had started.

This is obviously undesired, but even more concerning is this doesn't match my expectations, as I have an animateChild() query at the very end of my sequence that I expected to link up the child animations with the route animation.

transition('* => *', [
  // Prep for animation
  group([
    query(':enter', style(...), { optional: true }),
    query(':leave', style(...), { optional: true }),
  ]),
  // Slide in
  group([
    query(':enter', animate(...), { optional: true }),
    query(':leave', animate(...), { optional: true }),
  ]),
  // Animate children
  query(':enter', animateChild(), { optional: true }),
]);

I admit I know very little about Angular animations, this is the first project I have used them, so I thought if I moved the animateChild() higher up in the sequence it would do all the on-page animations first, then slide the page in. However, no matter where I put the animateChild() the results were the same. Even if it was the first element in the sequence, the animations on-page always ran some time after the route animation had finished.

Stumbling into a solution

What stood out to me at this point was the Child of animateChild(), I had assumed that it animated all descendants not just direct children. Maybe that was a bad assumption? I didn't find anything like animateChildRecursive() or animateDescendants().

So, is there a way I can target all descendants? A quick read of the query function, I spotted some potential candidates

`query(":animating")` : Query all currently animating elements.
`query("@*")` : Query all elements that contain an animation triggers.

I tried with query(":animating") and got the same results as before, so I tried query("@*") and unexpectedly everything worked!

Working Solution

In the end my animation sequence ended up being

transition('* => *', [
  // Prep for animation
  group([
    query(':enter', style(...), { optional: true }),
    query(':leave', style(...), { optional: true }),
  ]),
  // Slide in
  group([
    query(':enter', animate(...), { optional: true }),
    query(':leave', animate(...), { optional: true }),
  ]),
  // Animate :enter descendants
query(':enter', query('@*', animateChild(), { optional: true }), { optional: true }),
]);

I think query(':enter', query('@*' is targeting all descendants of the page entering and animating them.

I still had the .ng-animating red border @simeyla suggested from before, and with the fix, the expansion panel no longer has a red border while the page is sliding in.

Final Thoughts

While this did fix all the issues I was having, I still don't fully understand why it fixed them. It seems like Angular was not animating the full descendant tree with animateChild() and I don't know if that was my fault or just a bad expectation. I hope this helps someone else having similar issues, but I will say I don't know the full scope of the @* selector so it is entirely possible this will trigger animations deep in the page undesirably.

My remaining unanswered questions are

  1. Why query(':enter', animateChild() doesn't seem to make it all the way down to the elements on the page.
    • I assume this is a failure of my understanding of Angular animations, and I have a broken "chain" of animation links
  2. Why does placing query(':enter', query('@*' as the last step in the route animation fix the problem?
    • I would have expected that placing it at the end would yield the same results, but no, the expansion panel is collapsed before the route animation slides the page over.