Skip to content

Commit d256219

Browse files
crisbetowagnermaciel
authored andcommitted
fix(material/tabs): picking up mat-tab-label from child tabs (#23560)
We use `ContenChild` to get a hold of the projected `mat-tab-label` inside a tab, but the problem is that this will also pick up label inside of nested tabs. These changes add some safeguards so the labels don't get mixed up. Fixes #23558. (cherry picked from commit 505c0d1)
1 parent 1aaec0e commit d256219

File tree

8 files changed

+114
-13
lines changed

8 files changed

+114
-13
lines changed

src/material-experimental/mdc-tabs/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ export {
2828
MatTabsConfig,
2929
MAT_TABS_CONFIG,
3030
MAT_TAB_GROUP,
31+
MAT_TAB,
3132
ScrollDirection,
3233
} from '@angular/material/tabs';

src/material-experimental/mdc-tabs/tab-group.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('MDC-based MatTabGroup', () => {
4040
NestedTabs,
4141
TabGroupWithIndirectDescendantTabs,
4242
TabGroupWithSpaceAbove,
43+
NestedTabGroupWithLabel,
4344
],
4445
});
4546

@@ -673,6 +674,19 @@ describe('MDC-based MatTabGroup', () => {
673674

674675
expect(fixture.nativeElement.textContent).toContain('pizza is active');
675676
}));
677+
678+
it('should not pick up mat-tab-label from a child tab', fakeAsync(() => {
679+
const fixture = TestBed.createComponent(NestedTabGroupWithLabel);
680+
fixture.detectChanges();
681+
tick();
682+
fixture.detectChanges();
683+
684+
const labels = fixture.nativeElement.querySelectorAll('.mdc-tab__text-label');
685+
const contents = Array.from<HTMLElement>(labels).map(label => label.textContent?.trim());
686+
687+
expect(contents).toEqual(
688+
['Parent 1', 'Parent 2', 'Parent 3', 'Child 1', 'Child 2', 'Child 3']);
689+
}));
676690
});
677691

