diff --git a/src/material-experimental/mdc-tabs/tab-group.html b/src/material-experimental/mdc-tabs/tab-group.html index 8db3d19cd45b..e5cb0e22ab52 100644 --- a/src/material-experimental/mdc-tabs/tab-group.html +++ b/src/material-experimental/mdc-tabs/tab-group.html @@ -19,6 +19,7 @@ [attr.aria-label]="tab.ariaLabel || null" [attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null" [class.mdc-tab--active]="selectedIndex == i" + [ngClass]="tab.labelClassList" [disabled]="tab.disabled" [fitInkBarToContent]="fitInkBarToContent" (click)="_handleClick(tab, tabHeader, i)" @@ -36,12 +37,12 @@ - + - {{tab.textLabel}} + {{tab.textLabel}} @@ -52,16 +53,17 @@ [class._mat-animation-noopable]="_animationMode === 'NoopAnimations'" #tabBodyWrapper> + *ngFor="let tab of _tabs; let i = index" + [id]="_getTabContentId(i)" + [attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null" + [attr.aria-labelledby]="_getTabLabelId(i)" + [class.mat-mdc-tab-body-active]="selectedIndex === i" + [ngClass]="tab.bodyClassList" + [content]="tab.content!" + [position]="tab.position!" + [origin]="tab.origin" + [animationDuration]="animationDuration" + (_onCentered)="_removeTabBodyWrapperHeight()" + (_onCentering)="_setTabBodyWrapperHeight($event)"> diff --git a/src/material-experimental/mdc-tabs/tab-group.spec.ts b/src/material-experimental/mdc-tabs/tab-group.spec.ts index 4cdff1500a05..d6a6c4f207b8 100644 --- a/src/material-experimental/mdc-tabs/tab-group.spec.ts +++ b/src/material-experimental/mdc-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, @@ -41,6 +41,7 @@ describe('MDC-based MatTabGroup', () => { TabGroupWithIndirectDescendantTabs, TabGroupWithSpaceAbove, NestedTabGroupWithLabel, + TabsWithClassesTestApp, ], }); @@ -364,7 +365,6 @@ describe('MDC-based MatTabGroup', () => { expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]); }); - }); describe('aria labelling', () => { @@ -409,6 +409,7 @@ describe('MDC-based MatTabGroup', () => { describe('disable tabs', () => { let fixture: ComponentFixture; + beforeEach(() => { fixture = TestBed.createComponent(DisabledTabsTestApp); }); @@ -482,7 +483,6 @@ describe('MDC-based MatTabGroup', () => { expect(tabs[0].origin).toBeLessThan(0); })); - it('should update selected index if the last tab removed while selected', fakeAsync(() => { const component: MatTabGroup = fixture.debugElement.query(By.css('mat-tab-group')).componentInstance; @@ -500,7 +500,6 @@ describe('MDC-based MatTabGroup', () => { expect(component.selectedIndex).toBe(numberOfTabs - 2); })); - it('should maintain the selected tab if a new tab is added', () => { fixture.detectChanges(); const component: MatTabGroup = @@ -517,7 +516,6 @@ describe('MDC-based MatTabGroup', () => { expect(component._tabs.toArray()[2].isActive).toBe(true); }); - it('should maintain the selected tab if a tab is removed', () => { // Select the second tab. fixture.componentInstance.selectedIndex = 1; @@ -565,7 +563,6 @@ describe('MDC-based MatTabGroup', () => { expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled(); })); - }); describe('async tabs', () => { @@ -756,6 +753,100 @@ describe('MDC-based MatTabGroup', () => { })); }); + describe('tabs with custom css classes', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(TabsWithClassesTestApp); + }); + + it('should apply label classes', () => { + fixture.detectChanges(); + + const labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.hardcoded.label.classes')); + expect(labelElements.length).toBe(1); + }); + + it('should apply body classes', () => { + fixture.detectChanges(); + + const bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.hardcoded.body.classes')); + expect(bodyElements.length).toBe(1); + }); + + it('should set classes as strings dynamically', () => { + fixture.detectChanges(); + let labelElements: DebugElement[]; + let bodyElements: DebugElement[]; + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + + fixture.componentInstance.labelClassList = 'custom-label-class one-more-label-class'; + fixture.componentInstance.bodyClassList = 'custom-body-class one-more-body-class'; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(2); + expect(bodyElements.length).toBe(2); + + delete fixture.componentInstance.labelClassList; + delete fixture.componentInstance.bodyClassList; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + }); + + it('should set classes as strings array dynamically', () => { + fixture.detectChanges(); + let labelElements: DebugElement[]; + let bodyElements: DebugElement[]; + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + + fixture.componentInstance.labelClassList = ['custom-label-class', 'one-more-label-class']; + fixture.componentInstance.bodyClassList = ['custom-body-class', 'one-more-body-class']; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(2); + expect(bodyElements.length).toBe(2); + + delete fixture.componentInstance.labelClassList; + delete fixture.componentInstance.bodyClassList; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + }); + }); + /** * Checks that the `selectedIndex` has been updated; checks that the label and body have their * respective `active` classes @@ -935,6 +1026,7 @@ class SimpleTabsTestApp { animationDone() { } } + @Component({ template: ` @@ -990,8 +1083,8 @@ class BindedTabsTestApp { } } + @Component({ - selector: 'test-app', template: ` @@ -1014,6 +1107,7 @@ class DisabledTabsTestApp { isDisabled = false; } + @Component({ template: ` @@ -1059,7 +1153,6 @@ class TabGroupWithSimpleApi { @Component({ - selector: 'nested-tabs', template: ` Tab one content @@ -1077,8 +1170,8 @@ class NestedTabs { @ViewChildren(MatTabGroup) groups: QueryList; } + @Component({ - selector: 'template-tabs', template: ` @@ -1091,11 +1184,11 @@ class NestedTabs { `, - }) - class TemplateTabs {} +}) +class TemplateTabs {} - @Component({ +@Component({ template: ` @@ -1160,6 +1253,7 @@ class TabGroupWithInkBarFitToContent { fitInkBarToContent = true; } + @Component({ template: `
@@ -1202,3 +1296,31 @@ class TabGroupWithSpaceAbove { }) class NestedTabGroupWithLabel { } + + +@Component({ + template: ` + + + Tab one content + + + Tab two content + + + Tab three content + + + Tab four content + + + Tab five content + + + `, +}) +class TabsWithClassesTestApp { + @ViewChildren(MatTab) tabs: QueryList; + labelClassList?: string | string[]; + bodyClassList?: string | string[]; +} diff --git a/src/material/tabs/tab-group.html b/src/material/tabs/tab-group.html index 268ed3076d1e..f143dc6906b8 100644 --- a/src/material/tabs/tab-group.html +++ b/src/material/tabs/tab-group.html @@ -4,7 +4,8 @@ [disablePagination]="disablePagination" (indexFocused)="_focusChanged($event)" (selectFocusedIndex)="selectedIndex = $event"> -
@@ -38,16 +40,17 @@ [class._mat-animation-noopable]="_animationMode === 'NoopAnimations'" #tabBodyWrapper> + *ngFor="let tab of _tabs; let i = index" + [id]="_getTabContentId(i)" + [attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null" + [attr.aria-labelledby]="_getTabLabelId(i)" + [class.mat-tab-body-active]="selectedIndex === i" + [ngClass]="tab.bodyClassList" + [content]="tab.content!" + [position]="tab.position!" + [origin]="tab.origin" + [animationDuration]="animationDuration" + (_onCentered)="_removeTabBodyWrapperHeight()" + (_onCentering)="_setTabBodyWrapperHeight($event)"> diff --git a/src/material/tabs/tab-group.spec.ts b/src/material/tabs/tab-group.spec.ts index 08198300588b..b4dccf0cec73 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, @@ -41,6 +41,7 @@ describe('MatTabGroup', () => { TabGroupWithIndirectDescendantTabs, TabGroupWithSpaceAbove, NestedTabGroupWithLabel, + TabsWithClassesTestApp, ], }); @@ -363,7 +364,6 @@ describe('MatTabGroup', () => { expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]); }); - }); describe('aria labelling', () => { @@ -408,6 +408,7 @@ describe('MatTabGroup', () => { describe('disable tabs', () => { let fixture: ComponentFixture; + beforeEach(() => { fixture = TestBed.createComponent(DisabledTabsTestApp); }); @@ -481,7 +482,6 @@ describe('MatTabGroup', () => { expect(tabs[0].origin).toBeLessThan(0); })); - it('should update selected index if the last tab removed while selected', fakeAsync(() => { const component: MatTabGroup = fixture.debugElement.query(By.css('mat-tab-group'))!.componentInstance; @@ -499,7 +499,6 @@ describe('MatTabGroup', () => { expect(component.selectedIndex).toBe(numberOfTabs - 2); })); - it('should maintain the selected tab if a new tab is added', () => { fixture.detectChanges(); const component: MatTabGroup = @@ -516,7 +515,6 @@ describe('MatTabGroup', () => { expect(component._tabs.toArray()[2].isActive).toBe(true); }); - it('should maintain the selected tab if a tab is removed', () => { // Select the second tab. fixture.componentInstance.selectedIndex = 1; @@ -564,7 +562,6 @@ describe('MatTabGroup', () => { expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled(); })); - }); describe('async tabs', () => { @@ -756,6 +753,100 @@ describe('MatTabGroup', () => { })); }); + describe('tabs with custom css classes', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(TabsWithClassesTestApp); + }); + + it('should apply label classes', () => { + fixture.detectChanges(); + + const labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.hardcoded.label.classes')); + expect(labelElements.length).toBe(1); + }); + + it('should apply body classes', () => { + fixture.detectChanges(); + + const bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.hardcoded.body.classes')); + expect(bodyElements.length).toBe(1); + }); + + it('should set classes as strings dynamically', () => { + fixture.detectChanges(); + let labelElements: DebugElement[]; + let bodyElements: DebugElement[]; + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + + fixture.componentInstance.labelClassList = 'custom-label-class one-more-label-class'; + fixture.componentInstance.bodyClassList = 'custom-body-class one-more-body-class'; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(2); + expect(bodyElements.length).toBe(2); + + delete fixture.componentInstance.labelClassList; + delete fixture.componentInstance.bodyClassList; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + }); + + it('should set classes as strings array dynamically', () => { + fixture.detectChanges(); + let labelElements: DebugElement[]; + let bodyElements: DebugElement[]; + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + + fixture.componentInstance.labelClassList = ['custom-label-class', 'one-more-label-class']; + fixture.componentInstance.bodyClassList = ['custom-body-class', 'one-more-body-class']; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(2); + expect(bodyElements.length).toBe(2); + + delete fixture.componentInstance.labelClassList; + delete fixture.componentInstance.bodyClassList; + fixture.detectChanges(); + + labelElements = fixture.debugElement + .queryAll(By.css('.mat-tab-label.custom-label-class.one-more-label-class')); + bodyElements = fixture.debugElement + .queryAll(By.css('mat-tab-body.custom-body-class.one-more-body-class')); + expect(labelElements.length).toBe(0); + expect(bodyElements.length).toBe(0); + }); + }); + /** * Checks that the `selectedIndex` has been updated; checks that the label and body have their * respective `active` classes @@ -881,6 +972,7 @@ class SimpleTabsTestApp { animationDone() { } } + @Component({ template: ` @@ -936,8 +1029,8 @@ class BindedTabsTestApp { } } + @Component({ - selector: 'test-app', template: ` @@ -960,6 +1053,7 @@ class DisabledTabsTestApp { isDisabled = false; } + @Component({ template: ` @@ -1005,7 +1099,6 @@ class TabGroupWithSimpleApi { @Component({ - selector: 'nested-tabs', template: ` Tab one content @@ -1023,8 +1116,8 @@ class NestedTabs { @ViewChildren(MatTabGroup) groups: QueryList; } + @Component({ - selector: 'template-tabs', template: ` @@ -1037,11 +1130,11 @@ class NestedTabs { `, - }) - class TemplateTabs {} +}) +class TemplateTabs {} - @Component({ +@Component({ template: ` @@ -1093,6 +1186,7 @@ class TabGroupWithIndirectDescendantTabs { @ViewChild(MatTabGroup) tabGroup: MatTabGroup; } + @Component({ template: `
@@ -1135,3 +1229,31 @@ class TabGroupWithSpaceAbove { }) class NestedTabGroupWithLabel { } + + +@Component({ + template: ` + + + Tab one content + + + Tab two content + + + Tab three content + + + Tab four content + + + Tab five content + + + `, +}) +class TabsWithClassesTestApp { + @ViewChildren(MatTab) tabs: QueryList; + labelClassList?: string | string[]; + bodyClassList?: string | string[]; +} diff --git a/src/material/tabs/tab.ts b/src/material/tabs/tab.ts index 775842436b16..dfceb0d077c2 100644 --- a/src/material/tabs/tab.ts +++ b/src/material/tabs/tab.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {BooleanInput} from '@angular/cdk/coercion'; +import {BooleanInput, coerceStringArray} from '@angular/cdk/coercion'; import {TemplatePortal} from '@angular/cdk/portal'; import { ChangeDetectionStrategy, @@ -79,6 +79,40 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges */ @Input('aria-labelledby') ariaLabelledby: string; + /** + * Takes classes set on the host mat-tab element and applies them to the tab + * label inside the mat-tab-header container to allow for easy styling. + */ + @Input('class') + set labelClass(value: string | string[]) { + if (value && value.length) { + this.labelClassList = coerceStringArray(value).reduce((classList, className) => { + classList[className] = true; + return classList; + }, {} as {[key: string]: boolean}); + } else { + this.labelClassList = {}; + } + } + labelClassList: {[key: string]: boolean} = {}; + + /** + * Takes classes set on the host mat-tab element and applies them to the tab + * label inside the mat-tab-body container to allow for easy styling. + */ + @Input() + set bodyClass(value: string | string[]) { + if (value && value.length) { + this.bodyClassList = coerceStringArray(value).reduce((classList, className) => { + classList[className] = true; + return classList; + }, {} as {[key: string]: boolean}); + } else { + this.bodyClassList = {}; + } + } + bodyClassList: {[key: string]: boolean} = {}; + /** 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 c7e52a211b57..c7c616ff744d 100644 --- a/tools/public_api_guard/material/tabs.md +++ b/tools/public_api_guard/material/tabs.md @@ -101,12 +101,22 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges constructor(_viewContainerRef: ViewContainerRef, _closestTabGroup: any); ariaLabel: string; ariaLabelledby: string; + set bodyClass(value: string | string[]); + // (undocumented) + bodyClassList: { + [key: string]: boolean; + }; // (undocumented) _closestTabGroup: any; get content(): TemplatePortal | null; _explicitContent: TemplateRef; _implicitContent: TemplateRef; isActive: boolean; + set labelClass(value: string | string[]); + // (undocumented) + labelClassList: { + [key: string]: boolean; + }; // (undocumented) static ngAcceptInputType_disabled: BooleanInput; // (undocumented) @@ -125,7 +135,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; }