Skip to content

Commit b2c4492

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

File tree

11 files changed

+221
-39
lines changed

11 files changed

+221
-39
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export class MatTabBodyPortal extends BaseMatTabBodyPortal {
5555
templateUrl: 'tab-body.html',
5656
styleUrls: ['tab-body.css'],
5757
encapsulation: ViewEncapsulation.None,
58-
changeDetection: ChangeDetectionStrategy.OnPush,
58+
// tslint:disable-next-line:validate-decorators
59+
changeDetection: ChangeDetectionStrategy.Default,
5960
animations: [matTabsAnimations.translateTab],
6061
host: {
6162
'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

+91-12
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,62 @@ describe('MDC-based MatTabGroup', () => {
756757
}));
757758
});
758759

760+
describe('tabs with custom css classes', () => {
761+
let fixture: ComponentFixture<TabsWithClassesTestApp>;
762+
let labelElements: DebugElement[];
763+
let bodyElements: DebugElement[];
764+
765+
beforeEach(() => {
766+
fixture = TestBed.createComponent(TabsWithClassesTestApp);
767+
fixture.detectChanges();
768+
labelElements = fixture.debugElement.queryAll(By.css('.mdc-tab'));
769+
bodyElements = fixture.debugElement.queryAll(By.css('mat-tab-body'));
770+
});
771+
772+
it('should apply label/body classes', () => {
773+
expect(labelElements[1].nativeElement.classList).toContain('hardcoded-label-class');
774+
expect(bodyElements[1].nativeElement.classList).toContain('hardcoded-body-class');
775+
});
776+
777+
it('should set classes as strings dynamically', () => {
778+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
779+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
780+
781+
fixture.componentInstance.labelClassList = 'custom-label-class';
782+
fixture.componentInstance.bodyClassList = 'custom-body-class';
783+
fixture.detectChanges();
784+
785+
expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
786+
expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
787+
788+
delete fixture.componentInstance.labelClassList;
789+
delete fixture.componentInstance.bodyClassList;
790+
fixture.detectChanges();
791+
792+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
793+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
794+
});
795+
796+
it('should set classes as strings array dynamically', () => {
797+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
798+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
799+
800+
fixture.componentInstance.labelClassList = ['custom-label-class'];
801+
fixture.componentInstance.bodyClassList = ['custom-body-class'];
802+
fixture.detectChanges();
803+
804+
expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
805+
expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
806+
807+
delete fixture.componentInstance.labelClassList;
808+
delete fixture.componentInstance.bodyClassList;
809+
fixture.detectChanges();
810+
811+
expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
812+
expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
813+
});
814+
});
815+
759816
/**
760817
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
761818
* respective `active` classes
@@ -935,6 +992,7 @@ class SimpleTabsTestApp {
935992
animationDone() { }
936993
}
937994

995+
938996
@Component({
939997
template: `
940998
<mat-tab-group class="tab-group"
@@ -965,6 +1023,7 @@ class SimpleDynamicTabsTestApp {
9651023
}
9661024
}
9671025

1026+
9681027
@Component({
9691028
template: `
9701029
<mat-tab-group class="tab-group" [(selectedIndex)]="selectedIndex">
@@ -990,8 +1049,8 @@ class BindedTabsTestApp {
9901049
}
9911050
}
9921051

1052+
9931053
@Component({
994-
selector: 'test-app',
9951054
template: `
9961055
<mat-tab-group class="tab-group">
9971056
<mat-tab>
@@ -1014,6 +1073,7 @@ class DisabledTabsTestApp {
10141073
isDisabled = false;
10151074
}
10161075

1076+
10171077
@Component({
10181078
template: `
10191079
<mat-tab-group class="tab-group">
@@ -1059,7 +1119,6 @@ class TabGroupWithSimpleApi {
10591119

10601120

10611121
@Component({
1062-
selector: 'nested-tabs',
10631122
template: `
10641123
<mat-tab-group>
10651124
<mat-tab label="One">Tab one content</mat-tab>
@@ -1077,8 +1136,8 @@ class NestedTabs {
10771136
@ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
10781137
}
10791138

1139+
10801140
@Component({
1081-
selector: 'template-tabs',
10821141
template: `
10831142
<mat-tab-group>
10841143
<mat-tab label="One">
@@ -1091,11 +1150,11 @@ class NestedTabs {
10911150
</mat-tab>
10921151
</mat-tab-group>
10931152
`,
1094-
})
1095-
class TemplateTabs {}
1153+
})
1154+
class TemplateTabs {}
10961155

10971156

1098-
@Component({
1157+
@Component({
10991158
template: `
11001159
<mat-tab-group>
11011160
<mat-tab [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"></mat-tab>
@@ -1160,6 +1219,7 @@ class TabGroupWithInkBarFitToContent {
11601219
fitInkBarToContent = true;
11611220
}
11621221

1222+
11631223
@Component({
11641224
template: `
11651225
<div style="height: 300px; background-color: aqua">
@@ -1202,3 +1262,22 @@ class TabGroupWithSpaceAbove {
12021262
})
12031263
class NestedTabGroupWithLabel {
12041264
}
1265+
1266+
1267+
@Component({
1268+
template: `
1269+
<mat-tab-group class="tab-group">
1270+
<mat-tab label="Tab One" [labelClass]="labelClassList" [bodyClass]="bodyClassList">
1271+
Tab one content
1272+
</mat-tab>
1273+
<mat-tab label="Tab Two" labelClass="hardcoded-label-class"
1274+
bodyClass="hardcoded-body-class">
1275+
Tab two content
1276+
</mat-tab>
1277+
</mat-tab-group>
1278+
`,
1279+
})
1280+
class TabsWithClassesTestApp {
1281+
labelClassList?: string | string[];
1282+
bodyClassList?: string | string[];
1283+
}

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
provide: MAT_TAB_GROUP,

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ import {MatInkBar} from './ink-bar';
4343
inputs: ['selectedIndex'],
4444
outputs: ['selectFocusedIndex', 'indexFocused'],
4545
encapsulation: ViewEncapsulation.None,
46-
changeDetection: ChangeDetectionStrategy.OnPush,
46+
// tslint:disable-next-line:validate-decorators
47+
changeDetection: ChangeDetectionStrategy.Default,
4748
host: {
4849
'class': 'mat-mdc-tab-header',
4950
'[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
@@ -64,7 +64,8 @@ import {takeUntil} from 'rxjs/operators';
6464
'[class._mat-animation-noopable]' : '_animationMode === "NoopAnimations"',
6565
},
6666
encapsulation: ViewEncapsulation.None,
67-
changeDetection: ChangeDetectionStrategy.OnPush,
67+
// tslint:disable-next-line:validate-decorators
68+
changeDetection: ChangeDetectionStrategy.Default,
6869
})
6970
export class MatTabNav extends _MatTabNavBase implements AfterContentInit {
7071
/** 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)