angular / components

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

[Stepper] Vertical should scroll to top when changing steps #8881

Open tiswrt opened 6 years ago

tiswrt commented 6 years ago

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

demisx commented 6 years ago

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

sitewerk commented 6 years ago

Would be a very helpful feature!

jberall commented 6 years ago

A simple work around is a directive.

Then in the next or back ...

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

@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; } }

AlbertoBonfiglio commented 6 years ago

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 ); } }

daniortiz-bj commented 6 years ago

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?

AlbertoBonfiglio commented 6 years ago

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 notifications@github.com 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)

mackelito commented 6 years ago

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

mmalerba commented 6 years ago

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

Laurensvdw commented 5 years ago

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 ); } }

webprofusion-chrisc commented 5 years ago

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);
    }
Sabartius commented 5 years ago

@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);
    }
  }
}
AdrianPal commented 5 years ago

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));
lsfgrd commented 5 years ago

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.

hisham commented 5 years ago

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

zenojr commented 5 years ago

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?

mrjoedang commented 5 years ago

@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);
    }
  }
}
bostondevin commented 4 years ago

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);
    }
  }
}
hhsissi commented 4 years ago

+1

leomendoza123 commented 4 years ago

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

greenled commented 3 years ago

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' });
    }
  }
}
basant-hembram commented 3 years ago

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);
    }
danbraik commented 3 years ago

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]',

greenled commented 3 years ago

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

aYorky commented 2 years ago

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.

greenled commented 2 years ago

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

aYorky commented 2 years ago

@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?

greenled commented 2 years ago

@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?

aYorky commented 2 years ago

@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.

Bschitter commented 2 years ago

+1

MatkoMilic commented 11 months ago

Had exactly the same issue, these steps work for me:

Create a ref variable.

Give ref prop to element where you are sure that this scroll bar is in, in my case it was Grid element from ui, when I inspected elements I found that problematic scroll bar is within that element.

Then just add this code and it works:

const handleChangeStep = async (stepIndex: number) => { setStep(stepIndex); if (gridRef.current) gridRef.current.scrollIntoView(); };