Skip to content

Commit 672699e

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

File tree

11 files changed

+269
-39
lines changed

11 files changed

+269
-39
lines changed

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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 6 additions & 4 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.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

Lines changed: 115 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,77 @@ 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 classes', () => {
773+
expect(labelElements[4].nativeElement.classList).toContain('hardcoded-label-class');
774+
});
775+
776+
it('should apply body classes', () => {
777+
expect(bodyElements[4].nativeElement.classList).toContain('hardcoded-body-class');
778+
});
779+
780+
it('should set classes as strings dynamically', () => {
781+
expect(labelElements[1].nativeElement.classList).not.toContain('custom-label-class');
782+
expect(labelElements[3].nativeElement.classList).not.toContain('custom-label-class');
783+
expect(bodyElements[2].nativeElement.classList).not.toContain('custom-body-class');
784+
expect(bodyElements[3].nativeElement.classList).not.toContain('custom-body-class');
785+
786+
fixture.componentInstance.labelClassList = 'custom-label-class';
787+
fixture.componentInstance.bodyClassList = 'custom-body-class';
788+
fixture.detectChanges();
789+
790+
expect(labelElements[1].nativeElement.classList).toContain('custom-label-class');
791+
expect(labelElements[3].nativeElement.classList).toContain('custom-label-class');
792+
expect(bodyElements[2].nativeElement.classList).toContain('custom-body-class');
793+
expect(bodyElements[3].nativeElement.classList).toContain('custom-body-class');
794+
795+
delete fixture.componentInstance.labelClassList;
796+
delete fixture.componentInstance.bodyClassList;
797+
fixture.detectChanges();
798+
799+
expect(labelElements[1].nativeElement.classList).not.toContain('custom-label-class');
800+
expect(labelElements[3].nativeElement.classList).not.toContain('custom-label-class');
801+
expect(bodyElements[2].nativeElement.classList).not.toContain('custom-body-class');
802+
expect(bodyElements[3].nativeElement.classList).not.toContain('custom-body-class');
803+
});
804+
805+
it('should set classes as strings array dynamically', () => {
806+
expect(labelElements[1].nativeElement.classList).not.toContain('custom-label-class');
807+
expect(labelElements[3].nativeElement.classList).not.toContain('custom-label-class');
808+
expect(bodyElements[2].nativeElement.classList).not.toContain('custom-body-class');
809+
expect(bodyElements[3].nativeElement.classList).not.toContain('custom-body-class');
810+
811+
fixture.componentInstance.labelClassList = ['custom-label-class'];
812+
fixture.componentInstance.bodyClassList = ['custom-body-class'];
813+
fixture.detectChanges();
814+
815+
expect(labelElements[1].nativeElement.classList).toContain('custom-label-class');
816+
expect(labelElements[3].nativeElement.classList).toContain('custom-label-class');
817+
expect(bodyElements[2].nativeElement.classList).toContain('custom-body-class');
818+
expect(bodyElements[3].nativeElement.classList).toContain('custom-body-class');
819+
820+
delete fixture.componentInstance.labelClassList;
821+
delete fixture.componentInstance.bodyClassList;
822+
fixture.detectChanges();
823+
824+
expect(labelElements[1].nativeElement.classList).not.toContain('custom-label-class');
825+
expect(labelElements[3].nativeElement.classList).not.toContain('custom-label-class');
826+
expect(bodyElements[2].nativeElement.classList).not.toContain('custom-body-class');
827+
expect(bodyElements[3].nativeElement.classList).not.toContain('custom-body-class');
828+
});
829+
});
830+
759831
/**
760832
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
761833
* respective `active` classes
@@ -935,6 +1007,7 @@ class SimpleTabsTestApp {
9351007
animationDone() { }
9361008
}
9371009

1010+
9381011
@Component({
9391012
template: `
9401013
<mat-tab-group class="tab-group"
@@ -965,6 +1038,7 @@ class SimpleDynamicTabsTestApp {
9651038
}
9661039
}
9671040

1041+
9681042
@Component({
9691043
template: `
9701044
<mat-tab-group class="tab-group" [(selectedIndex)]="selectedIndex">
@@ -990,8 +1064,8 @@ class BindedTabsTestApp {
9901064
}
9911065
}
9921066

1067+
9931068
@Component({
994-
selector: 'test-app',
9951069
template: `
9961070
<mat-tab-group class="tab-group">
9971071
<mat-tab>
@@ -1014,6 +1088,7 @@ class DisabledTabsTestApp {
10141088
isDisabled = false;
10151089
}
10161090

1091+
10171092
@Component({
10181093
template: `
10191094
<mat-tab-group class="tab-group">
@@ -1059,7 +1134,6 @@ class TabGroupWithSimpleApi {
10591134

10601135

10611136
@Component({
1062-
selector: 'nested-tabs',
10631137
template: `
10641138
<mat-tab-group>
10651139
<mat-tab label="One">Tab one content</mat-tab>
@@ -1077,8 +1151,8 @@ class NestedTabs {
10771151
@ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
10781152
}
10791153

1154+
10801155
@Component({
1081-
selector: 'template-tabs',
10821156
template: `
10831157
<mat-tab-group>
10841158
<mat-tab label="One">
@@ -1091,11 +1165,11 @@ class NestedTabs {
10911165
</mat-tab>
10921166
</mat-tab-group>
10931167
`,
1094-
})
1095-
class TemplateTabs {}
1168+
})
1169+
class TemplateTabs {}
10961170

10971171

1098-
@Component({
1172+
@Component({
10991173
template: `
11001174
<mat-tab-group>
11011175
<mat-tab [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"></mat-tab>
@@ -1160,6 +1234,7 @@ class TabGroupWithInkBarFitToContent {
11601234
fitInkBarToContent = true;
11611235
}
11621236

1237+
11631238
@Component({
11641239
template: `
11651240
<div style="height: 300px; background-color: aqua">
@@ -1202,3 +1277,31 @@ class TabGroupWithSpaceAbove {
12021277
})
12031278
class NestedTabGroupWithLabel {
12041279
}
1280+
1281+
1282+
@Component({
1283+
template: `
1284+
<mat-tab-group class="tab-group">
1285+
<mat-tab label="Tab One">
1286+
Tab one content
1287+
</mat-tab>
1288+
<mat-tab label="Tab Two" [labelClass]="labelClassList">
1289+
Tab two content
1290+
</mat-tab>
1291+
<mat-tab label="Tab Three" [bodyClass]="bodyClassList">
1292+
Tab three content
1293+
</mat-tab>
1294+
<mat-tab label="Tab Four" [labelClass]="labelClassList" [bodyClass]="bodyClassList">
1295+
Tab four content
1296+
</mat-tab>
1297+
<mat-tab label="Tab Five" labelClass="hardcoded-label-class"
1298+
bodyClass="hardcoded-body-class">
1299+
Tab five content
1300+
</mat-tab>
1301+
</mat-tab-group>
1302+
`,
1303+
})
1304+
class TabsWithClassesTestApp {
1305+
labelClassList?: string | string[];
1306+
bodyClassList?: string | string[];
1307+
}

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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 8 additions & 5 deletions
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)