angular / components

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

[Tabs] Automatically page scrolls to top when switching between some tabs #9592

Closed hoangthau closed 3 years ago

hoangthau commented 6 years ago

Bug, feature request, or proposal:

We found a bug. We had top content and a tab group below this content. And then we scrolled a little bit to bottom. We switched between some tabs. Now, page scrolled to top automatically.

It only occurs when we have top content on this page.

What is the expected behavior?

Page should keep scroll position when switching some tabs

What is the current behavior?

Page scrolls to top when switching some tabs

What are the steps to reproduce?

-Step 1: Scroll a little bit to bottom on page -Step 2: Click on Tab 3 The page will scroll to top.

Demo here: https://stackblitz.com/edit/angular-wjtrxy

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

A bug

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

Angular 5.0.3 - Material 5.0.2 - Window 10 - Typescript 2.4.2

Is there anything else we should know?

Thanks a lot!

DanielPinkpank commented 6 years ago

I have the same Issue. And I noticed that the height of the "top content" seems to have an influence on this problem. If you change the height to 900px for example, it doesn't scroll to top anymore.

praveenpuglia commented 6 years ago

Confirmed. Have the same issue.

anshumansworld commented 6 years ago

👍 same issue

umimehar commented 6 years ago

same issue

umimehar commented 6 years ago

Well the reason is when you change the tab, the next tab not have that much height of its inner contents so the parent height also decrease. thats why the its scroll to top. try the parent element height fixed like 1000px and then change tab, it will not scroll up. 👍

umimehar commented 6 years ago

https://stackblitz.com/edit/angular-wjtrxy-g4ksm6

Add a min-height="800px" to parent elemnt of mat-tab-group. :)

hoangthau commented 6 years ago

Thanks @umimehar. That's a good workaround.

TolgaBA commented 6 years ago

Seriously, why is there no clean solution to this existing problem starting from 2015!

luchoman08 commented 6 years ago
.fill-available {
  min-height: min-height: stretch;
}

With this class called in the main container of the page worked for me, only in chrome not in mozilla.

luchoman08 commented 6 years ago

Better -webkit-fill-available (tested on google chrome). Here is an example https://stackblitz.com/edit/angular-wjtrxy-x9shma

Lakston commented 6 years ago

None of the suggested solutions completely fix the problem for me, I still end up with some weird scrolling behaviors with a pixel min-height, stretch, fill-available...

edit: I modified @luchoman08 's stackblitz to reflect the problem: https://stackblitz.com/edit/angular-wjtrxy-utgzn1

I removed some content from tab1 so now if you go to tab4 for example, scroll down a bit, then click tab1, the scrolling bug is still there, switch between those 2 for extra sea sickness effects.

imransilvake commented 6 years ago

still no solution for this? page shouldn't go up when click on the tab :/

KlausHans commented 6 years ago

Still a problem with Angular 6.1 and Angular Material 6.4.

shifatul-i commented 6 years ago

min-height property don't work with MS Edge, still jumps to the top.

RobChangCA commented 5 years ago

Still a problem with angular 7.1.4 and material 7.2

aszorenyi commented 5 years ago

Same issue here.

GSchutz commented 5 years ago

another hack approach, is to set min-height and padding/margin bottom for short content and to keep a footer in place.

min-height: 450px;
padding-bottom: 200px;
margin-bottom: -200px;
ludarous commented 5 years ago

"min-height" solution did not work for me. It works, but i had to set min-height to meaningless value. I wrote directive that works for me. You may have to change document.documentElement to your scrolling element. It is not extra clean solution, but it works.

import {MatTabChangeEvent, MatTabGroup} from '@angular/material';

@Directive({
  selector: '[mat-tab-scroll-fix]'
})
export class MatTabScrollFixDirective implements AfterViewInit {

  constructor(private matTabGroup: MatTabGroup) {

  }

  private scrollPosition: number;
  private tabChanging: boolean;

  ngAfterViewInit(): void {
    const scrollHandler = (event) => {
      if (this.tabChanging) {
        document.documentElement.scrollTop = this.scrollPosition;
      }
      this.scrollPosition = document.documentElement.scrollTop;
    };

    window.addEventListener('scroll', scrollHandler);

    this.matTabGroup.selectedTabChange.subscribe((tabChangeEvent: MatTabChangeEvent) => {
      this.tabChanging = false;
      document.documentElement.scrollTop = this.scrollPosition;
    });

    this.matTabGroup.selectedIndexChange.subscribe((index: number) => {
      this.tabChanging = true;
    });
  }
}
runningdavid commented 5 years ago

Found same issue.

BobaFett58 commented 5 years ago

