From aa44e826634b441d8f6e8082cac4e9614776b8ff Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sat, 18 Dec 2021 12:32:03 +0100 Subject: [PATCH] fix(material/tabs): wrong scroll distance if selected tab is removed Fixes the case where the user has scrolled to the end of the tab list and the selected tab from the beginning is removed. Since we no longer have a selected tab, the scroll distance wasn't being updated. Fixes #24117. --- .../mdc-tabs/tab-header.spec.ts | 22 +++++++++++++++++++ src/material/tabs/paginated-tab-header.ts | 11 +++++++++- src/material/tabs/tab-header.spec.ts | 22 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-tabs/tab-header.spec.ts b/src/material-experimental/mdc-tabs/tab-header.spec.ts index 5922f5c8c679..7f710c019d1e 100644 --- a/src/material-experimental/mdc-tabs/tab-header.spec.ts +++ b/src/material-experimental/mdc-tabs/tab-header.spec.ts @@ -318,6 +318,28 @@ describe('MDC-based MatTabHeader', () => { .withContext('Expected no ripple to show up after mousedown') .toBe(0); }); + + it('should update the scroll distance if a tab is removed and no tabs are selected', fakeAsync(() => { + appComponent.selectedIndex = 0; + appComponent.addTabsForScrolling(); + fixture.detectChanges(); + + // Focus the last tab so the header scrolls to the end. + appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1; + fixture.detectChanges(); + expect(appComponent.tabHeader.scrollDistance).toBe( + appComponent.tabHeader._getMaxScrollDistance(), + ); + + // Remove the first two tabs which includes the selected tab. + appComponent.tabs = appComponent.tabs.slice(2); + fixture.detectChanges(); + tick(); + + expect(appComponent.tabHeader.scrollDistance).toBe( + appComponent.tabHeader._getMaxScrollDistance(), + ); + })); }); describe('rtl', () => { diff --git a/src/material/tabs/paginated-tab-header.ts b/src/material/tabs/paginated-tab-header.ts index 184e4162e7aa..fdbf2f4d332e 100644 --- a/src/material/tabs/paginated-tab-header.ts +++ b/src/material/tabs/paginated-tab-header.ts @@ -211,7 +211,16 @@ export abstract class MatPaginatedTabHeader // We need to defer this to give the browser some time to recalculate // the element dimensions. The call has to be wrapped in `NgZone.run`, // because the viewport change handler runs outside of Angular. - this._ngZone.run(() => Promise.resolve().then(realign)); + this._ngZone.run(() => { + Promise.resolve().then(() => { + // Clamp the scroll distance, because it can change with the number of tabs. + this._scrollDistance = Math.max( + 0, + Math.min(this._getMaxScrollDistance(), this._scrollDistance), + ); + realign(); + }); + }); this._keyManager.withHorizontalOrientation(this._getLayoutDirection()); }); diff --git a/src/material/tabs/tab-header.spec.ts b/src/material/tabs/tab-header.spec.ts index 41749fd0adee..b891cc23ce81 100644 --- a/src/material/tabs/tab-header.spec.ts +++ b/src/material/tabs/tab-header.spec.ts @@ -315,6 +315,28 @@ describe('MatTabHeader', () => { .withContext('Expected no ripple to show up after mousedown') .toBe(0); }); + + it('should update the scroll distance if a tab is removed and no tabs are selected', fakeAsync(() => { + appComponent.selectedIndex = 0; + appComponent.addTabsForScrolling(); + fixture.detectChanges(); + + // Focus the last tab so the header scrolls to the end. + appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1; + fixture.detectChanges(); + expect(appComponent.tabHeader.scrollDistance).toBe( + appComponent.tabHeader._getMaxScrollDistance(), + ); + + // Remove the first two tabs which includes the selected tab. + appComponent.tabs = appComponent.tabs.slice(2); + fixture.detectChanges(); + tick(); + + expect(appComponent.tabHeader.scrollDistance).toBe( + appComponent.tabHeader._getMaxScrollDistance(), + ); + })); }); describe('rtl', () => {