678692
describe('nested tabs', () => {
@@ -1166,3 +1180,25 @@ class TabGroupWithInkBarFitToContent {
11661180
class TabGroupWithSpaceAbove {
11671181
@ViewChild(MatTabGroup) tabGroup: MatTabGroup;
11681182
}
1183+
1184+
1185+
@Component({
1186+
template: `
1187+
<mat-tab-group>
1188+
<mat-tab label="Parent 1">
1189+
<mat-tab-group>
1190+
<mat-tab label="Child 1">Content 1</mat-tab>
1191+
<mat-tab>
1192+
<ng-template mat-tab-label>Child 2</ng-template>
1193+
Content 2
1194+
</mat-tab>
1195+
<mat-tab label="Child 3">Child 3</mat-tab>
1196+
</mat-tab-group>
1197+
</mat-tab>
1198+
<mat-tab label="Parent 2">Parent 2</mat-tab>
1199+
<mat-tab label="Parent 3">Parent 3</mat-tab>
1200+
</mat-tab-group>
1201+
`
1202+
})
1203+
class NestedTabGroupWithLabel {
1204+
}

src/material-experimental/mdc-tabs/tab.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
TemplateRef,
1414
ContentChild,
1515
} from '@angular/core';
16-
import {MatTab as BaseMatTab} from '@angular/material/tabs';
16+
import {MatTab as BaseMatTab, MAT_TAB} from '@angular/material/tabs';
1717
import {MatTabContent} from './tab-content';
1818
import {MatTabLabel} from './tab-label';
1919

@@ -28,6 +28,7 @@ import {MatTabLabel} from './tab-label';
2828
changeDetection: ChangeDetectionStrategy.OnPush,
2929
encapsulation: ViewEncapsulation.None,
3030
exportAs: 'matTab',
31+
providers: [{provide: MAT_TAB, useExisting: MatTab}]
3132
})
3233
export class MatTab extends BaseMatTab {
3334
/**

src/material/tabs/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export {
1919
export {MatTabHeader, _MatTabHeaderBase} from './tab-header';
2020
export {MatTabLabelWrapper} from './tab-label-wrapper';
2121
export {MatTab, MAT_TAB_GROUP} from './tab';
22-
export {MatTabLabel} from './tab-label';
22+
export {MatTabLabel, MAT_TAB} from './tab-label';
2323
export {MatTabNav, MatTabLink, _MatTabNavBase, _MatTabLinkBase} from './tab-nav-bar/index';
2424
export {MatTabContent} from './tab-content';
2525
export {ScrollDirection} from './paginated-tab-header';

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('MatTabGroup', () => {
4040
NestedTabs,
4141
TabGroupWithIndirectDescendantTabs,
4242
TabGroupWithSpaceAbove,
43+
NestedTabGroupWithLabel,
4344
],
4445
});
4546

@@ -673,6 +674,19 @@ describe('MatTabGroup', () => {
673674

674675
expect(fixture.nativeElement.textContent).toContain('pizza is active');
675676
}));
677+
678+
it('should not pick up mat-tab-label from a child tab', fakeAsync(() => {
679+
const fixture = TestBed.createComponent(NestedTabGroupWithLabel);
680+
fixture.detectChanges();
681+
tick();
682+
fixture.detectChanges();
683+
684+
const labels = fixture.nativeElement.querySelectorAll('.mat-tab-label-content');
685+
const contents = Array.from<HTMLElement>(labels).map(label => label.textContent?.trim());
686+
687+
expect(contents).toEqual(
688+
['Parent 1', 'Parent 2', 'Parent 3', 'Child 1', 'Child 2', 'Child 3']);
689+
}));
676690
});
677691

678692
describe('nested tabs', () => {
@@ -1099,3 +1113,25 @@ class TabGroupWithIndirectDescendantTabs {
10991113
class TabGroupWithSpaceAbove {
11001114
@ViewChild(MatTabGroup) tabGroup: MatTabGroup;
11011115
}
1116+
1117+
1118+
@Component({
1119+
template: `
1120+
<mat-tab-group>
1121+
<mat-tab label="Parent 1">
1122+
<mat-tab-group>
1123+
<mat-tab label="Child 1">Content 1</mat-tab>
1124+
<mat-tab>
1125+
<ng-template mat-tab-label>Child 2</ng-template>
1126+
Content 2
1127+
</mat-tab>
1128+
<mat-tab label="Child 3">Child 3</mat-tab>
1129+
</mat-tab-group>
1130+
</mat-tab>
1131+
<mat-tab label="Parent 2">Parent 2</mat-tab>
1132+
<mat-tab label="Parent 3">Parent 3</mat-tab>
1133+
</mat-tab-group>
1134+
`
1135+
})
1136+
class NestedTabGroupWithLabel {
1137+
}

src/material/tabs/tab-label.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, InjectionToken} from '@angular/core';
9+
import {
10+
Directive,
11+
Inject,
12+
InjectionToken,
13+
Optional,
14+
TemplateRef,
15+
ViewContainerRef,
16+
} from '@angular/core';
1017
import {CdkPortal} from '@angular/cdk/portal';
1118

1219
/**
@@ -16,9 +23,22 @@ import {CdkPortal} from '@angular/cdk/portal';
1623
*/
1724
export const MAT_TAB_LABEL = new InjectionToken<MatTabLabel>('MatTabLabel');
1825

26+
/**
27+
* Used to provide a tab label to a tab without causing a circular dependency.
28+
* @docs-private
29+
*/
30+
export const MAT_TAB = new InjectionToken<any>('MAT_TAB');
31+
1932
/** Used to flag tab labels for use with the portal directive */
2033
@Directive({
2134
selector: '[mat-tab-label], [matTabLabel]',
2235
providers: [{provide: MAT_TAB_LABEL, useExisting: MatTabLabel}],
2336
})
24-
export class MatTabLabel extends CdkPortal {}
37+
export class MatTabLabel extends CdkPortal {
38+
constructor(
39+
templateRef: TemplateRef<any>,
40+
viewContainerRef: ViewContainerRef,
41+
@Inject(MAT_TAB) @Optional() public _closestTab: any) {
42+
super(templateRef, viewContainerRef);
43+
}
44+
}

src/material/tabs/tab.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
import {CanDisable, mixinDisabled} from '@angular/material/core';
2929
import {Subject} from 'rxjs';
3030
import {MAT_TAB_CONTENT} from './tab-content';
31-
import {MAT_TAB_LABEL, MatTabLabel} from './tab-label';
31+
import {MAT_TAB_LABEL, MatTabLabel, MAT_TAB} from './tab-label';
3232

3333

3434
// Boilerplate for applying mixins to MatTab.
@@ -49,6 +49,7 @@ export const MAT_TAB_GROUP = new InjectionToken<any>('MAT_TAB_GROUP');
4949
changeDetection: ChangeDetectionStrategy.Default,
5050
encapsulation: ViewEncapsulation.None,
5151
exportAs: 'matTab',
52+
providers: [{provide: MAT_TAB, useExisting: MatTab}]
5253
})
5354
export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges, OnDestroy {
5455
/** Content for the tab label given by `<ng-template mat-tab-label>`. */
@@ -133,12 +134,12 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
133134
* TS 4.0 doesn't allow properties to override accessors or vice-versa.
134135
* @docs-private
135136
*/
136-
protected _setTemplateLabelInput(value: MatTabLabel) {
137-
// Only update the templateLabel via query if there is actually
138-
// a MatTabLabel found. This works around an issue where a user may have
139-
// manually set `templateLabel` during creation mode, which would then get clobbered
140-
// by `undefined` when this query resolves.
141-
if (value) {
137+
protected _setTemplateLabelInput(value: MatTabLabel|undefined) {
138+
// Only update the label if the query managed to find one. This works around an issue where a
139+
// user may have manually set `templateLabel` during creation mode, which would then get
140+
// clobbered by `undefined` when the query resolves. Also note that we check that the closest
141+
// tab matches the current one so that we don't pick up labels from nested tabs.
142+
if (value && value._closestTab === this) {
142143
this._templateLabel = value;
143144
}
144145
}

tools/public_api_guard/material/tabs.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export const _MAT_INK_BAR_POSITIONER: InjectionToken<_MatInkBarPositioner>;
5858
// @public
5959
function _MAT_INK_BAR_POSITIONER_FACTORY(): _MatInkBarPositioner;
6060

61+
// @public
62+
export const MAT_TAB: InjectionToken<any>;
63+
6164
// @public
6265
const MAT_TAB_CONTENT: InjectionToken<MatTabContent>;
6366

@@ -114,7 +117,7 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
114117
ngOnInit(): void;
115118
origin: number | null;
116119
position: number | null;
117-
protected _setTemplateLabelInput(value: MatTabLabel): void;
120+
protected _setTemplateLabelInput(value: MatTabLabel | undefined): void;
118121
readonly _stateChanges: Subject<void>;
119122
get templateLabel(): MatTabLabel;
120123
set templateLabel(value: MatTabLabel);
@@ -316,10 +319,13 @@ export type MatTabHeaderPosition = 'above' | 'below';
316319

317320
// @public
318321
export class MatTabLabel extends CdkPortal {
322+
constructor(templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef, _closestTab: any);
323+
// (undocumented)
324+
_closestTab: any;
319325
// (undocumented)
320326
static ɵdir: i0.ɵɵDirectiveDeclaration<MatTabLabel, "[mat-tab-label], [matTabLabel]", never, {}, {}, never>;
321327
// (undocumented)
322-
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabLabel, never>;
328+
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabLabel, [null, null, { optional: true; }]>;
323329
}
324330

325331
// @public

0 commit comments

Comments
 (0)