components
components copied to clipboard
[Stepper] Vertical should scroll to top when changing steps
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
We desperately need this as the stepper is virtually unusable on mobile devices.
Would be a very helpful feature!
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; } }
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 ); } }
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?
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)
Is this on the agenda? or should we provide a custom fix for it?
@mackelito It's something we need to fix, but if you need a solution now I would look at the workarounds posted above.
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 ); } }
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 '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);
}
}
}
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));
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.
+1 for this. @Sabartius's solution worked fine for me for now.
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?
@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);
}
}
}
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);
}
}
}
+1
this is still an issue on Angular 9 https://stackblitz.com/edit/angular-9-material-starter-cbywvk?file=package.json
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' });
}
}
}
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); }
An alternative to @Sabartius's solution, using
animationDone
event instead ofselectionChange
so we get rid of thesetTimeout
: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 you're welcome. I'm glad it works for you too.
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.
Hi @aYorky ! The selector should go in the stepper, not on each step. You placed it right.
@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 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 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.
+1
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(); };