From 21d610f6d9075830df65eb247cc6e39b959facb4 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 9 Sep 2021 22:26:16 +0200 Subject: [PATCH] fix(material/tabs): picking up mat-tab-label from child tabs 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. --- .../mdc-tabs/public-api.ts | 1 + .../mdc-tabs/tab-group.spec.ts | 36 +++++++++++++++++++ src/material-experimental/mdc-tabs/tab.ts | 3 +- src/material/tabs/public-api.ts | 2 +- src/material/tabs/tab-group.spec.ts | 36 +++++++++++++++++++ src/material/tabs/tab-label.ts | 24 +++++++++++-- src/material/tabs/tab.ts | 15 ++++---- tools/public_api_guard/material/tabs.md | 10 ++++-- 8 files changed, 114 insertions(+), 13 deletions(-) diff --git a/src/material-experimental/mdc-tabs/public-api.ts b/src/material-experimental/mdc-tabs/public-api.ts index ef0305ded33a..4e6b1629af0e 100644 --- a/src/material-experimental/mdc-tabs/public-api.ts +++ b/src/material-experimental/mdc-tabs/public-api.ts @@ -28,5 +28,6 @@ export { MatTabsConfig, MAT_TABS_CONFIG, MAT_TAB_GROUP, + MAT_TAB, ScrollDirection, } from '@angular/material/tabs'; diff --git a/src/material-experimental/mdc-tabs/tab-group.spec.ts b/src/material-experimental/mdc-tabs/tab-group.spec.ts index 74e6269642ec..4cdff1500a05 100644 --- a/src/material-experimental/mdc-tabs/tab-group.spec.ts +++ b/src/material-experimental/mdc-tabs/tab-group.spec.ts @@ -40,6 +40,7 @@ describe('MDC-based MatTabGroup', () => { NestedTabs, TabGroupWithIndirectDescendantTabs, TabGroupWithSpaceAbove, + NestedTabGroupWithLabel, ], }); @@ -673,6 +674,19 @@ describe('MDC-based MatTabGroup', () => { expect(fixture.nativeElement.textContent).toContain('pizza is active'); })); + + it('should not pick up mat-tab-label from a child tab', fakeAsync(() => { + const fixture = TestBed.createComponent(NestedTabGroupWithLabel); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const labels = fixture.nativeElement.querySelectorAll('.mdc-tab__text-label'); + const contents = Array.from(labels).map(label => label.textContent?.trim()); + + expect(contents).toEqual( + ['Parent 1', 'Parent 2', 'Parent 3', 'Child 1', 'Child 2', 'Child 3']); + })); }); describe('nested tabs', () => { @@ -1166,3 +1180,25 @@ class TabGroupWithInkBarFitToContent { class TabGroupWithSpaceAbove { @ViewChild(MatTabGroup) tabGroup: MatTabGroup; } + + +@Component({ + template: ` + + + + Content 1 + + Child 2 + Content 2 + + Child 3 + + + Parent 2 + Parent 3 + + ` +}) +class NestedTabGroupWithLabel { +} diff --git a/src/material-experimental/mdc-tabs/tab.ts b/src/material-experimental/mdc-tabs/tab.ts index a2f9f0d75e29..eac99fce3a52 100644 --- a/src/material-experimental/mdc-tabs/tab.ts +++ b/src/material-experimental/mdc-tabs/tab.ts @@ -13,7 +13,7 @@ import { TemplateRef, ContentChild, } from '@angular/core'; -import {MatTab as BaseMatTab} from '@angular/material/tabs'; +import {MatTab as BaseMatTab, MAT_TAB} from '@angular/material/tabs'; import {MatTabContent} from './tab-content'; import {MatTabLabel} from './tab-label'; @@ -28,6 +28,7 @@ import {MatTabLabel} from './tab-label'; changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, exportAs: 'matTab', + providers: [{provide: MAT_TAB, useExisting: MatTab}] }) export class MatTab extends BaseMatTab { /** diff --git a/src/material/tabs/public-api.ts b/src/material/tabs/public-api.ts index cfce3a1ec09f..720b06298b2a 100644 --- a/src/material/tabs/public-api.ts +++ b/src/material/tabs/public-api.ts @@ -19,7 +19,7 @@ export { export {MatTabHeader, _MatTabHeaderBase} from './tab-header'; export {MatTabLabelWrapper} from './tab-label-wrapper'; export {MatTab, MAT_TAB_GROUP} from './tab'; -export {MatTabLabel} from './tab-label'; +export {MatTabLabel, MAT_TAB} from './tab-label'; export {MatTabNav, MatTabLink, _MatTabNavBase, _MatTabLinkBase} from './tab-nav-bar/index'; export {MatTabContent} from './tab-content'; export {ScrollDirection} from './paginated-tab-header'; diff --git a/src/material/tabs/tab-group.spec.ts b/src/material/tabs/tab-group.spec.ts index 7ae246fa39cf..08198300588b 100644 --- a/src/material/tabs/tab-group.spec.ts +++ b/src/material/tabs/tab-group.spec.ts @@ -40,6 +40,7 @@ describe('MatTabGroup', () => { NestedTabs, TabGroupWithIndirectDescendantTabs, TabGroupWithSpaceAbove, + NestedTabGroupWithLabel, ], }); @@ -673,6 +674,19 @@ describe('MatTabGroup', () => { expect(fixture.nativeElement.textContent).toContain('pizza is active'); })); + + it('should not pick up mat-tab-label from a child tab', fakeAsync(() => { + const fixture = TestBed.createComponent(NestedTabGroupWithLabel); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const labels = fixture.nativeElement.querySelectorAll('.mat-tab-label-content'); + const contents = Array.from(labels).map(label => label.textContent?.trim()); + + expect(contents).toEqual( + ['Parent 1', 'Parent 2', 'Parent 3', 'Child 1', 'Child 2', 'Child 3']); + })); }); describe('nested tabs', () => { @@ -1099,3 +1113,25 @@ class TabGroupWithIndirectDescendantTabs { class TabGroupWithSpaceAbove { @ViewChild(MatTabGroup) tabGroup: MatTabGroup; } + + +@Component({ + template: ` + + + + Content 1 + + Child 2 + Content 2 + + Child 3 + + + Parent 2 + Parent 3 + + ` +}) +class NestedTabGroupWithLabel { +} diff --git a/src/material/tabs/tab-label.ts b/src/material/tabs/tab-label.ts index 06f59b9c208f..8c06ca33ae03 100644 --- a/src/material/tabs/tab-label.ts +++ b/src/material/tabs/tab-label.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, InjectionToken} from '@angular/core'; +import { + Directive, + Inject, + InjectionToken, + Optional, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; import {CdkPortal} from '@angular/cdk/portal'; /** @@ -16,9 +23,22 @@ import {CdkPortal} from '@angular/cdk/portal'; */ export const MAT_TAB_LABEL = new InjectionToken('MatTabLabel'); +/** + * Used to provide a tab label to a tab without causing a circular dependency. + * @docs-private + */ + export const MAT_TAB = new InjectionToken('MAT_TAB'); + /** Used to flag tab labels for use with the portal directive */ @Directive({ selector: '[mat-tab-label], [matTabLabel]', providers: [{provide: MAT_TAB_LABEL, useExisting: MatTabLabel}], }) -export class MatTabLabel extends CdkPortal {} +export class MatTabLabel extends CdkPortal { + constructor( + templateRef: TemplateRef, + viewContainerRef: ViewContainerRef, + @Inject(MAT_TAB) @Optional() public _closestTab: any) { + super(templateRef, viewContainerRef); + } +} diff --git a/src/material/tabs/tab.ts b/src/material/tabs/tab.ts index 357274c67574..775842436b16 100644 --- a/src/material/tabs/tab.ts +++ b/src/material/tabs/tab.ts @@ -28,7 +28,7 @@ import { import {CanDisable, mixinDisabled} from '@angular/material/core'; import {Subject} from 'rxjs'; import {MAT_TAB_CONTENT} from './tab-content'; -import {MAT_TAB_LABEL, MatTabLabel} from './tab-label'; +import {MAT_TAB_LABEL, MatTabLabel, MAT_TAB} from './tab-label'; // Boilerplate for applying mixins to MatTab. @@ -49,6 +49,7 @@ export const MAT_TAB_GROUP = new InjectionToken('MAT_TAB_GROUP'); changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.None, exportAs: 'matTab', + providers: [{provide: MAT_TAB, useExisting: MatTab}] }) export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges, OnDestroy { /** Content for the tab label given by ``. */ @@ -133,12 +134,12 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges * TS 4.0 doesn't allow properties to override accessors or vice-versa. * @docs-private */ - protected _setTemplateLabelInput(value: MatTabLabel) { - // Only update the templateLabel via query if there is actually - // a MatTabLabel found. This works around an issue where a user may have - // manually set `templateLabel` during creation mode, which would then get clobbered - // by `undefined` when this query resolves. - if (value) { + protected _setTemplateLabelInput(value: MatTabLabel|undefined) { + // Only update the label if the query managed to find one. This works around an issue where a + // user may have manually set `templateLabel` during creation mode, which would then get + // clobbered by `undefined` when the query resolves. Also note that we check that the closest + // tab matches the current one so that we don't pick up labels from nested tabs. + if (value && value._closestTab === this) { this._templateLabel = value; } } diff --git a/tools/public_api_guard/material/tabs.md b/tools/public_api_guard/material/tabs.md index b4dcaf4fbe6b..c7e52a211b57 100644 --- a/tools/public_api_guard/material/tabs.md +++ b/tools/public_api_guard/material/tabs.md @@ -58,6 +58,9 @@ export const _MAT_INK_BAR_POSITIONER: InjectionToken<_MatInkBarPositioner>; // @public function _MAT_INK_BAR_POSITIONER_FACTORY(): _MatInkBarPositioner; +// @public +export const MAT_TAB: InjectionToken; + // @public const MAT_TAB_CONTENT: InjectionToken; @@ -114,7 +117,7 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges ngOnInit(): void; origin: number | null; position: number | null; - protected _setTemplateLabelInput(value: MatTabLabel): void; + protected _setTemplateLabelInput(value: MatTabLabel | undefined): void; readonly _stateChanges: Subject; get templateLabel(): MatTabLabel; set templateLabel(value: MatTabLabel); @@ -316,10 +319,13 @@ export type MatTabHeaderPosition = 'above' | 'below'; // @public export class MatTabLabel extends CdkPortal { + constructor(templateRef: TemplateRef, viewContainerRef: ViewContainerRef, _closestTab: any); + // (undocumented) + _closestTab: any; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public