angular / components

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

Feature: Sidenav or Route Scroll Service #4280

Open mtpultz opened 7 years ago

mtpultz commented 7 years ago

Bug, feature request, or proposal:

Feature Request

What is the expected behavior?

Using a common native API call to window.scrollTo(0, 0) or doing something similar on the document body, etc would allow for scrolling the page to the top after a route ends when using a sidenav, otherwise the next view starts at the same position in the page.

What is the current behavior?

Routing between views within the sidenav content maintains the same scroll position assuming the content is the same length.

What are the steps to reproduce?

Add a sidenav and try to scroll to the top of the page anywhere at anytime. Eventually, I figure out this solution and stuck it in my app.component.

this.router.events
  .filter(event => event instanceof NavigationEnd)
  .subscribe(() => {
        const contentContainer = document.querySelector('.mat-sidenav-content');
        if (contentContainer) {
          document.querySelector('.mat-sidenav-content').scroll({ top: 0, left: 0, behavior: 'smooth' });
        } else {
          this.window.scroll({ top: 0, left: 0, behavior: 'smooth' });
        }
  });

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

Routing between views within the sidenav should be able to start at the top of the page, which is a common use case. Also, scrolling a browser using the native API has been around forever so it's a bit confusing when you expect it or one of the many variations of scrolling to work and they don't.

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

Angular 4.0.2 Angular Material 2.0.0-beta.3

elishaterada commented 7 years ago

Building on @mtpultz answer, you can keep it little simpler if your page is always going to have md sidenav, and without relying on third party library which I assume is what .scroll() is about.

this.router.events
  .filter(event => event instanceof NavigationEnd)
  .subscribe(() => {
    document.querySelector('.mat-sidenav-content').scrollTop = 0;
  });
ravishivt commented 6 years ago

I'm using the following in app.component.ts which preserves the scroll position when navigating back or forward. It is based off https://stackoverflow.com/a/44372167/684893

  private lastPoppedUrl: string | undefined;
  private yScrollStack: number[] = [];
  private baseScrollOptions: ScrollToOptions = {
    left: 0,
    top: 0,
    behavior: 'smooth',
  };

  constructor(private router: Router, private location: Location) {}

  ngOnInit() {
    this.location.subscribe((ev: PopStateEvent) => {
      this.lastPoppedUrl = ev.url;
    });
    this.router.events.subscribe((ev: any) => {
      const sidenavContentElement = document.querySelector(
        '.mat-sidenav-content',
      );
      if (!sidenavContentElement) {
        return;
      }
      if (ev instanceof NavigationStart) {
        if (ev.url !== this.lastPoppedUrl) {
          this.yScrollStack.push(sidenavContentElement.scrollTop);
        }
      } else if (ev instanceof NavigationEnd) {
        if (ev.url === this.lastPoppedUrl) {
          this.lastPoppedUrl = undefined;
          sidenavContentElement.scroll({
            ...this.baseScrollOptions,
            top: this.yScrollStack.pop(),
          });
        } else {
          sidenavContentElement.scroll(this.baseScrollOptions);
        }
      }
    });
  }
damienwebdev commented 6 years ago

Just chiming in here... this is fixed by https://github.com/angular/angular/pull/20030 and is being released in @angular/router@6.1.0

@jelbourn @andrewseguin @crisbeto this can be closed.

Splaktar commented 5 years ago

@damienwebdev I'm not sure that this can be closed. Based on https://github.com/angular/angular/issues/7791#issuecomment-428259641, there appear to be some incompatibilities or lack of documentation for how scrollPositionRestoration should work with a mat-sidenav-container.

Splaktar commented 5 years ago

Some of the issues related to Angular Material and the SideNav also effect Ionic and other component sets. They are discussed in https://github.com/angular/angular/issues/24547.

odahcam commented 5 years ago

Disclaimer: I've post these somewhere else before, but as @Splaktar requested, here are some demos of the routing scrolling issue that I'm experiencing.

Here are two examples, the first working and the second not:

Why that happens? Because when mat-sidenav-container is used with its fullscreen attribute, it gets its own scrolling container instead of relying on the default viewport overflow behavior.

A curious fact is that the router scrolling seems to work when you play with the browser history, but not when you click the links.

ashnewport commented 5 years ago

I'm experiencing this issue on a new project using "@angular/core": "~7.2.4" and "@angular/material": "^7.3.2" with MatSidenavModule. I agree with the others that not having the scroll position behave as expected in a standard browser app/site (i.e. non single page app) is poor for usability and accessibility, and does not meet user expectations.

I'd like to emphasise that accessibility is a large component for my work and user base.

This issue, in my experience, has additionally made it more difficult than needed to recommend to stakeholder that an Angular/Material combo is a stable choice for a platform.

I've implemented a couple of suggestions from other comments here, linked issues, stackoverflow, and my own code but they all have some minor issue which doesn't make navigation and scroll position feel natural.

I see that in viewport_scroller.ts there is a class BrowserViewportScroller with a constructor constructor(private document: any, private window: any) {}.

Looking for a solution here, could Material provide a way to specify what elements the BrowserViewportScroller should accept as the main content scroll regions and inject that into the Angular app at load time?

That way the app would know which DOM element to treat as the main view region and listen to scroll events within that area instead of the default document or window which may not always be scrollable due to the implementation. Granted there can also be multiple side nav components so perhaps a .forRoot() equivalent might help here.

Maybe there is already a way of providing an injection like this into Angular but I'm not familiar with how to do that or if that functionality exists. Either way, having this as part of Material might help to solve the issue or bring it closer to a solution.

BenjaminHofstetter commented 5 years ago

I have an ugly solution for this. I think it's a hack.

 <mat-sidenav-content  #benji>
...
    <router-outlet (activate)="onActivate($event)"></router-outlet>
...
  </mat-sidenav-content>

then the onActivate($event) funciton scrolls to the top

  @ViewChild('benji', {static: false}) el !: MatSidenavContent;

onActivate(e: any) {
    if(this.el && this.el.getElementRef().nativeElement) {
      this.el.getElementRef().nativeElement.scrollTop = 0;
    }
  }

I think it's ugly but it seems to work.

odahcam commented 5 years ago

I miss the browser's native scrolling. :disappointed:

DibyodyutiMondal commented 5 years ago

Any updates on this?

juan2manny commented 5 years ago

Kind of like @BenjaminHofstetter solution above, I found that if I use an obscure html element (in my case <cite> and set it directly above the router-outlet, put it's styles to a negative top margin and position absolute, I was able to use

onActivate(e) { if (isPlatformBrowser) { document.querySelector('cite').scrollIntoView() } }

The negative margin makes the browser attempt to scroll past 0, but since it can't the mat-sidenav-container content stops at the top.

quemao18 commented 4 years ago

this.router.events .filter(event => event instanceof NavigationEnd) .subscribe(() => { document.querySelector('.mat-sidenav-content').scrollTop = 0; });

Hi, I worked without the filter; this.router.events .subscribe(() => { document.querySelector('.mat-sidenav-content').scrollTop = 0; });

DibyodyutiMondal commented 4 years ago

I believe the folks who wrote the angular documentation have a workaround. Although it is rather overkill... But it really shows the principle of approaching this problem - even when anchor tags and hashes are involved.

https://github.com/angular/angular/tree/master/aio/src/app

The files to look for are

  1. app.component.ts
  2. app.component.html
  3. doc-viewer.component.ts
  4. scroll-service.ts
  5. scroll-spy-service.ts
  6. location-service.ts

It's going to be tough trying to understand exactly what they have going on in there, and this won't even give you a complete idea. BUT, you will gain many pointers of how to "coax" angular into doing what you want to do...

SvenBudak commented 2 years ago

fyi: Anchor Scrolling is also not working if you use fullscreen on <mat-sidenav-content> : https://lachimi.com/angular/routing-navigation/extra-options/anchor-scrolling

it should be simply possible to define in the routerExtraOptions the scrollable element.