Skip to content

Commit f8eb62b

Browse files
devversionjosephperrott
authored andcommitted
fix(tabs): selectedIndex being overwritten if tabs are being added / removed
Due to a recent change that ensures that the selected tab will be kept selected if a new tab has been added or removed (#9132), updating the `selectedIndex` at the same time will not have any effect because it will be overwritten by the `_tabs.change` (from #9132). In order to guarantee that developers can add new tabs and immediately select them once the change detection runs, we only re-index the `selectedIndex` (purpose of #9132) whenever the `indexToSelect` has not explicitly changed (through developer bindings for example) Fixes #12038
1 parent 3255cf3 commit f8eb62b

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
@@ -382,25 +382,35 @@ describe('MatTabGroup', () => {
382382

383383

384384
it('should maintain the selected tab if a tab is removed', () => {
385-
// Add a couple of tabs so we have more to work with.
386-
fixture.componentInstance.tabs.push(
387-
{label: 'New tab', content: 'with new content'},
388-
{label: 'Another new tab', content: 'with newer content'}
389-
);
390-
391-
// Select the second-to-last tab.
392-
fixture.componentInstance.selectedIndex = 3;
385+
// Select the second tab.
386+
fixture.componentInstance.selectedIndex = 1;
393387
fixture.detectChanges();
394388

395389
const component: MatTabGroup =
396390
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;
397391

398-
// Remove a tab right before the selected one.
399-
fixture.componentInstance.tabs.splice(2, 1);
392+
// Remove the first tab that is right before the selected one.
393+
fixture.componentInstance.tabs.splice(0, 1);
400394
fixture.detectChanges();
401395

402-
expect(component.selectedIndex).toBe(1);
403-
expect(component._tabs.toArray()[1].isActive).toBe(true);
396+
// Since the first tab has been removed and the second one was selected before, the selected
397+
// tab moved one position to the right. Meaning that the tab is now the first tab.
398+
expect(component.selectedIndex).toBe(0);
399+
expect(component._tabs.toArray()[0].isActive).toBe(true);
400+
});
401+
402+
it('should be able to select a new tab after creation', () => {
403+
fixture.detectChanges();
404+
const component: MatTabGroup =
405+
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;
406+
407+
fixture.componentInstance.tabs.push({label: 'Last tab', content: 'at the end'});
408+
fixture.componentInstance.selectedIndex = 3;
409+
410+
fixture.detectChanges();
411+
412+
expect(component.selectedIndex).toBe(3);
413+
expect(component._tabs.toArray()[3].isActive).toBe(true);
404414
});
405415

406416
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 boundsof 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+
// Clamp the `indexToSelect` not 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._clampIndexToSelect();
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._clampIndexToSelect();
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 indexToSelect to the bounds of 0 and the tabs length. */
267+
private _clampIndexToSelect(): 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(this._indexToSelect || 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)