Skip to content

Commit b6fbd97

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

File tree

6 files changed

+346
-53
lines changed

6 files changed

+346
-53
lines changed

src/material-experimental/mdc-tabs/tab-group.html

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
[attr.aria-label]="tab.ariaLabel || null"
2020
[attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null"
2121
[class.mdc-tab--active]="selectedIndex == i"
22+
[ngClass]="tab.labelClassList"
2223
[disabled]="tab.disabled"
2324
[fitInkBarToContent]="fitInkBarToContent"
2425
(click)="_handleClick(tab, tabHeader, i)"
@@ -36,12 +37,12 @@
3637
<span class="mdc-tab__content">
3738
<span class="mdc-tab__text-label">
3839
<!-- If there is a label template, use it. -->
39-
<ng-template [ngIf]="tab.templateLabel">
40+
<ng-template [ngIf]="tab.templateLabel" [ngIfElse]="tabTextLabel">
4041
<ng-template [cdkPortalOutlet]="tab.templateLabel"></ng-template>
4142
</ng-template>
4243

4344
<!-- If there is not a label template, fall back to the text label. -->
44-
<ng-template [ngIf]="!tab.templateLabel">{{tab.textLabel}}</ng-template>
45+
<ng-template #tabTextLabel>{{tab.textLabel}}</ng-template>
4546
</span>
4647
</span>
4748
</div>
@@ -52,16 +53,17 @@
5253
[class._mat-animation-noopable]="_animationMode === 'NoopAnimations'"
5354
#tabBodyWrapper>
5455
<mat-tab-body role="tabpanel"
55-
*ngFor="let tab of _tabs; let i = index"
56-
[id]="_getTabContentId(i)"
57-
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
58-
[attr.aria-labelledby]="_getTabLabelId(i)"
59-
[class.mat-mdc-tab-body-active]="selectedIndex === i"
60-
[content]="tab.content!"
61-
[position]="tab.position!"
62-
[origin]="tab.origin"
63-
[animationDuration]="animationDuration"
64-
(_onCentered)="_removeTabBodyWrapperHeight()"
65-
(_onCentering)="_setTabBodyWrapperHeight($event)">
56+
*ngFor="let tab of _tabs; let i = index"
57+
[id]="_getTabContentId(i)"
58+
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
59+
[attr.aria-labelledby]="_getTabLabelId(i)"
60+
[class.mat-mdc-tab-body-active]="selectedIndex === i"
61+
[ngClass]="tab.bodyClassList"
62+
[content]="tab.content!"
63+
[position]="tab.position!"
64+
[origin]="tab.origin"
65+
[animationDuration]="animationDuration"
66+
(_onCentered)="_removeTabBodyWrapperHeight()"
67+
(_onCentering)="_setTabBodyWrapperHeight($event)">
6668
</mat-tab-body>
6769
</div>

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

Lines changed: 134 additions & 12 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('MDC-based MatTabGroup', () => {
4141
TabGroupWithIndirectDescendantTabs,
4242
TabGroupWithSpaceAbove,
4343
NestedTabGroupWithLabel,
44+
TabsWithClassesTestApp,
4445
],
4546
});
4647

@@ -364,7 +365,6 @@ describe('MDC-based MatTabGroup', () => {
364365

365366
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
366367
});
367-
368368
});
369369

