Skip to content

Commit 8d5aefe

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 1b6b270 commit 8d5aefe

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed

src/lib/tabs/tab-body.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
AfterViewChecked,
1919
ViewEncapsulation,
2020
ChangeDetectionStrategy,
21+
OnChanges,
22+
SimpleChanges,
2123
} from '@angular/core';
2224
import {
2325
trigger,
@@ -65,6 +67,7 @@ export type MdTabBodyOriginState = 'left' | 'right';
6567
changeDetection: ChangeDetectionStrategy.OnPush,
6668
host: {
6769
'class': 'mat-tab-body',
70+
'[class.mat-tab-body-active]': 'active',
6871
},
6972
animations: [
7073
trigger('translateTab', [
@@ -87,18 +90,27 @@ export type MdTabBodyOriginState = 'left' | 'right';
8790
])
8891
]
8992
})
90-
export class MdTabBody implements OnInit, AfterViewChecked {
93+
export class MdTabBody implements OnInit, OnChanges, AfterViewChecked {
9194
/** The portal host inside of this container into which the tab body content will be loaded. */
9295
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
9396

97+
/** Element wrapping the tab's content. */
98+
@ViewChild('content') _contentElement: ElementRef;
99+
94100
/** Event emitted when the tab begins to animate towards the center as the active tab. */
95101
@Output() onCentering: EventEmitter<number> = new EventEmitter<number>();
96102

97103
/** Event emitted when the tab completes its animation towards the center. */
98104
@Output() onCentered: EventEmitter<void> = new EventEmitter<void>(true);
99105

100106
/** The tab body content to display. */
101-
@Input('content') _content: TemplatePortal<any>;
107+
@Input('content') _contentPortal: TemplatePortal<any>;
108+
109+
/** Whether the tab is currently active. */
110+
@Input() active: boolean;
111+
112+
/** Scroll position of the tab before the user switched away. */
113+
private _lastScrollPosition = 0;
102114

103115
/** The shifted index position of the tab body, where zero represents the active center tab. */
104116
_position: MdTabBodyPositionState;
@@ -146,7 +158,25 @@ export class MdTabBody implements OnInit, AfterViewChecked {
146158
*/
147159
ngAfterViewChecked() {
148160
if (this._isCenterPosition(this._position) && !this._portalHost.hasAttached()) {
149-
this._portalHost.attach(this._content);
161+
this._portalHost.attach(this._contentPortal);
162+
163+
if (this._lastScrollPosition) {
164+
// Depending on the browser, the scrollable element can end up being
165+
// either the host element or the element with all the content.
166+
this._contentElement.nativeElement.scrollTop =
167+
this._elementRef.nativeElement.scrollTop =
168+
this._lastScrollPosition;
169+
}
170+
}
171+
}
172+
173+
ngOnChanges(changes: SimpleChanges) {
174+
// Cache the scroll position before moving away from the tab. Note that this has to be done
175+
// through change detection and as early as possible, because some browsers (namely Safari)
176+
// will reset the scroll position when we switch from an absolute to a relative position.
177+
if (changes.active && changes.active.previousValue) {
178+
this._lastScrollPosition = this._elementRef.nativeElement.scrollTop ||
179+
this._contentElement.nativeElement.scrollTop;
150180
}
151181
}
152182

@@ -176,7 +206,7 @@ export class MdTabBody implements OnInit, AfterViewChecked {
176206
/** Whether the provided position state is considered center, regardless of origin. */
177207
private _isCenterPosition(position: MdTabBodyPositionState|string): boolean {
178208
return position == 'center' ||
179-
position == 'left-origin-center' ||
180-
position == 'right-origin-center';
209+
position == 'left-origin-center' ||
210+
position == 'right-origin-center';
181211
}
182212
}

src/lib/tabs/tab-group.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*ngFor="let tab of _tabs; let i = index"
3030
[id]="_getTabContentId(i)"
3131
[attr.aria-labelledby]="_getTabLabelId(i)"
32-
[class.mat-tab-body-active]="selectedIndex == i"
32+
[active]="selectedIndex === i"
3333
[content]="tab.content"
3434
[position]="tab.position"
3535
[origin]="tab.origin"

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,44 @@ describe('MdTabGroup', () => {
259259
expect(component.selectedIndex).toBe(numberOfTabs - 2);
260260
});
261261

262+
it('should preserve the scroll position when switching between tabs', fakeAsync(() => {
263+
const testComponent = fixture.componentInstance;
264+
265+
// Add a lot of content to make one of the tabs scrollable.
266+
testComponent.tabs[1].content = new Array(500).fill('content!').join('\n\n');
267+
fixture.detectChanges();
268+
tick(500);
269+
270+
// Cap the tab group height.
271+
fixture.debugElement.query(By.css('md-tab-group')).nativeElement.style.height = `300px`;
272+
273+
const tabElements = fixture.debugElement.queryAll(By.css('.mat-tab-body-content'))
274+
.map(debugElement => debugElement.nativeElement as HTMLElement);
275+
276+
// Focus the tab with the extra content.
277+
testComponent.selectedIndex = 1;
278+
fixture.detectChanges();
279+
tick(500);
280+
281+
// Ensure that there is content and scroll down 100px.
282+
expect(tabElements[1].offsetHeight).toBeGreaterThan(0, 'Expected tab to have some content.');
283+
284+
// Handle some differences in the way browsers determine what element is scrollable.
285+
tabElements[1].scrollTop = tabElements[1].parentElement!.scrollTop = 100;
286+
287+
// Move to another tab.
288+
testComponent.selectedIndex = 0;
289+
fixture.detectChanges();
290+
tick(500);
291+
292+
// Switch back to the tab with the extra content.
293+
testComponent.selectedIndex = 1;
294+
fixture.detectChanges();
295+
tick(500);
296+
297+
expect(tabElements[1].scrollTop || tabElements[1].parentElement!.scrollTop)
298+
.toBe(100, 'Expected scroll position to be restored.');
299+
}));
262300
});
263301

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

0 commit comments

Comments
 (0)