angular / components

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

Sidenav: content area size not calculated properly with animation #9837

Open KlausHans opened 6 years ago

KlausHans commented 6 years ago

Bug, feature request, or proposal:

Bug

What is the expected behavior?

The size of the content area is calculated correctly according to the width of the sidenav menu.

What is the current behavior?

The calculation is wrong. The margin-left value of the content area is inverted to the width defined in the animation.

What are the steps to reproduce?

https://stackblitz.com/edit/angular-etpys5 (to see the issue you have to click a few times on the expand button)

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

@angular/material@5.2.0 @angular/cdk@5.2.0 @angular/core@5.2.4 @angular/animations@5.2.4

mmalerba commented 6 years ago

It looks like a change detection issue. It's not exactly ideal, but you can use the start and done handlers on the animation to start and stop a requestAnimationFrame ticker that will trigger the appropriate change detection: https://stackblitz.com/edit/angular-o5fjz1?file=app/app.component.ts

@crisbeto Any way you can think of that we can do this for people?

crisbeto commented 6 years ago

I'm not sure whether there's an efficient way to do it, apart from using a ResizeObserver which has pretty bad browser support. For these kinds of cases it might be better to make _updateContentMargins a part of the public API so people can trigger a recalculation manually.

KlausHans commented 5 years ago

With the latest Angular (Material) version the behavior changed slightly. But its still broken. https://stackblitz.com/edit/angular-kzia2e @angular/animations7.2.9 @angular/cdk7.3.5 @angular/common7.2.9 @angular/compiler7.2.9 @angular/core7.2.9 @angular/material7.3.5

robert-king commented 4 years ago

I'm facing a similar issue - I adjust the width of the side-nav from 200 to 79 pixels:

sidenav component sets its width internally with hostbinding:

@HostBinding('style.width') width = '200px';

  toggleCollapse() {
    this.collapse = !this.collapse;
    this.width = this.collapse ? '79px' : '200px'
  }

but mat-sidenav-content margin-left stays at 200px when the side-nav changes to 79px. It makes collapsing the side-nav redundant.

<mat-sidenav-container>
  <mat-sidenav
    #sidenav
    [autoSize]="true" <--- INVALID, NOT AN OPTION FOR MAT-SIDENAV
    [mode]="(isBarkMobile$ | async) ? 'over': 'side'"
    [opened]="sideNavOpen"
    [disableClose]="true"
    [fixedInViewport]="(isBarkMobile$ | async)"
    [fixedTopGap]="0"
    [fixedBottomGap]="0">
    <shared-sidenav (closeSideNav)="closeSideNavIfMobile()"> <-- WIDTH ADJUSTS 200 -> 79!!
    </shared-sidenav>
  </mat-sidenav>

  <mat-sidenav-content> <-- MARGIN-RIGHT STAYS AT 200PX and doesnt go to 79px
    <router-outlet></router-outlet>
    <div style="display: none">
      <!-- preload these icons -->
      <mat-icon svgIcon="close-2"></mat-icon>
      <mat-icon svgIcon="offline"></mat-icon>
    </div>
  </mat-sidenav-content>
</mat-sidenav-container>
robert-king commented 4 years ago

Here's my temporary fix:

toggleCollapse() {
    this.collapse = !this.collapse;
    this.width = this.collapse ? '79px' : '200px'
    document.querySelector('.mat-drawer-content.mat-sidenav-content').style.marginLeft = this.width;
  }
KlausHans commented 4 years ago

Still broken. https://stackblitz.com/edit/angular-rf4mmn

@angular/animations9.1.0
@angular/cdk9.2.0
@angular/common9.1.0
@angular/compiler9.1.0
@angular/core9.1.0
@angular/forms9.1.0
@angular/material9.2.0
@angular/platform-browser9.1.0
@angular/platform-browser-dynamic9.1.0
@angular/router9.1.0
meblum commented 4 years ago

Any updates on this?

empinator commented 3 years ago

this workaround with setTimeout worked for me

export class AppComponent implements  AfterViewInit {

  @ViewChild('sidenav') 
  private sidenav: MdSidenav;

  ngAfterViewInit(): void {
    setTimeout(() => {
      this.sidenav.open();
    }, 250);
  }
}

Source: https://github.com/angular/components/issues/6743#issuecomment-327416089

andrewalderson commented 2 years ago

I have been having these same issues and I have not been able to find a way to animate the width change of the drawer with the current functionality of these components. This is just one thing that I don't think was taken into consideration when the autosize functionality was added. It works great as long as the width of the drawer is not animated.

The main problem I have encountered is the 'mat-drawer-transition' css class. It doesn't get added until the drawer is toggled for the first time so the content container won't animate unless the drawer has been toggled and once added it causes issues with the animations if you animate the width of the drawer. The original issue reported here is caused by this.

My temporary solution to this is to add an appropriately named directive to the MatSidenav or MatDrawer component.

@Directive({
  selector: "[appDrawerAutosizeHack]",
})
export class DrawerAutosizeHackDirective implements OnInit, OnDestroy {
  constructor(
    @Optional() @Self() @Host() drawer: MatDrawer,
    @Optional() @Self() @Host() sidenav: MatSidenav,
    @Optional() drawerContainer: MatDrawerContainer,
    @Optional() sidenavContainer: MatSidenavContainer,
    @Optional() @Inject(DOCUMENT) private _doc: Document,
    private _el: ElementRef<HTMLElement>
  ) {
    this._drawer = drawer ?? sidenav;
    this._container = drawerContainer ?? sidenavContainer;
  }

  private _drawer?: MatDrawer | MatSidenav;
  private _container?: MatDrawerContainer | MatSidenavContainer;

  private _destroyed = new Subject<void>();

  private resizeObserver?: ResizeObserver;

  ngOnInit(): void {
    this._drawer?.openedChange
      .pipe(takeUntil(this._destroyed))
      .subscribe(() => {
        this._doc
          .querySelector(".mat-drawer-transition")
          ?.classList.remove("mat-drawer-transition");
      });

    this.resizeObserver = new ResizeObserver((entries) => {
      this._container?.updateContentMargins();
    });
    this.resizeObserver.observe(this._el.nativeElement);
  }

  ngOnDestroy(): void {
    this._destroyed.next();
    this._destroyed.complete();

    this.resizeObserver?.unobserve(this._el.nativeElement);
  }
}

This just simply removes the 'mat-drawer-transition' class and manually updates the content margins of the container. This is not ideal because it relies on implementation details (the name of the css class and the fact that it is added each time the animation starts) but it does keep this hack isolated. Doing this also means that the autosize property on the container is superfluous.

The easiest fix for this is to change the MatDrawerContainer class to add the 'mat-drawer-transition' class when the drawer animation starts and remove it when it ends. This would allow developers to handle their own functionality if they want to transition the drawer size.