Still have this problem. Angular 8

lxREAPERxl commented 5 years ago

I might have a solution for this. Its a crude solution, but is seems to be working, at least for me it does :) So let me explain:

You need a parent component, which has the entire mat-tab-group code, and child components, for each individual mat-tab body (I suppose you can do without parent/child as well).

Parent component HTML:

<mat-tab-group (selectedTabChange)="forceScrollPosition($event)">
  <mat-tab>
    <app-tab-child-0
    [scrollPosition]="scrollChild0" 
    [canUpdateScroll]="updateChild0Scroll"
    (scrollChanged)="scrollChanged($event)">
    </app-tab-child-0>
  </mat-tab>
  <mat-tab>
    <app-tab-child-1 
    [scrollPosition]="scrollChild1" 
    [canUpdateScroll]="updateChild1Scroll"
    (scrollChanged)="scrollChanged($event)">
    </app-tab-child-1>
  </mat-tab>
</mat-tab-group>

Parent component TS:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-tab-parent',
  templateUrl: './app-tab-parent.html',
  styleUrls: ['./app-tab-parent.scss']
})
export class ParentComponent implements OnInit {

  // Hold scroll positions for each child (can be also array of values, it doesnt matter)
  scrollChild0: number = 0;
  scrollChild1: number = 0;

  // Permission for child component to emit scroll change
  updateChild0Scroll: boolean = true; //The FIRST tab displayed should have this attribute set to ,,true"
  updateChild1Scroll: boolean = false;

  // There are several ways to trigger method in child, I prefer this one
  @ViewChild(TabChild0Component) private tabChild0Component: TabChild0Component;
  @ViewChild(TabChild1Component) private tabChild0Component: TabChild1Component;

  //Here set new scroll position for specific child tab
  public scrollChanged(event: any) {
      //example
      if(event.index === 0) {
          this.scrollChild0 = event.value;
      }
      else if ...... {
          ....
      }
  }

  // When selected tab is changed, revalidate permissions and force child to load latest scroll
  public forceScrollPosition(event: any) {
      //example
      this.updateChild0Scroll= event.index === 0;
      this.updateChild1Scroll= event.index === 1;
      ....
      // now you need to identify selected tab and force child component to load last saved scroll
      if(event.index === 0) {
          this.tabChild0Component.loadScroll();
      }
      else if ...... {
          ....
      }
  }
}

Child component TS:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-tab-child-0',
  templateUrl: './app-tab-child-0.html',
  styleUrls: ['./app-tab-child-0.scss']
})
export class TabChild0Component implements OnInit {
  @Input() scrollPosition: number;
  @Input() canUpdateScroll: boolean;

  @Output() scrollChanged = new EventEmitter();

  // This path targets mat tab body. the ,,#mat-tab-content-0-0" needs to be changes, depending on 
  // child index position in the tabs (#mat-tab-content-0-0, #mat-tab-content-0-1, ...)
  private readonly elementPath = "#mat-tab-content-0-0 .mat-tab-body-content.ng-trigger.ng-trigger-translateTab";

  ngOnInit() {
     let element = document.querySelector(this.elementPath);
      let base = this;

      if(element) {
        element.addEventListener('scroll', function() {
          // Check if this child is currently focused tab, otherwise it emits 0 by default and defeats the whole 
          // purpose
          if(base.canUpdateScroll) {
            base.scrollChanged.emit(document.querySelector(base.elementPath).scrollTop);
          }
        });
      }
  }

   /** Load last known scroll position */
  public loadScroll() {
     let element = document.querySelector(this.elementPath);
     if(element) {
       document.querySelector(this.elementPath).scrollTop = this.scrollPosition;
     }
  }    
}

Again, sorry for the crude code, but I hope it can give you at least some basic idea of my solution. If it will be helpful to anyone, well, I am glad I could help :)

VeeRHunter commented 5 years ago

https://stackblitz.com/edit/angular-wjtrxy-g4ksm6

Add a min-height="800px" to parent elemnt of mat-tab-group. :)

Thank you, I solved scroll to top of page issue using this code 👍

perotom commented 5 years ago

Especially bad with variable tab content as min-height and padding hacks are not working.

noamichael commented 5 years ago

It would be great to get an update on this.

skaiser commented 5 years ago

I was able to use min-height: 100% on the parent with no other styling.

FTello31 commented 5 years ago

Still have this problem. Angular material 8.2.1

vsarunov commented 4 years ago

Still a problem in 2020.

IgorKurkov commented 4 years ago

less min-height: 500px or 400px works too...

jneuhaus20 commented 4 years ago

