Skip to content

Commit f10d245

Browse files
authoredJan 15, 2022
feat(material/tabs): label & body classes (#23691)
closes #23685, #9290, #15997
1 parent 653e46b commit f10d245

File tree

11 files changed

+201
-23
lines changed

11 files changed

+201
-23
lines changed
 

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export class MatTabBodyPortal extends BaseMatTabBodyPortal {
5656
templateUrl: 'tab-body.html',
5757
styleUrls: ['tab-body.css'],
5858
encapsulation: ViewEncapsulation.None,
59-
changeDetection: ChangeDetectionStrategy.OnPush,
59+
// tslint:disable-next-line:validate-decorators
60+
changeDetection: ChangeDetectionStrategy.Default,
6061
animations: [matTabsAnimations.translateTab],
6162
host: {
6263
'class': 'mat-mdc-tab-body',

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

+6-4
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.labelClass"
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>
@@ -57,6 +58,7 @@
5758
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
5859
[attr.aria-labelledby]="_getTabLabelId(i)"
5960
[class.mat-mdc-tab-body-active]="selectedIndex === i"
61+
[ngClass]="tab.bodyClass"
6062
[content]="tab.content!"
6163
[position]="tab.position!"
6264
[origin]="tab.origin"

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

+81-4
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,
@@ -40,6 +40,7 @@ describe('MDC-based MatTabGroup', () => {
4040
TabGroupWithIndirectDescendantTabs,
4141
TabGroupWithSpaceAbove,
4242
NestedTabGroupWithLabel,
43+
TabsWithClassesTestApp,
4344
],
4445
});
4546

@@ -420,11 +421,16 @@ describe('MDC-based MatTabGroup', () => {
420421

421422
expect(tab.getAttribute('aria-label')).toBe('Fruit');
422423
expect(tab.hasAttribute('aria-labelledby')).toBe(false);
424+
425+
fixture.componentInstance.ariaLabel = 'Veggie';
426+
fixture.detectChanges();
427+
expect(tab.getAttribute('aria-label')).toBe('Veggie');
423428
});
424429
});
425430

426431
describe('disable tabs', () => {
427432
let fixture: ComponentFixture<DisabledTabsTestApp>;
433+
428434
beforeEach(() => {
429435
fixture = TestBed.createComponent(DisabledTabsTestApp);
430436
});
@@ -780,6 +786,62 @@ describe('MDC-based MatTabGroup', () => {
780786
}));
781787
});
782788

