Skip to content

Commit 04df98b

Browse files
committed
feat(material/tabs): label & body classes
closes #23685, #9290, #15997
1 parent 7c16258 commit 04df98b

File tree

4 files changed

+197
-25
lines changed

4 files changed

+197
-25
lines changed

src/material/tabs/tab-group.html

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
[disablePagination]="disablePagination"
55
(indexFocused)="_focusChanged($event)"
66
(selectFocusedIndex)="selectedIndex = $event">
7-
<div class="mat-tab-label mat-focus-indicator" role="tab" matTabLabelWrapper mat-ripple cdkMonitorElementFocus
7+
<div class="mat-tab-label mat-focus-indicator" role="tab" matTabLabelWrapper mat-ripple
8+
cdkMonitorElementFocus
89
*ngFor="let tab of _tabs; let i = index"
910
[id]="_getTabLabelId(i)"
1011
[attr.tabIndex]="_getTabIndex(tab, i)"
@@ -15,6 +16,7 @@
1516
[attr.aria-label]="tab.ariaLabel || null"
1617
[attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null"
1718
[class.mat-tab-label-active]="selectedIndex == i"
19+
[ngClass]="tab.labelClassList"
1820
[disabled]="tab.disabled"
1921
[matRippleDisabled]="tab.disabled || disableRipple"
2022
(click)="_handleClick(tab, tabHeader, i)"
@@ -23,12 +25,12 @@
2325

2426
<div class="mat-tab-label-content">
2527
<!-- If there is a label template, use it. -->
26-
<ng-template [ngIf]="tab.templateLabel">
28+
<ng-template [ngIf]="tab.templateLabel" [ngIfElse]="tabTextLabel">
2729
<ng-template [cdkPortalOutlet]="tab.templateLabel"></ng-template>
2830
</ng-template>
2931

3032
<!-- If there is not a label template, fall back to the text label. -->
31-
<ng-template [ngIf]="!tab.templateLabel">{{tab.textLabel}}</ng-template>
33+
<ng-template #tabTextLabel>{{tab.textLabel}}</ng-template>
3234
</div>
3335
</div>
3436
</mat-tab-header>
@@ -38,16 +40,17 @@
3840
[class._mat-animation-noopable]="_animationMode === 'NoopAnimations'"
3941
#tabBodyWrapper>
4042
<mat-tab-body role="tabpanel"
41-
*ngFor="let tab of _tabs; let i = index"
42-
[id]="_getTabContentId(i)"
43-
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
44-
[attr.aria-labelledby]="_getTabLabelId(i)"
45-
[class.mat-tab-body-active]="selectedIndex === i"
46-
[content]="tab.content!"
47-
[position]="tab.position!"
48-
[origin]="tab.origin"
49-
[animationDuration]="animationDuration"
50-
(_onCentered)="_removeTabBodyWrapperHeight()"
51-
(_onCentering)="_setTabBodyWrapperHeight($event)">
43+
*ngFor="let tab of _tabs; let i = index"
44+
[id]="_getTabContentId(i)"
45+
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
46+
[attr.aria-labelledby]="_getTabLabelId(i)"
47+
[class.mat-tab-body-active]="selectedIndex === i"
48+
[ngClass]="tab.bodyClassList"
49+
[content]="tab.content!"
50+
[position]="tab.position!"
51+
[origin]="tab.origin"
52+
[animationDuration]="animationDuration"
53+
(_onCentered)="_removeTabBodyWrapperHeight()"
54+
(_onCentering)="_setTabBodyWrapperHeight($event)">
5255
</mat-tab-body>
5356
</div>

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

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {LEFT_ARROW} from '@angular/cdk/keycodes';
22
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
3-
import {Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
3+
import {Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
44
import {
55
waitForAsync,
66
ComponentFixture,
@@ -41,6 +41,7 @@ describe('MatTabGroup', () => {
4141
TabGroupWithIndirectDescendantTabs,
4242
TabGroupWithSpaceAbove,
4343
NestedTabGroupWithLabel,
44+
TabsWithClassesTestApp,
4445
],
4546
});
4647

@@ -408,6 +409,7 @@ describe('MatTabGroup', () => {
408409

409410
describe('disable tabs', () => {
410411
let fixture: ComponentFixture<DisabledTabsTestApp>;
412+
411413
beforeEach(() => {
412414
fixture = TestBed.createComponent(DisabledTabsTestApp);
413415
});
@@ -481,7 +483,6 @@ describe('MatTabGroup', () => {
481483
expect(tabs[0].origin).toBeLessThan(0);
482484
}));
483485

484-
485486
it('should update selected index if the last tab removed while selected', fakeAsync(() => {
486487
const component: MatTabGroup =
487488
fixture.debugElement.query(By.css('mat-tab-group'))!.componentInstance;
@@ -499,7 +500,6 @@ describe('MatTabGroup', () => {
499500
expect(component.selectedIndex).toBe(numberOfTabs - 2);
500501
}));
501502

502-
503503
it('should maintain the selected tab if a new tab is added', () => {
504504
fixture.detectChanges();
505505
const component: MatTabGroup =
@@ -516,7 +516,6 @@ describe('MatTabGroup', () => {
516516
expect(component._tabs.toArray()[2].isActive).toBe(true);
517517
});
518518

519-
520519
it('should maintain the selected tab if a tab is removed', () => {
521520
// Select the second tab.
522521
fixture.componentInstance.selectedIndex = 1;
@@ -564,7 +563,6 @@ describe('MatTabGroup', () => {
564563

565564
expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled();
566565
}));
567-
568566
});
569567

570568
describe('async tabs', () => {
@@ -756,6 +754,100 @@ describe('MatTabGroup', () => {
756754
}));
757755
});
758756

757+
describe('tabs with custom css classes', () => {
758+
let fixture: ComponentFixture<TabsWithClassesTestApp>;
759+
760+
beforeEach(() => {
761+
fixture = TestBed.createComponent(TabsWithClassesTestApp);
762+
});
763+
764+
it('should apply label classes', () => {
765+
fixture.detectChanges();
766+
767+
const labelElements = fixture.debugElement
768+
.queryAll(By.css('.mat-tab-label.hardcoded.label.classes'));
769+
expect(labelElements.length).toBe(1);
770+
});
771+
772+
it('should apply body classes', () => {
773+
fixture.detectChanges();
774+
775+
const bodyElements = fixture.debugElement
776+
.queryAll(By.css('mat-tab-body.hardcoded.body.classes'));
777+
expect(bodyElements.length).toBe(1);
778+
});
779+
780+
it('should set classes as strings dynamically', () => {
781+
fixture.detectChanges();
782+
let labelElements: DebugElement[];
783+
let bodyElements: DebugElement[];
784+
785+
labelElements = fixture.debugElement
786+
.queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class'));
787+
bodyElements = fixture.debugElement
788+
.queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class'));
789+
expect(labelElements.length).toBe(0);
790+
expect(bodyElements.length).toBe(0);
791+
792+
fixture.componentInstance.labelClassList = 'custom-label-class one-more-label-class';
793+
fixture.componentInstance.bodyClassList = 'custom-body-class one-more-body-class';
794+
fixture.detectChanges();
795+
796+
labelElements = fixture.debugElement
797+
.queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class'));
798+
bodyElements = fixture.debugElement
799+
.queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class'));
800+
expect(labelElements.length).toBe(2);
801+
expect(bodyElements.length).toBe(2);
802+
803+
delete fixture.componentInstance.labelClassList;
804+
delete fixture.componentInstance.bodyClassList;
805+
fixture.detectChanges();
806+
807+
labelElements = fixture.debugElement
808+
.queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class'));
809+
bodyElements = fixture.debugElement
810+
.queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class'));
811+
expect(labelElements.length).toBe(0);
812+
expect(bodyElements.length).toBe(0);
813+
});
814+
815+
it('should set classes as strings array dynamically', () => {
816+
fixture.detectChanges();
817+
let labelElements: DebugElement[];
818+
let bodyElements: DebugElement[];
819+
820+
labelElements = fixture.debugElement
821+
.queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class'));
822+
bodyElements = fixture.debugElement
823+
.queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class'));
824+
expect(labelElements.length).toBe(0);
825+
expect(bodyElements.length).toBe(0);
826+
827+
fixture.componentInstance.labelClassList = ['custom-label-class', 'one-more-label-class'];
828+
fixture.componentInstance.bodyClassList = ['custom-body-class', 'one-more-body-class'];
829+
fixture.detectChanges();
830+
831+
labelElements = fixture.debugElement
832+
.queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class'));
833+
bodyElements = fixture.debugElement
834+
.queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class'));
835+
expect(labelElements.length).toBe(2);
836+
expect(bodyElements.length).toBe(2);
837+
838+
delete fixture.componentInstance.labelClassList;
839+
delete fixture.componentInstance.bodyClassList;
840+
fixture.detectChanges();
841+
842+
labelElements = fixture.debugElement
843+
.queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class'));
844+
bodyElements = fixture.debugElement
845+
.queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class'));
846+
expect(labelElements.length).toBe(0);
847+
expect(bodyElements.length).toBe(0);
848+
});
849+
});
850+
759851
/**
760852
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
761853
* respective `active` classes
@@ -881,6 +973,7 @@ class SimpleTabsTestApp {
881973
animationDone() { }
882974
}
883975

976+
884977
@Component({
885978
template: `
886979
<mat-tab-group class="tab-group"
@@ -911,6 +1004,7 @@ class SimpleDynamicTabsTestApp {
9111004
}
9121005
}
9131006

1007+
9141008
@Component({
9151009
template: `
9161010
<mat-tab-group class="tab-group" [(selectedIndex)]="selectedIndex">
@@ -936,8 +1030,8 @@ class BindedTabsTestApp {
9361030
}
9371031
}
9381032

1033+
9391034
@Component({
940-
selector: 'test-app',
9411035
template: `
9421036
<mat-tab-group class="tab-group">
9431037
<mat-tab>
@@ -960,6 +1054,7 @@ class DisabledTabsTestApp {
9601054
isDisabled = false;
9611055
}
9621056

1057+
9631058
@Component({
9641059
template: `
9651060
<mat-tab-group class="tab-group">
@@ -1023,6 +1118,7 @@ class NestedTabs {
10231118
@ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
10241119
}
10251120

1121+
10261122
@Component({
10271123
selector: 'template-tabs',
10281124
template: `
@@ -1037,11 +1133,11 @@ class NestedTabs {
10371133
</mat-tab>
10381134
</mat-tab-group>
10391135
`,
1040-
})
1041-
class TemplateTabs {}
1136+
})
1137+
class TemplateTabs {}
10421138

10431139

1044-
@Component({
1140+
@Component({
10451141
template: `
10461142
<mat-tab-group>
10471143
<mat-tab [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"></mat-tab>
@@ -1093,6 +1189,7 @@ class TabGroupWithIndirectDescendantTabs {
10931189
@ViewChild(MatTabGroup) tabGroup: MatTabGroup;
10941190
}
10951191

1192+
10961193
@Component({
10971194
template: `
10981195
<div style="height: 300px; background-color: aqua">
@@ -1135,3 +1232,31 @@ class TabGroupWithSpaceAbove {
11351232
})
11361233
class NestedTabGroupWithLabel {
11371234
}
1235+
1236+
1237+
@Component({
1238+
template: `
1239+
<mat-tab-group class="tab-group">
1240+
<mat-tab label="Tab One">
1241+
Tab one content
1242+
</mat-tab>
1243+
<mat-tab label="Tab Two" [class]="labelClassList">
1244+
Tab two content
1245+
</mat-tab>
1246+
<mat-tab label="Tab Three" [bodyClass]="bodyClassList">
1247+
Tab three content
1248+
</mat-tab>
1249+
<mat-tab label="Tab Four" [class]="labelClassList" [bodyClass]="bodyClassList">
1250+
Tab four content
1251+
</mat-tab>
1252+
<mat-tab label="Tab Five" class="hardcoded label classes" bodyClass="hardcoded body classes">
1253+
Tab five content
1254+
</mat-tab>
1255+
</mat-tab-group>
1256+
`,
1257+
})
1258+
class TabsWithClassesTestApp {
1259+
@ViewChildren(MatTab) tabs: QueryList<MatTab>;
1260+
labelClassList?: string | string[];
1261+
bodyClassList?: string | string[];
1262+
}

src/material/tabs/tab.ts

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

9-
import {BooleanInput} from '@angular/cdk/coercion';
9+
import {BooleanInput, coerceStringArray} from '@angular/cdk/coercion';
1010
import {TemplatePortal} from '@angular/cdk/portal';
1111
import {
1212
ChangeDetectionStrategy,
@@ -79,6 +79,40 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
7979
*/
8080
@Input('aria-labelledby') ariaLabelledby: string;
8181

82+
/**
83+
* Takes classes set on the host mat-tab element and applies them to the tab
84+
* label inside the mat-tab-header container to allow for easy styling.
85+
*/
86+
@Input('class')
87+
set labelClass(value: string | string[]) {
88+
if (value && value.length) {
89+
this.labelClassList = coerceStringArray(value).reduce((classList, className) => {
90+
classList[className] = true;
91+
return classList;
92+
}, {} as {[key: string]: boolean});
93+
} else {
94+
this.labelClassList = {};
95+
}
96+
}
97+
labelClassList: {[key: string]: boolean} = {};
98+
99+
/**
100+
* Takes classes set on the host mat-tab element and applies them to the tab
101+
* label inside the mat-tab-body container to allow for easy styling.
102+
*/
103+
@Input()
104+
set bodyClass(value: string | string[]) {
105+
if (value && value.length) {
106+
this.bodyClassList = coerceStringArray(value).reduce((classList, className) => {
107+
classList[className] = true;
108+
return classList;
109+
}, {} as {[key: string]: boolean});
110+
} else {
111+
this.bodyClassList = {};
112+
}
113+
}
114+
bodyClassList: {[key: string]: boolean} = {};
115+
82116
/** Portal that will be the hosted content of the tab */
83117
private _contentPortal: TemplatePortal | null = null;
84118

tools/public_api_guard/material/tabs.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,22 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
101101
constructor(_viewContainerRef: ViewContainerRef, _closestTabGroup: any);
102102
ariaLabel: string;
103103
ariaLabelledby: string;
104+
set bodyClass(value: string | string[]);
105+
// (undocumented)
106+
bodyClassList: {
107+
[key: string]: boolean;
108+
};
104109
// (undocumented)
105110
_closestTabGroup: any;
106111
get content(): TemplatePortal | null;
107112
_explicitContent: TemplateRef<any>;
108113
_implicitContent: TemplateRef<any>;
109114
isActive: boolean;
115+
set labelClass(value: string | string[]);
116+
// (undocumented)
117+
labelClassList: {
118+
[key: string]: boolean;
119+
};
110120
// (undocumented)
111121
static ngAcceptInputType_disabled: BooleanInput;
112122
// (undocumented)
@@ -125,7 +135,7 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
125135
protected _templateLabel: MatTabLabel;
126136
textLabel: string;
127137
// (undocumented)
128-
static ɵcmp: i0.ɵɵComponentDeclaration<MatTab, "mat-tab", ["matTab"], { "disabled": "disabled"; "textLabel": "label"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; }, {}, ["templateLabel", "_explicitContent"], ["*"]>;
138+
static ɵcmp: i0.ɵɵComponentDeclaration<MatTab, "mat-tab", ["matTab"], { "disabled": "disabled"; "textLabel": "label"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "labelClass": "class"; "bodyClass": "bodyClass"; }, {}, ["templateLabel", "_explicitContent"], ["*"]>;
129139
// (undocumented)
130140
static ɵfac: i0.ɵɵFactoryDeclaration<MatTab, [null, { optional: true; }]>;
131141
}

0 commit comments

Comments
 (0)