Skip to content

Commit 5279fe3

Browse files
devversionjosephperrott
authored andcommitted
fix(tabs): selectedIndex being overwritten if tabs are being added / removed (#12245)
1 parent dc66263 commit 5279fe3

File tree

2 files changed

+48
-28
lines changed

2 files changed

+48
-28
lines changed

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -427,25 +427,35 @@ describe('MatTabGroup', () => {
427427

428428

429429
it('should maintain the selected tab if a tab is removed', () => {
430-
// Add a couple of tabs so we have more to work with.
431-
fixture.componentInstance.tabs.push(
432-
{label: 'New tab', content: 'with new content'},
433-
{label: 'Another new tab', content: 'with newer content'}
434-
);
435-
436-
// Select the second-to-last tab.
437-
fixture.componentInstance.selectedIndex = 3;
430+
// Select the second tab.
431+
fixture.componentInstance.selectedIndex = 1;
438432
fixture.detectChanges();
439433

440434
const component: MatTabGroup =
441435
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;
442436

443-
// Remove a tab right before the selected one.
444-
fixture.componentInstance.tabs.splice(2, 1);
437+
// Remove the first tab that is right before the selected one.
438+
fixture.componentInstance.tabs.splice(0, 1);
445439
fixture.detectChanges();
446440

447-
expect(component.selectedIndex).toBe(1);
448-
expect(component._tabs.toArray()[1].isActive).toBe(true);
441+
// Since the first tab has been removed and the second one was selected before, the selected
442+
// tab moved one position to the right. Meaning that the tab is now the first tab.
443+
expect(component.selectedIndex).toBe(0);
444+
expect(component._tabs.toArray()[0].isActive).toBe(true);
445+
});
446+
447+
it('should be able to select a new tab after creation', () => {
448+
fixture.detectChanges();
449+
const component: MatTabGroup =
450+
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;
451+
452+
fixture.componentInstance.tabs.push({label: 'Last tab', content: 'at the end'});
453+
fixture.componentInstance.selectedIndex = 3;
454+
455+
fixture.detectChanges();
456+
457+
expect(component.selectedIndex).toBe(3);
458+
expect(component._tabs.toArray()[3].isActive).toBe(true);
449459
});
450460

451461
it('should not fire `selectedTabChange` when the amount of tabs changes', fakeAsync(() => {

src/lib/tabs/tab-group.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,9 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
159159
* a new selected tab should transition in (from the left or right).
160160
*/
161161
ngAfterContentChecked() {
162-
// Clamp the next selected index to the bounds of 0 and the tabs length.
163-
// Note the `|| 0`, which ensures that values like NaN can't get through
164-
// and which would otherwise throw the component into an infinite loop
165-
// (since Math.max(NaN, 0) === NaN).
166-
let indexToSelect = this._indexToSelect =
167-
Math.min(this._tabs.length - 1, Math.max(this._indexToSelect || 0, 0));
162+
// Don't clamp the `indexToSelect` immediately in the setter because it can happen that
163+
// the amount of tabs changes before the actual change detection runs.
164+
const indexToSelect = this._indexToSelect = this._clampTabIndex(this._indexToSelect);
168165

169166
// If there is a change in selected index, emit a change event. Should not trigger if
170167
// the selected index has not yet been initialized.
@@ -200,16 +197,21 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
200197
// Subscribe to changes in the amount of tabs, in order to be
201198
// able to re-render the content as new tabs are added or removed.
202199
this._tabsSubscription = this._tabs.changes.subscribe(() => {
203-
const tabs = this._tabs.toArray();
204-
205-
// Maintain the previously-selected tab if a new tab is added or removed.
206-
for (let i = 0; i < tabs.length; i++) {
207-
if (tabs[i].isActive) {
208-
// Assign both to the `_indexToSelect` and `_selectedIndex` so we don't fire a changed
209-
// event, otherwise the consumer may end up in an infinite loop in some edge cases like
210-
// adding a tab within the `selectedIndexChange` event.
211-
this._indexToSelect = this._selectedIndex = i;
212-
break;
200+
const indexToSelect = this._clampTabIndex(this._indexToSelect);
201+
202+
// Maintain the previously-selected tab if a new tab is added or removed and there is no
203+
// explicit change that selects a different tab.
204+
if (indexToSelect === this._selectedIndex) {
205+
const tabs = this._tabs.toArray();
206+
207+
for (let i = 0; i < tabs.length; i++) {
208+
if (tabs[i].isActive) {
209+
// Assign both to the `_indexToSelect` and `_selectedIndex` so we don't fire a changed
210+
// event, otherwise the consumer may end up in an infinite loop in some edge cases like
211+
// adding a tab within the `selectedIndexChange` event.
212+
this._indexToSelect = this._selectedIndex = i;
213+
break;
214+
}
213215
}
214216
}
215217

@@ -261,6 +263,14 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
261263
});
262264
}
263265

266+
/** Clamps the given index to the bounds of 0 and the tabs length. */
267+
private _clampTabIndex(index: number | null): number {
268+
// Note the `|| 0`, which ensures that values like NaN can't get through
269+
// and which would otherwise throw the component into an infinite loop
270+
// (since Math.max(NaN, 0) === NaN).
271+
return Math.min(this._tabs.length - 1, Math.max(index || 0, 0));
272+
}
273+
264274
/** Returns a unique id for each tab label element */
265275
_getTabLabelId(i: number): string {
266276
return `mat-tab-label-${this._groupId}-${i}`;

0 commit comments

Comments
 (0)