789+
describe('tabs with custom css classes', () => {
790+
let fixture: ComponentFixture<TabsWithClassesTestApp>;
791+
let labelElements: DebugElement[];
792+
let bodyElements: DebugElement[];
793+
794+
beforeEach(() => {
795+
fixture = TestBed.createComponent(TabsWithClassesTestApp);
796+
fixture.detectChanges();
797+
labelElements = fixture.debugElement.queryAll(By.css('.mdc-tab'));
798+
bodyElements = fixture.debugElement.queryAll(By.css('mat-tab-body'));
799+
});
800+
801+
it('should apply label/body classes', () => {
802+
expect(labelElements[1].nativeElement.classList).toContain('hardcoded-label-class');
803+
expect(bodyElements[1].nativeElement.classList).toContain('hardcoded-body-class');
804+
});
805+
806+
it('should set classes as strings dynamically', () => {
807+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
808+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
809+
810+
fixture.componentInstance.labelClassList = 'custom-label-class';
811+
fixture.componentInstance.bodyClassList = 'custom-body-class';
812+
fixture.detectChanges();
813+
814+
expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
815+
expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
816+
817+
delete fixture.componentInstance.labelClassList;
818+
delete fixture.componentInstance.bodyClassList;
819+
fixture.detectChanges();
820+
821+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
822+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
823+
});
824+
825+
it('should set classes as strings array dynamically', () => {
826+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
827+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
828+
829+
fixture.componentInstance.labelClassList = ['custom-label-class'];
830+
fixture.componentInstance.bodyClassList = ['custom-body-class'];
831+
fixture.detectChanges();
832+
833+
expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
834+
expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
835+
836+
delete fixture.componentInstance.labelClassList;
837+
delete fixture.componentInstance.bodyClassList;
838+
fixture.detectChanges();
839+
840+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
841+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
842+
});
843+
});
844+
783845
/**
784846
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
785847
* respective `active` classes
@@ -1014,7 +1076,6 @@ class BindedTabsTestApp {
10141076
}
10151077

10161078
@Component({
1017-
selector: 'test-app',
10181079
template: `
10191080
<mat-tab-group class="tab-group">
10201081
<mat-tab>
@@ -1080,7 +1141,6 @@ class TabGroupWithSimpleApi {
10801141
}
10811142

10821143
@Component({
1083-
selector: 'nested-tabs',
10841144
template: `
10851145
<mat-tab-group>
10861146
<mat-tab label="One">Tab one content</mat-tab>
@@ -1099,7 +1159,6 @@ class NestedTabs {
10991159
}
11001160

11011161
@Component({
1102-
selector: 'template-tabs',
11031162
template: `
11041163
<mat-tab-group>
11051164
<mat-tab label="One">
@@ -1215,3 +1274,21 @@ class TabGroupWithSpaceAbove {
12151274
`,
12161275
})
12171276
class NestedTabGroupWithLabel {}
1277+
1278+
@Component({
1279+
template: `
1280+
<mat-tab-group class="tab-group">
1281+
<mat-tab label="Tab One" [labelClass]="labelClassList" [bodyClass]="bodyClassList">
1282+
Tab one content
1283+
</mat-tab>
1284+
<mat-tab label="Tab Two" labelClass="hardcoded-label-class"
1285+
bodyClass="hardcoded-body-class">
1286+
Tab two content
1287+
</mat-tab>
1288+
</mat-tab-group>
1289+
`,
1290+
})
1291+
class TabsWithClassesTestApp {
1292+
labelClassList?: string | string[];
1293+
bodyClassList?: string | string[];
1294+
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
4141
templateUrl: 'tab-group.html',
4242
styleUrls: ['tab-group.css'],
4343
encapsulation: ViewEncapsulation.None,
44-
changeDetection: ChangeDetectionStrategy.OnPush,
44+
// tslint:disable-next-line:validate-decorators
45+
changeDetection: ChangeDetectionStrategy.Default,
4546
inputs: ['color', 'disableRipple'],
4647
providers: [
4748
{

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ import {MatInkBar} from './ink-bar';
4242
inputs: ['selectedIndex'],
4343
outputs: ['selectFocusedIndex', 'indexFocused'],
4444
encapsulation: ViewEncapsulation.None,
45-
changeDetection: ChangeDetectionStrategy.OnPush,
45+
// tslint:disable-next-line:validate-decorators
46+
changeDetection: ChangeDetectionStrategy.Default,
4647
host: {
4748
'class': 'mat-mdc-tab-header',
4849
'[class.mat-mdc-tab-header-pagination-controls-enabled]': '_showPaginationControls',

‎src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ import {takeUntil} from 'rxjs/operators';
6666
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
6767
},
6868
encapsulation: ViewEncapsulation.None,
69-
changeDetection: ChangeDetectionStrategy.OnPush,
69+
// tslint:disable-next-line:validate-decorators
70+
changeDetection: ChangeDetectionStrategy.Default,
7071
})
7172
export class MatTabNav extends _MatTabNavBase implements AfterContentInit {
7273
/** Whether the ink bar should fit its width to the size of the tab label content. */

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {MatTabLabel} from './tab-label';
2525
// that creating the extra class will generate more code than just duplicating the template.
2626
templateUrl: 'tab.html',
2727
inputs: ['disabled'],
28-
changeDetection: ChangeDetectionStrategy.OnPush,
28+
// tslint:disable-next-line:validate-decorators
29+
changeDetection: ChangeDetectionStrategy.Default,
2930
encapsulation: ViewEncapsulation.None,
3031
exportAs: 'matTab',
3132
providers: [{provide: MAT_TAB, useExisting: MatTab}],

‎src/material/tabs/tab-group.html

+8-5
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@
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)"
1112
[attr.aria-posinset]="i + 1"
1213
[attr.aria-setsize]="_tabs.length"
1314
[attr.aria-controls]="_getTabContentId(i)"
14-
[attr.aria-selected]="selectedIndex == i"
15+
[attr.aria-selected]="selectedIndex === i"
1516
[attr.aria-label]="tab.ariaLabel || null"
1617
[attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null"
17-
[class.mat-tab-label-active]="selectedIndex == i"
18+
[class.mat-tab-label-active]="selectedIndex === i"
19+
[ngClass]="tab.labelClass"
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>
@@ -43,6 +45,7 @@
4345
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
4446
[attr.aria-labelledby]="_getTabLabelId(i)"
4547
[class.mat-tab-body-active]="selectedIndex === i"
48+
[ngClass]="tab.bodyClass"
4649
[content]="tab.content!"
4750
[position]="tab.position!"
4851
[origin]="tab.origin"

0 commit comments

Comments
 (0)
Please sign in to comment.