This is super-frustrating, and min-height is absolutely a hack (more power to you if it works for your situation though.)

min-height does exactly what you expect here, which is a problem for both static and dynamic tab contents. Say I have three tabs, A, B, and C, with "natural" (fits content, looks good, i.e. what the dev wants) heights of 600px, 800px, and 200px, respectively. Say I also have content above the tab group such that there's about 220px left "before the fold" (bottom of viewport) on a desktop browser. A min-height: 200px will respect the min height of my component, but switching between tabs A and B while viewing them in full will always bounce to the top. Increasing min-height any more will start adding extra whitespace to tab C, which could be perfectly acceptable, or could look like trash.

No one adequately explained what's going on, in my opinion, so here's an illustrative demo: https://stackblitz.com/edit/angular-material-bouncing-tabs?file=app%2Fbouncing-tabs-demo.html

There's an object with property, matTabsAnimations.translateTab, that forces a min height of 1px for some issues. If that instead chose the height of the previous tab, I think that would solve things. That would eliminate this hidden third size that causes the jump.

For a slightly better hack, you can target .mat-tab-body-content.ng-trigger-translateTab.ng-animate-queued instead of the containing element. You still have to pick a third size that will potentially cause unexpected jumps, but this doesn't impact the steady-state layout at least. If you give it a fat red border, slow the animation way down, and throttle the CPU down (via the Chrome debugger,) you still won't see it unless you're on that breakpoint.

qortex commented 4 years ago

Still an issue with Angular 9 & Angular Material 9.1.0. Seriously.

Prateema1 commented 4 years ago

Scrolls to top after route change. 1) Have a list of brands. 2)Scroll to the middle . Select any brand. Navigates to another route with its description. 3)Click on Back Button. 4)Redirects to the list but scrolls to top.

Still a problem in Angular 7, Material.

Required behaviour: Restore original scroll position.

Any solution?

britvik commented 4 years ago

Made a small directive that retains the scroll position when you change tabs.

import { Directive, ElementRef, OnDestroy } from '@angular/core'
import { fromEvent, Subscription } from 'rxjs'

@Directive({
  selector: '[xxxScrollRetainer]'
})
export class ScrollRetainerDirective implements OnDestroy {
  private changes: MutationObserver
  private lastScrollPosition = 0
  private subscription: Subscription

  constructor(private elementRef: ElementRef) {
    this.changes = new MutationObserver(() => this.rollbackScrollPosition())
    this.changes.observe(this.elementRef.nativeElement, { childList: true, subtree: true })
    this.subscription = fromEvent(window, 'scroll').subscribe(() => {
      this.lastScrollPosition = window.pageYOffset
    })
  }

  private rollbackScrollPosition() {
    window.scrollTo(window.pageXOffset, this.lastScrollPosition)
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe()
    this.changes.disconnect()
  }

}
leojkwan commented 4 years ago

The issue is unrelated to UI tab modules, I can confirmed because the same scrolling happened with ngbTab. The issue, for me at least, is storing tab state in the query params.

You can implement custom scroll restoration behavior by adapting the enabled behavior as in the following example. https://angular.io/api/router/ExtraOptions#scrollPositionRestoration

Setting scrollPositionRestoration: 'enabled', solves the issue, but screws up expected behavior in my app everywhere else. Use the custom check option the link above mentions to hack your edge case together.

For context, the min-height does not solve it for me on Angular 9.

pookdeveloper commented 4 years ago

