components icon indicating copy to clipboard operation
components copied to clipboard

[Stepper] Vertical should scroll to top when changing steps

Open tiswrt opened this issue 6 years ago • 29 comments

Bug, feature request, or proposal:

vertical-stepper should scroll to the beginning of the selected step when changing step

What are the steps to reproduce?

https://stackblitz.com/edit/angular-material2-issue-rerdhn

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

material 5.0

tiswrt avatar Dec 08 '17 14:12 tiswrt

We desperately need this as the stepper is virtually unusable on mobile devices.

demisx avatar Dec 13 '17 20:12 demisx

Would be a very helpful feature!

sitewerk avatar Dec 14 '17 10:12 sitewerk

A simple work around is a directive.

Then in the next or back ... <button mat-button matStepperPrevious scrollToTop>Back

import { Directive, Output, HostListener } from '@angular/core'; import { EventEmitter } from 'events'; /**

  • This directive scrolls to the class .main-content-wrap (top of the page)

*/

@Directive({ selector: '[scrollToTop]' }) export class ScrollToTopDirective {

@Output() myClick: EventEmitter = new EventEmitter() @HostListener('click', ['$event'])

// constructor(){}

onClick(e) { // console.log('scrolltotop') const container = document.querySelector('.main-content-wrap'); container.scrollTop = 0; } }

jberall avatar Mar 12 '18 03:03 jberall

I second the need for the scroll to top. In the meantime I added a class to the container for mat-vertical-stepper and used on StepperSelectionChange and called my function.
private scrollToSectionHook() { const element = document.querySelector('.stepperTop'); console.log(element); if (element) { setTimeout(() => { element.scrollIntoView({behavior: 'smooth', block: 'start', inline: 'nearest'}); console.log('scrollIntoView'); }, 500 ); } }

AlbertoBonfiglio avatar Mar 19 '18 23:03 AlbertoBonfiglio

hi! I am having exactly the same issue mentioned earlier, but I have tried those solutions and they didn't work for me, every time I change to another step, the page is scrolled way down. Do you have any other solution for this problem?

daniortiz-bj avatar Mar 22 '18 00:03 daniortiz-bj

Hi Daniela, I used the stepperselectionchange event and scroll to the top. Not perfect but works for my project

public onStepperSelectionChange(evant: any) { this.scrollToSectionHook(); }

private scrollToSectionHook() { const element = document.querySelector('.stepperTop'); console.log(element); if (element) { setTimeout(() => { element.scrollIntoView({behavior: 'smooth', block: 'start', inline: 'nearest'}); console.log('scrollIntoView'); }, 250 ); }

}

Cheers!

On Wed, Mar 21, 2018 at 5:19 PM, Daniela Ortiz [email protected] wrote:

hi! I am having exactly the same issue mentioned earlier, but I have tried those solutions and they didn't work for me, every time I change to another step, the page is scrolled way down. Do you have any other solution for this problem?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/angular/material2/issues/8881#issuecomment-375138011, or mute the thread https://github.com/notifications/unsubscribe-auth/AOdvtyWLCtl7yK-Nzqv2KeX5PKUAGZUPks5tgu4ogaJpZM4Q7I4W .

-- Very respectfully,

CPT Alberto L. Bonfiglio USA Retired Tel 541 704 PETS (7387)

AlbertoBonfiglio avatar Mar 22 '18 23:03 AlbertoBonfiglio

Is this on the agenda? or should we provide a custom fix for it?

mackelito avatar Mar 25 '18 11:03 mackelito

@mackelito It's something we need to fix, but if you need a solution now I would look at the workarounds posted above.

mmalerba avatar Mar 25 '18 17:03 mmalerba

Might help someone... I've added a stepperTop + index as a class to each step's label (and just a plain stepperTop to the wrapper of the stepper): <ng-template matStepLabel><span class="stepperTop0">Step0</span></ng-template>

I've changed AlbertoBonfiglio's logic a little bit to provide the selectedIndex of the selectionChange event. this.stepper.selectionChange.subscribe((event) => { this.scrollToSectionHook(event.selectedIndex); });

And the scrollToSectionHook modified like so to achieve the best result imo: private scrollToSectionHook(index) { const element = document.querySelector(.stepperTop${index ? index - 1 : ''); if(element) { setTimeout(() => { element.scrollIntoView({block: 'start', inline: 'nearest', behavior: 'smooth'}); }, 250 ); } }

Laurensvdw avatar Jul 23 '18 20:07 Laurensvdw

As per @Laurensvdw here is my version using the internal stepper api for the label id:

  @ViewChild(MatStepper)
  private stepper: MatStepper;
..
..
  // scroll to selected step
    const stepId = this.stepper._getStepLabelId(this.stepper.selectedIndex);
    const stepElement = document.getElementById(stepId);
    if (stepElement) {
      setTimeout(() => {
        stepElement.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
      }, 250);
    }

webprofusion-chrisc avatar Sep 28 '18 03:09 webprofusion-chrisc

@webprofusion-chrisc 's code works even nicer as a directive

@Directive({
  selector: 'mat-vertical-stepper'
})
export class MatVerticalStepperScrollerDirective {

  constructor(private stepper: MatStepper) {}

  @HostListener('selectionChange', ['$event'])
  selectionChanged(selection: StepperSelectionEvent) {
    const stepId = this.stepper._getStepLabelId(selection.selectedIndex);
    const stepElement = document.getElementById(stepId);
    if (stepElement) {
      setTimeout(() => {
        stepElement.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
      }, 250);
    }
  }
}

Sabartius avatar Mar 13 '19 06:03 Sabartius

In my case, I have a mat-toolbar which is fixed at the top of my page.

The trick is to scroll to the step label with id selectedIndex - 1 (except for 0, of course):

const previousIndex: number = (_index === 0) ? 0 : _index - 1;
// Get element
const element: Element = document.querySelector('#' + _stepper._getStepLabelId(previousIndex));

AdrianPal avatar Mar 13 '19 09:03 AdrianPal

The only workaround I could think of, in a layout using mat-sidenav:

const stepElement = document.getElementsByClassName('mat-drawer-content')[0];
stepElement.scrollTop = 0;

Hope it helps someone.

lsfgrd avatar Mar 15 '19 16:03 lsfgrd

+1 for this. @Sabartius's solution worked fine for me for now.

hisham avatar Mar 19 '19 17:03 hisham

Same problem here, i'm going to try @Sabartius solution and post the feedback after. But someone have a idea or preview to official solution for that?

zenojr avatar Apr 03 '19 12:04 zenojr

@Sabartius solution cuts off the step indicator/heading, so I offset the selected index by one and it seems to work nicely.

import { Directive, HostListener, ViewChild } from '@angular/core';
import { MatStepperModule, MatStepper } from '@angular/material';
import { StepperSelectionEvent } from '@angular/cdk/stepper';

@Directive({
  selector: '[appMatVerticalStepperScroller]',
})
export class MatVerticalStepperScrollerDirective {
  constructor(private stepper: MatStepper) {}

  @HostListener('selectionChange', ['$event'])
  selectionChanged(selection: StepperSelectionEvent) {
    const stepId = this.stepper._getStepLabelId(selection.selectedIndex - 1);
    const stepElement = document.getElementById(stepId);
    if (stepElement) {
      setTimeout(() => {
        stepElement.scrollIntoView(true);
      }, 250);
    }
  }
}

mrjoedang avatar Apr 30 '19 02:04 mrjoedang

I would just animation to what @mrjoedang did and it works perfect as a directive:

import { Directive, HostListener, ViewChild } from '@angular/core';
import { MatStepperModule, MatStepper } from '@angular/material';
import { StepperSelectionEvent } from '@angular/cdk/stepper';

@Directive({
  selector: '[appMatVerticalStepperScroller]',
})
export class MatVerticalStepperScrollerDirective {
  constructor(private stepper: MatStepper) {}

  @HostListener('selectionChange', ['$event'])
  selectionChanged(selection: StepperSelectionEvent) {
    const stepId = this.stepper._getStepLabelId(selection.selectedIndex - 1);
    const stepElement = document.getElementById(stepId);
    if (stepElement) {
      setTimeout(() => {
        stepElement.scrollIntoView({behavior: 'smooth', block: 'start', inline: 'nearest'});
      }, 250);
    }
  }
}

bostondevin avatar Sep 16 '19 18:09 bostondevin

+1

hhsissi avatar Nov 07 '19 14:11 hhsissi

this is still an issue on Angular 9 https://stackblitz.com/edit/angular-9-material-starter-cbywvk?file=package.json

leomendoza123 avatar Jun 03 '20 19:06 leomendoza123

An alternative to @Sabartius's solution, using animationDone event instead of selectionChange so we get rid of the setTimeout:

import { Directive, HostListener } from '@angular/core';
import { MatStepper } from '@angular/material/stepper';

@Directive({
  selector: 'mat-vertical-stepper',
})
export class MatVerticalStepperScrollerDirective {
  constructor(private stepper: MatStepper) {}

  @HostListener('animationDone')
  selectionChanged() {
    const stepId = this.stepper._getStepLabelId(this.stepper.selectedIndex);
    const stepElement = document.getElementById(stepId);
    if (stepElement) {
      stepElement.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
    }
  }
}

greenled avatar Nov 09 '20 04:11 greenled

As per @Laurensvdw here is my version using the internal stepper api for the label id:

  @ViewChild(MatStepper)
  private stepper: MatStepper;
..
..
  // scroll to selected step
    const stepId = this.stepper._getStepLabelId(this.stepper.selectedIndex);
    const stepElement = document.getElementById(stepId);
    if (stepElement) {
      setTimeout(() => {
        stepElement.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
      }, 250);
    }

basant-hembram avatar Nov 30 '20 12:11 basant-hembram

An alternative to @Sabartius's solution, using animationDone event instead of selectionChange so we get rid of the setTimeout:

import { Directive, HostListener } from '@angular/core';
import { MatStepper } from '@angular/material/stepper';

@Directive({
  selector: 'mat-vertical-stepper',
})
export class MatVerticalStepperScrollerDirective {
  constructor(private stepper: MatStepper) {}

  @HostListener('animationDone')
  selectionChanged() {
    const stepId = this.stepper._getStepLabelId(this.stepper.selectedIndex);
    const stepElement = document.getElementById(stepId);
    if (stepElement) {
      stepElement.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
    }
  }
}

Thank you, it works well with animationDone. I just had to use another selector, because the direct one didnt seem to work. So I simply use selector: '[mat-vertical-stepper-scroller]',

danbraik avatar Mar 06 '21 10:03 danbraik

@danbraik you're welcome. I'm glad it works for you too.

greenled avatar Jun 25 '21 05:06 greenled

By all means, @greenled's solution looks great, but I think I'm missing something in the implementation of it (sorry, I'm new to angular/material). Does the selector go on the <mat-stepper> wrapper or on each <mat-step> child? Do the <mat-step> children have internal IDs, or do I need to add my own ID? Right now my html looks like

<mat-stepper appMatVerticalStepperScroller [linear]="true" orientation="vertical">
    <mat-step label="step1">
        // stuff
    </mat-step>
    <mat-step label="step2">
        // more stuff
    </mat-step>
</mat-stepper>

but it's not working for me.

aYorky avatar Dec 03 '21 15:12 aYorky

Hi @aYorky ! The selector should go in the stepper, not on each step. You placed it right.

greenled avatar Dec 03 '21 17:12 greenled

@greenled Huh. Yet it doesn't work for me. All I should need to do is have that directive declared in the module and throw the selector in the html, and it should work right?

aYorky avatar Dec 03 '21 17:12 aYorky

@aYorky I was working with Angular 11 when I wrote it. Maybe it just doesn't work on newer versions. Which version are you using? Could you share more details about your setup? Maybe a StackBlitz example?

greenled avatar Dec 03 '21 18:12 greenled

@greenled I'm on A12. What other information would you need? I've tried to do a stackblitz, but it won't recognize the material package even though it's an installed dependency.

aYorky avatar Dec 06 '21 14:12 aYorky

+1

Bschitter avatar Jan 20 '22 08:01 Bschitter