@@ -1215,3 +1274,21 @@ class TabGroupWithSpaceAbove {
`,
})
class NestedTabGroupWithLabel {}
+
+@Component({
+ template: `
+
+
+ Tab one content
+
+
+ Tab two content
+
+
+ `,
+})
+class TabsWithClassesTestApp {
+ labelClassList?: string | string[];
+ bodyClassList?: string | string[];
+}
diff --git a/src/material-experimental/mdc-tabs/tab-group.ts b/src/material-experimental/mdc-tabs/tab-group.ts
index 6b33748921b8..77474f735d74 100644
--- a/src/material-experimental/mdc-tabs/tab-group.ts
+++ b/src/material-experimental/mdc-tabs/tab-group.ts
@@ -41,7 +41,8 @@ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
templateUrl: 'tab-group.html',
styleUrls: ['tab-group.css'],
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
inputs: ['color', 'disableRipple'],
providers: [
{
diff --git a/src/material-experimental/mdc-tabs/tab-header.ts b/src/material-experimental/mdc-tabs/tab-header.ts
index 9d080ac7a599..bad169aa64fe 100644
--- a/src/material-experimental/mdc-tabs/tab-header.ts
+++ b/src/material-experimental/mdc-tabs/tab-header.ts
@@ -43,7 +43,8 @@ import {MatInkBar} from './ink-bar';
inputs: ['selectedIndex'],
outputs: ['selectFocusedIndex', 'indexFocused'],
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
host: {
'class': 'mat-mdc-tab-header',
'[class.mat-mdc-tab-header-pagination-controls-enabled]': '_showPaginationControls',
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
index c37f7c41a9a8..1e691c198080 100644
--- a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
@@ -65,7 +65,8 @@ import {takeUntil} from 'rxjs/operators';
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
},
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
})
export class MatTabNav extends _MatTabNavBase implements AfterContentInit {
/** Whether the ink bar should fit its width to the size of the tab label content. */
diff --git a/src/material-experimental/mdc-tabs/tab.ts b/src/material-experimental/mdc-tabs/tab.ts
index 3d38e03c1a8e..b7384bb6b428 100644
--- a/src/material-experimental/mdc-tabs/tab.ts
+++ b/src/material-experimental/mdc-tabs/tab.ts
@@ -25,7 +25,8 @@ import {MatTabLabel} from './tab-label';
// that creating the extra class will generate more code than just duplicating the template.
templateUrl: 'tab.html',
inputs: ['disabled'],
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.None,
exportAs: 'matTab',
providers: [{provide: MAT_TAB, useExisting: MatTab}],
diff --git a/src/material/tabs/tab-group.html b/src/material/tabs/tab-group.html
index 268ed3076d1e..ebd767eda5b7 100644
--- a/src/material/tabs/tab-group.html
+++ b/src/material/tabs/tab-group.html
@@ -4,17 +4,19 @@
[disablePagination]="disablePagination"
(indexFocused)="_focusChanged($event)"
(selectFocusedIndex)="selectedIndex = $event">
-
-
+
- {{tab.textLabel}}
+ {{tab.textLabel}}
@@ -43,6 +45,7 @@
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
[attr.aria-labelledby]="_getTabLabelId(i)"
[class.mat-tab-body-active]="selectedIndex === i"
+ [ngClass]="tab.bodyClass"
[content]="tab.content!"
[position]="tab.position!"
[origin]="tab.origin"
diff --git a/src/material/tabs/tab-group.spec.ts b/src/material/tabs/tab-group.spec.ts
index d91b51ca43cf..6cb53a529583 100644
--- a/src/material/tabs/tab-group.spec.ts
+++ b/src/material/tabs/tab-group.spec.ts
@@ -1,6 +1,6 @@
import {LEFT_ARROW} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
-import {Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
+import {Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
@@ -40,6 +40,7 @@ describe('MatTabGroup', () => {
TabGroupWithIndirectDescendantTabs,
TabGroupWithSpaceAbove,
NestedTabGroupWithLabel,
+ TabsWithClassesTestApp,
],
});
@@ -419,11 +420,16 @@ describe('MatTabGroup', () => {
expect(tab.getAttribute('aria-label')).toBe('Fruit');
expect(tab.hasAttribute('aria-labelledby')).toBe(false);
+
+ fixture.componentInstance.ariaLabel = 'Veggie';
+ fixture.detectChanges();
+ expect(tab.getAttribute('aria-label')).toBe('Veggie');
});
});
describe('disable tabs', () => {
let fixture: ComponentFixture;
+
beforeEach(() => {
fixture = TestBed.createComponent(DisabledTabsTestApp);
});
@@ -779,6 +785,62 @@ describe('MatTabGroup', () => {
}));
});
+ describe('tabs with custom css classes', () => {
+ let fixture: ComponentFixture;
+ let labelElements: DebugElement[];
+ let bodyElements: DebugElement[];
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TabsWithClassesTestApp);
+ fixture.detectChanges();
+ labelElements = fixture.debugElement.queryAll(By.css('.mat-tab-label'));
+ bodyElements = fixture.debugElement.queryAll(By.css('mat-tab-body'));
+ });
+
+ it('should apply label/body classes', () => {
+ expect(labelElements[1].nativeElement.classList).toContain('hardcoded-label-class');
+ expect(bodyElements[1].nativeElement.classList).toContain('hardcoded-body-class');
+ });
+
+ it('should set classes as strings dynamically', () => {
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+
+ fixture.componentInstance.labelClassList = 'custom-label-class';
+ fixture.componentInstance.bodyClassList = 'custom-body-class';
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
+
+ delete fixture.componentInstance.labelClassList;
+ delete fixture.componentInstance.bodyClassList;
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+ });
+
+ it('should set classes as strings array dynamically', () => {
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+
+ fixture.componentInstance.labelClassList = ['custom-label-class'];
+ fixture.componentInstance.bodyClassList = ['custom-body-class'];
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
+
+ delete fixture.componentInstance.labelClassList;
+ delete fixture.componentInstance.bodyClassList;
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+ });
+ });
+
/**
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
* respective `active` classes
@@ -960,7 +1022,6 @@ class BindedTabsTestApp {
}
@Component({
- selector: 'test-app',
template: `
@@ -1026,7 +1087,6 @@ class TabGroupWithSimpleApi {
}
@Component({
- selector: 'nested-tabs',
template: `
Tab one content
@@ -1045,7 +1105,6 @@ class NestedTabs {
}
@Component({
- selector: 'template-tabs',
template: `
@@ -1149,3 +1208,21 @@ class TabGroupWithSpaceAbove {
`,
})
class NestedTabGroupWithLabel {}
+
+@Component({
+ template: `
+
+
+ Tab one content
+
+
+ Tab two content
+
+
+ `,
+})
+class TabsWithClassesTestApp {
+ labelClassList?: string | string[];
+ bodyClassList?: string | string[];
+}
diff --git a/src/material/tabs/tab.ts b/src/material/tabs/tab.ts
index 5f6019879b13..ab9a75de8299 100644
--- a/src/material/tabs/tab.ts
+++ b/src/material/tabs/tab.ts
@@ -82,6 +82,18 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
*/
@Input('aria-labelledby') ariaLabelledby: string;
+ /**
+ * Classes to be passed to the tab label inside the mat-tab-header container.
+ * Supports string and string array values, same as `ngClass`.
+ */
+ @Input() labelClass: string | string[];
+
+ /**
+ * Classes to be passed to the tab mat-tab-body container.
+ * Supports string and string array values, same as `ngClass`.
+ */
+ @Input() bodyClass: string | string[];
+
/** Portal that will be the hosted content of the tab */
private _contentPortal: TemplatePortal | null = null;
diff --git a/tools/public_api_guard/material/tabs.md b/tools/public_api_guard/material/tabs.md
index 7e0b17546247..c7fbe04c9e10 100644
--- a/tools/public_api_guard/material/tabs.md
+++ b/tools/public_api_guard/material/tabs.md
@@ -101,12 +101,14 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
constructor(_viewContainerRef: ViewContainerRef, _closestTabGroup: any);
ariaLabel: string;
ariaLabelledby: string;
+ bodyClass: string | string[];
// (undocumented)
_closestTabGroup: any;
get content(): TemplatePortal | null;
_explicitContent: TemplateRef;
_implicitContent: TemplateRef;
isActive: boolean;
+ labelClass: string | string[];
// (undocumented)
static ngAcceptInputType_disabled: BooleanInput;
// (undocumented)
@@ -125,7 +127,7 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
protected _templateLabel: MatTabLabel;
textLabel: string;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}