370370
describe('aria labelling', () => {
@@ -409,6 +409,7 @@ describe('MDC-based MatTabGroup', () => {
409409

410410
describe('disable tabs', () => {
411411
let fixture: ComponentFixture<DisabledTabsTestApp>;
412+
412413
beforeEach(() => {
413414
fixture = TestBed.createComponent(DisabledTabsTestApp);
414415
});
@@ -482,7 +483,6 @@ describe('MDC-based MatTabGroup', () => {
482483
expect(tabs[0].origin).toBeLessThan(0);
483484
}));
484485

485-
486486
it('should update selected index if the last tab removed while selected', fakeAsync(() => {
487487
const component: MatTabGroup =
488488
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;
@@ -500,7 +500,6 @@ describe('MDC-based MatTabGroup', () => {
500500
expect(component.selectedIndex).toBe(numberOfTabs - 2);
501501
}));
502502

503-
504503
it('should maintain the selected tab if a new tab is added', () => {
505504
fixture.detectChanges();
506505
const component: MatTabGroup =
@@ -517,7 +516,6 @@ describe('MDC-based MatTabGroup', () => {
517516
expect(component._tabs.toArray()[2].isActive).toBe(true);
518517
});
519518

520-
521519
it('should maintain the selected tab if a tab is removed', () => {
522520
// Select the second tab.
523521
fixture.componentInstance.selectedIndex = 1;
@@ -565,7 +563,6 @@ describe('MDC-based MatTabGroup', () => {
565563

566564
expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled();
567565
}));
568-
569566
});
570567

571568
describe('async tabs', () => {
@@ -756,6 +753,100 @@ describe('MDC-based MatTabGroup', () => {
756753
}));
757754
});
758755

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

1029+
9381030
@Component({
9391031
template: `
9401032
<mat-tab-group class="tab-group"
@@ -965,6 +1057,7 @@ class SimpleDynamicTabsTestApp {
9651057
}
9661058
}
9671059

1060+
9681061
@Component({
9691062
template: `
9701063
<mat-tab-group class="tab-group" [(selectedIndex)]="selectedIndex">
@@ -990,8 +1083,8 @@ class BindedTabsTestApp {
9901083
}
9911084
}
9921085

1086+
9931087
@Component({
994-
selector: 'test-app',
9951088
template: `
9961089
<mat-tab-group class="tab-group">
9971090
<mat-tab>
@@ -1014,6 +1107,7 @@ class DisabledTabsTestApp {
10141107
isDisabled = false;
10151108
}
10161109

1110+
10171111
@Component({
10181112
template: `
10191113
<mat-tab-group class="tab-group">
@@ -1059,7 +1153,6 @@ class TabGroupWithSimpleApi {
10591153

10601154

10611155
@Component({
1062-
selector: 'nested-tabs',
10631156
template: `
10641157
<mat-tab-group>
10651158
<mat-tab label="One">Tab one content</mat-tab>
@@ -1077,8 +1170,8 @@ class NestedTabs {
10771170
@ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
10781171
}
10791172

1173+
10801174
@Component({
1081-
selector: 'template-tabs',
10821175
template: `
10831176
<mat-tab-group>
10841177
<mat-tab label="One">
@@ -1091,11 +1184,11 @@ class NestedTabs {
10911184
</mat-tab>
10921185
</mat-tab-group>
10931186
`,
1094-
})
1095-
class TemplateTabs {}
1187+
})
1188+
class TemplateTabs {}
10961189

10971190

1098-
@Component({
1191+
@Component({
10991192
template: `
11001193
<mat-tab-group>
11011194
<mat-tab [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"></mat-tab>
@@ -1160,6 +1253,7 @@ class TabGroupWithInkBarFitToContent {
11601253
fitInkBarToContent = true;
11611254
}
11621255

1256+
11631257
@Component({
11641258
template: `
11651259
<div style="height: 300px; background-color: aqua">
@@ -1202,3 +1296,31 @@ class TabGroupWithSpaceAbove {
12021296
})
12031297
class NestedTabGroupWithLabel {
12041298
}
1299+
1300+
1301+
@Component({
1302+
template: `
1303+
<mat-tab-group class="tab-group">
1304+
<mat-tab label="Tab One">
1305+
Tab one content
1306+
</mat-tab>
1307+
<mat-tab label="Tab Two" [class]="labelClassList">
1308+
Tab two content
1309+
</mat-tab>
1310+
<mat-tab label="Tab Three" [bodyClass]="bodyClassList">
1311+
Tab three content
1312+
</mat-tab>
1313+
<mat-tab label="Tab Four" [class]="labelClassList" [bodyClass]="bodyClassList">
1314+
Tab four content
1315+
</mat-tab>
1316+
<mat-tab label="Tab Five" class="hardcoded label classes" bodyClass="hardcoded body classes">
1317+
Tab five content
1318+
</mat-tab>
1319+
</mat-tab-group>
1320+
`,
1321+
})
1322+
class TabsWithClassesTestApp {
1323+
@ViewChildren(MatTab) tabs: QueryList<MatTab>;
1324+
labelClassList?: string | string[];
1325+
bodyClassList?: string | string[];
1326+
}

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>

0 commit comments

Comments
 (0)