The same issue :(

pookdeveloper commented 4 years ago

The min height is depend the size of the window no always is needed 800px o 500px etc.. I do that: image Then: image

jneuhaus20 commented 4 years ago

The problem is the little placeholder element before sliding in the next tab. It has a min-height of 1px, and even though it's in and out (and the next tab in the DOM to the side) before you can see it, the scroll change still happens.

If it instead latched the height of the outgoing tab, behavior would be as most people expect.

pookdeveloper commented 4 years ago

@jneuhaus20 I dosen't see that, can you send a screenshot ?

olatignies commented 4 years ago

@britvik works for me. Thanks.

DavidTheProgrammer commented 4 years ago

@britvik Which component do you attach your directive to? I attached it to mat-tab-group that didn't work. Infact, the page didn't load.

britvik commented 4 years ago

@DavidTheProgrammer You can attach it to any element (e.g. div) that contains the tab group.

asyahril commented 4 years ago

why is there no definite solution? This issue started from 2 years ago, not closed until now

etay2000 commented 4 years ago

@asyahril Hey c'mon get off their back, this is open source. What do you expect from a small company with limited resources like Google? I'm just happy the issue hasn't been automatically locked yet.

Brooksie82 commented 4 years ago

I'm also having this issue. I've tried @britvik's Directive (doesn't fix for me... fromEvent doesn't appear to fire), I've also tried min-height on containing div. Works in Chrome & Edge but not Firefox.

Drillman commented 4 years ago

I've made this directive based on @ludarous answer, to make it work with Angular CDK scrolling. It work on my side, but not the smoothest option.

import { AfterViewInit, Directive, OnInit, OnDestroy } from '@angular/core';
import { MatTabGroup, MatTabChangeEvent } from '@angular/material/tabs';
import { ScrollDispatcher, CdkScrollable } from '@angular/cdk/scrolling';
import { Subscription } from 'rxjs';

@Directive({
  selector: '[ScrollRetainer]'
})
export class ScrollRetainerDirective implements AfterViewInit, OnInit, OnDestroy {

  constructor(private matTabGroup: MatTabGroup, private scrollDispatcher: ScrollDispatcher) {}

  private container: CdkScrollable;
  private scrollSub: Subscription;
  private tabSub: Subscription;

  ngOnInit(): void {
    this.scrollSub = this.scrollDispatcher.scrolled().subscribe(
      (el: CdkScrollable) => {
        this.container = el;
      });
  }

  ngAfterViewInit(): void {
    this.tabSub = this.matTabGroup.selectedTabChange.subscribe((tabChangeEvent: MatTabChangeEvent) => {
      this.container.scrollTo({
        top: this.container.measureScrollOffset('top'),
        left: 0,
        behavior: 'auto'
      });
    });
  }

  ngOnDestroy(): void {
    this.scrollSub.unsubscribe();
    this.tabSub.unsubscribe();
  }
}
owen26 commented 4 years ago

after tweak the min-height solution a few times, I realised the number is not totally random

if you set min-height to any number, it will prevent any subsequent tab clicks but not the first one after page load/refresh, in fact it'll jump to window top and then scroll to tab top, that's even worse.

if you set min-height to a number larger than the distance from top of the page to top of the nav tab, then it will prevent jumping for all tab clicks including the first click, that's why in a few previous examples in this thread, setting a min-height: 800px fixes it. But it doesn't have to be 800, it just needs to be larger than the distance mentioned above.

So if you have a project with predicted layout, setting min-height might be the quickest hacky way to fix this.

gokberknur commented 4 years ago

This issue happening on my angular 9 application, but in different scenario.

I have modal pop-up, when i close the modal, page automatically scrolls up in internet explorer.

previously with angular 8 and material 7.3.3 I was able to fix this issue by adding window.scroll() event after modal event.

but after updating angular 9 and material 9. window.scroll() is not working anymore.

I removed window.scroll() event and in chrome and other browsers scroll behaviour working as I expected.. but not in internet explorer, so I am not sure if it is related to window.scroll() compability with IE or is material breaking something.

anyone figured out how to solve this issue?

Omzig commented 4 years ago

Still happens in 9.1.12! Please fix this. It was driving me nuts till I used the work around, but work arounds are difficult to remember. my html now has bug comments in it. ugg

PTC-JoshuaMatthews commented 3 years ago

And another month goes by... if only google had developers working in house. They do? You don't say...

saithis commented 3 years ago

We had the same problem and fixed it with this workaround:

@Directive({
    // tslint:disable-next-line: directive-selector
    selector: 'mat-tab-group[scrollFix]'
})
export class PsMatTabGroupScrollFixDirective implements AfterViewInit, OnDestroy {

    private matTabGroupEl: HTMLElement = this.el.nativeElement;
    private animationSub = Subscription.EMPTY;

    constructor(private matTabGroup: MatTabGroup, private el: ElementRef) {

    }

    public ngAfterViewInit(): void {
        const orig = this.matTabGroup._handleClick.bind(this.matTabGroup);
        this.matTabGroup._handleClick = (tab, tabHeader, index) => {
            if (!tab.disabled) {
                this.matTabGroupEl.style.minHeight = this.matTabGroupEl.clientHeight + 'px';
            }

            return orig(tab, tabHeader, index);
        }
        this.animationSub = this.matTabGroup.animationDone.subscribe(() => {
            this.matTabGroupEl.style.minHeight = 'unset';
        });
    }

    public ngOnDestroy() {
        this.animationSub.unsubscribe();
    }
}

Unfortunatelly it overrides the _handleClick method, which is not in the public api. So it can break at any update, but at least it works for now.

etay2000 commented 3 years ago

Valid arguments, but let's give the Angular devs some more time and not over-react. Understand this is open source. Let's try to view things from their point of view. Explore other options in the meantime.