Skip to content

Commit d9ed100

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

File tree

7 files changed

+373
-58
lines changed

7 files changed

+373
-58
lines changed

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
[attr.aria-posinset]="i + 1"
1616
[attr.aria-setsize]="_tabs.length"
1717
[attr.aria-controls]="_getTabContentId(i)"
18-
[attr.aria-selected]="selectedIndex == i"
18+
[attr.aria-selected]="selectedIndex === i"
1919
[attr.aria-label]="tab.ariaLabel || null"
2020
[attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null"
21-
[class.mdc-tab--active]="selectedIndex == i"
21+
[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: 138 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', () => {
@@ -404,11 +404,16 @@ describe('MDC-based MatTabGroup', () => {
404404

405405
expect(tab.getAttribute('aria-label')).toBe('Fruit');
406406
expect(tab.hasAttribute('aria-labelledby')).toBe(false);
407+
408+
fixture.componentInstance.ariaLabel = 'Veggie';
409+
fixture.detectChanges();
410+
expect(tab.getAttribute('aria-label')).toBe('Veggie');
407411
});
408412
});
409413

410414
describe('disable tabs', () => {
411415
let fixture: ComponentFixture<DisabledTabsTestApp>;
416+
412417
beforeEach(() => {
413418
fixture = TestBed.createComponent(DisabledTabsTestApp);
414419
});
@@ -482,7 +487,6 @@ describe('MDC-based MatTabGroup', () => {
482487
expect(tabs[0].origin).toBeLessThan(0);
483488
}));
484489

485-
486490
it('should update selected index if the last tab removed while selected', fakeAsync(() => {
487491
const component: MatTabGroup =
488492
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;
@@ -500,7 +504,6 @@ describe('MDC-based MatTabGroup', () => {
500504
expect(component.selectedIndex).toBe(numberOfTabs - 2);
501505
}));
502506

503-
504507
it('should maintain the selected tab if a new tab is added', () => {
505508
fixture.detectChanges();
506509
const component: MatTabGroup =
@@ -517,7 +520,6 @@ describe('MDC-based MatTabGroup', () => {
517520
expect(component._tabs.toArray()[2].isActive).toBe(true);
518521
});
519522

520-
521523
it('should maintain the selected tab if a tab is removed', () => {
522524
// Select the second tab.
523525
fixture.componentInstance.selectedIndex = 1;
@@ -565,7 +567,6 @@ describe('MDC-based MatTabGroup', () => {
565567

566568
expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled();
567569
}));
568-
569570
});
570571

571572
describe('async tabs', () => {
@@ -756,6 +757,100 @@ describe('MDC-based MatTabGroup', () => {
756757
}));
757758
});
758759

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

1033+
9381034
@Component({
9391035
template: `
9401036
<mat-tab-group class="tab-group"
@@ -965,6 +1061,7 @@ class SimpleDynamicTabsTestApp {
9651061
}
9661062
}
9671063

1064+
9681065
@Component({
9691066
template: `
9701067
<mat-tab-group class="tab-group" [(selectedIndex)]="selectedIndex">
@@ -990,8 +1087,8 @@ class BindedTabsTestApp {
9901087
}
9911088
}
9921089

1090+
9931091
@Component({
994-
selector: 'test-app',
9951092
template: `
9961093
<mat-tab-group class="tab-group">
9971094
<mat-tab>
@@ -1014,6 +1111,7 @@ class DisabledTabsTestApp {
10141111
isDisabled = false;
10151112
}
10161113

1114+
10171115
@Component({
10181116
template: `
10191117
<mat-tab-group class="tab-group">
@@ -1059,7 +1157,6 @@ class TabGroupWithSimpleApi {
10591157

10601158

10611159
@Component({
1062-
selector: 'nested-tabs',
10631160
template: `
10641161
<mat-tab-group>
10651162
<mat-tab label="One">Tab one content</mat-tab>
@@ -1077,8 +1174,8 @@ class NestedTabs {
10771174
@ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
10781175
}
10791176

1177+
10801178
@Component({
1081-
selector: 'template-tabs',
10821179
template: `
10831180
<mat-tab-group>
10841181
<mat-tab label="One">
@@ -1091,11 +1188,11 @@ class NestedTabs {
10911188
</mat-tab>
10921189
</mat-tab-group>
10931190
`,
1094-
})
1095-
class TemplateTabs {}
1191+
})
1192+
class TemplateTabs {}
10961193

10971194

1098-
@Component({
1195+
@Component({
10991196
template: `
11001197
<mat-tab-group>
11011198
<mat-tab [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"></mat-tab>
@@ -1160,6 +1257,7 @@ class TabGroupWithInkBarFitToContent {
11601257
fitInkBarToContent = true;
11611258
}
11621259

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

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
ViewEncapsulation,
1313
TemplateRef,
1414
ContentChild,
15+
OnChanges,
16+
SimpleChanges,
1517
} from '@angular/core';
1618
import {MatTab as BaseMatTab, MAT_TAB} from '@angular/material/tabs';
1719
import {MatTabContent} from './tab-content';
@@ -30,7 +32,7 @@ import {MatTabLabel} from './tab-label';
3032
exportAs: 'matTab',
3133
providers: [{provide: MAT_TAB, useExisting: MatTab}]
3234
})
33-
export class MatTab extends BaseMatTab {
35+
export class MatTab extends BaseMatTab implements OnChanges {
3436
/**
3537
* Template provided in the tab content that will be used if present, used to enable lazy-loading
3638
*/
@@ -41,4 +43,16 @@ export class MatTab extends BaseMatTab {
4143
@ContentChild(MatTabLabel)
4244
override get templateLabel(): MatTabLabel { return this._templateLabel; }
4345
override set templateLabel(value: MatTabLabel) { this._setTemplateLabelInput(value); }
46+
47+
override ngOnChanges(changes: SimpleChanges): void {
48+
super.ngOnChanges(changes);
49+
50+
// Triggering ChangeDetectorRef.markForCheck()
51+
if (changes.hasOwnProperty('ariaLabel')
52+
|| changes.hasOwnProperty('ariaLabelledby')
53+
|| changes.hasOwnProperty('labelClass')
54+
|| changes.hasOwnProperty('bodyClass')) {
55+
this._stateChanges.next();
56+
}
57+
}
4458
}

0 commit comments

Comments
 (0)