Skip to content

Commit e67fda6

Browse files
committed
fix(tabs): preserve scroll position when switching between tabs
Preserves the scroll position when switching between tabs. Previously it was being reset to 0, because we detach and re-attach the content. Fixes #6722.
1 parent 3255cf3 commit e67fda6

File tree

3 files changed

+77
-5
lines changed

3 files changed

+77
-5
lines changed

src/lib/tabs/tab-body.ts

+37-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
ViewContainerRef,
2424
forwardRef,
2525
ViewChild,
26+
OnChanges,
27+
SimpleChanges,
2628
} from '@angular/core';
2729
import {AnimationEvent} from '@angular/animations';
2830
import {TemplatePortal, CdkPortalOutlet, PortalHostDirective} from '@angular/cdk/portal';
@@ -81,6 +83,7 @@ export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestr
8183
.subscribe((isCentering: boolean) => {
8284
if (isCentering && !this.hasAttached()) {
8385
this.attach(this._host._content);
86+
this._host._restoreScrollPosition();
8487
}
8588
});
8689

@@ -111,9 +114,13 @@ export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestr
111114
animations: [matTabsAnimations.translateTab],
112115
host: {
113116
'class': 'mat-tab-body',
117+
'[class.mat-tab-body-active]': 'active',
114118
},
115119
})
116-
export class MatTabBody implements OnInit {
120+
export class MatTabBody implements OnInit, OnChanges {
121+
/** Element wrapping the tab's content. */
122+
@ViewChild('content') _contentElement: ElementRef;
123+
117124
/** Event emitted when the tab begins to animate towards the center as the active tab. */
118125
@Output() readonly _onCentering: EventEmitter<number> = new EventEmitter<number>();
119126

@@ -132,6 +139,12 @@ export class MatTabBody implements OnInit {
132139
/** The tab body content to display. */
133140
@Input('content') _content: TemplatePortal;
134141

142+
/** Whether the tab is currently active. */
143+
@Input() active: boolean;
144+
145+
/** Scroll position of the tab before the user switched away. */
146+
private _lastScrollPosition = 0;
147+
135148
/** The shifted index position of the tab body, where zero represents the active center tab. */
136149
@Input()
137150
set position(position: number) {
@@ -180,6 +193,16 @@ export class MatTabBody implements OnInit {
180193
}
181194
}
182195

196+
ngOnChanges(changes: SimpleChanges) {
197+
// Cache the scroll position before moving away from the tab. Note that this has to be done
198+
// through change detection and as early as possible, because some browsers (namely Safari)
199+
// will reset the scroll position when we switch from an absolute to a relative position.
200+
if (changes.active && changes.active.previousValue) {
201+
this._lastScrollPosition = this._elementRef.nativeElement.scrollTop ||
202+
this._contentElement.nativeElement.scrollTop;
203+
}
204+
}
205+
183206
_onTranslateTabComplete(e: AnimationEvent): void {
184207
// If the transition to the center is complete, emit an event.
185208
if (this._isCenterPosition(e.toState) && this._isCenterPosition(this._position)) {
@@ -197,9 +220,19 @@ export class MatTabBody implements OnInit {
197220
}
198221

199222
/** Whether the provided position state is considered center, regardless of origin. */
200-
_isCenterPosition(position: MatTabBodyPositionState|string): boolean {
223+
_isCenterPosition(position: MatTabBodyPositionState | string): boolean {
201224
return position == 'center' ||
202-
position == 'left-origin-center' ||
203-
position == 'right-origin-center';
225+
position == 'left-origin-center' ||
226+
position == 'right-origin-center';
227+
}
228+
229+
_restoreScrollPosition() {
230+
if (this._lastScrollPosition) {
231+
// Depending on the browser, the scrollable element can end up being
232+
// either the host element or the element with all the content.
233+
this._contentElement.nativeElement.scrollTop =
234+
this._elementRef.nativeElement.scrollTop =
235+
this._lastScrollPosition;
236+
}
204237
}
205238
}

src/lib/tabs/tab-group.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
*ngFor="let tab of _tabs; let i = index"
3737
[id]="_getTabContentId(i)"
3838
[attr.aria-labelledby]="_getTabLabelId(i)"
39-
[class.mat-tab-body-active]="selectedIndex == i"
39+
[active]="selectedIndex === i"
4040
[content]="tab.content"
4141
[position]="tab.position"
4242
[origin]="tab.origin"

src/lib/tabs/tab-group.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,45 @@ describe('MatTabGroup', () => {
418418
expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled();
419419
}));
420420

421+
it('should preserve the scroll position when switching between tabs', fakeAsync(() => {
422+
const testComponent = fixture.componentInstance;
423+
424+
// Add a lot of content to make one of the tabs scrollable.
425+
testComponent.tabs[1].content = new Array(500).fill('content!').join('\n\n');
426+
fixture.detectChanges();
427+
tick(500);
428+
429+
// Cap the tab group height.
430+
fixture.debugElement.query(By.css('mat-tab-group')).nativeElement.style.height = `300px`;
431+
432+
const tabElements = fixture.debugElement.queryAll(By.css('.mat-tab-body-content'))
433+
.map(debugElement => debugElement.nativeElement as HTMLElement);
434+
435+
// Focus the tab with the extra content.
436+
testComponent.selectedIndex = 1;
437+
fixture.detectChanges();
438+
tick(500);
439+
440+
// Ensure that there is content and scroll down 100px.
441+
expect(tabElements[1].offsetHeight).toBeGreaterThan(0, 'Expected tab to have some content.');
442+
443+
// Handle some differences in the way browsers determine what element is scrollable.
444+
tabElements[1].scrollTop = tabElements[1].parentElement!.scrollTop = 100;
445+
446+
// Move to another tab.
447+
testComponent.selectedIndex = 0;
448+
fixture.detectChanges();
449+
tick(500);
450+
451+
// Switch back to the tab with the extra content.
452+
testComponent.selectedIndex = 1;
453+
fixture.detectChanges();
454+
tick(500);
455+
456+
expect(tabElements[1].scrollTop || tabElements[1].parentElement!.scrollTop)
457+
.toBe(100, 'Expected scroll position to be restored.');
458+
}));
459+
421460
});
422461

423462
describe('async tabs', () => {

0 commit comments

Comments
 (0)