Skip to content

Commit

Permalink
feat(material/tabs): label & body classes
Browse files Browse the repository at this point in the history
  • Loading branch information
lekhmanrus committed Oct 5, 2021
1 parent 7c16258 commit 04df98b
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 25 deletions.
31 changes: 17 additions & 14 deletions src/material/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
[disablePagination]="disablePagination"
(indexFocused)="_focusChanged($event)"
(selectFocusedIndex)="selectedIndex = $event">
<div class="mat-tab-label mat-focus-indicator" role="tab" matTabLabelWrapper mat-ripple cdkMonitorElementFocus
<div class="mat-tab-label mat-focus-indicator" role="tab" matTabLabelWrapper mat-ripple
cdkMonitorElementFocus
*ngFor="let tab of _tabs; let i = index"
[id]="_getTabLabelId(i)"
[attr.tabIndex]="_getTabIndex(tab, i)"
Expand All @@ -15,6 +16,7 @@
[attr.aria-label]="tab.ariaLabel || null"
[attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null"
[class.mat-tab-label-active]="selectedIndex == i"
[ngClass]="tab.labelClassList"
[disabled]="tab.disabled"
[matRippleDisabled]="tab.disabled || disableRipple"
(click)="_handleClick(tab, tabHeader, i)"
Expand All @@ -23,12 +25,12 @@

<div class="mat-tab-label-content">
<!-- If there is a label template, use it. -->
<ng-template [ngIf]="tab.templateLabel">
<ng-template [ngIf]="tab.templateLabel" [ngIfElse]="tabTextLabel">
<ng-template [cdkPortalOutlet]="tab.templateLabel"></ng-template>
</ng-template>

<!-- If there is not a label template, fall back to the text label. -->
<ng-template [ngIf]="!tab.templateLabel">{{tab.textLabel}}</ng-template>
<ng-template #tabTextLabel>{{tab.textLabel}}</ng-template>
</div>
</div>
</mat-tab-header>
Expand All @@ -38,16 +40,17 @@
[class._mat-animation-noopable]="_animationMode === 'NoopAnimations'"
#tabBodyWrapper>
<mat-tab-body role="tabpanel"
*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"
[content]="tab.content!"
[position]="tab.position!"
[origin]="tab.origin"
[animationDuration]="animationDuration"
(_onCentered)="_removeTabBodyWrapperHeight()"
(_onCentering)="_setTabBodyWrapperHeight($event)">
*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)">
</mat-tab-body>
</div>
143 changes: 134 additions & 9 deletions src/material/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -41,6 +41,7 @@ describe('MatTabGroup', () => {
TabGroupWithIndirectDescendantTabs,
TabGroupWithSpaceAbove,
NestedTabGroupWithLabel,
TabsWithClassesTestApp,
],
});

Expand Down Expand Up @@ -408,6 +409,7 @@ describe('MatTabGroup', () => {

describe('disable tabs', () => {
let fixture: ComponentFixture<DisabledTabsTestApp>;

beforeEach(() => {
fixture = TestBed.createComponent(DisabledTabsTestApp);
});
Expand Down Expand Up @@ -481,7 +483,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;
Expand All @@ -499,7 +500,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 =
Expand All @@ -516,7 +516,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;
Expand Down Expand Up @@ -564,7 +563,6 @@ describe('MatTabGroup', () => {

expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled();
}));

});

describe('async tabs', () => {
Expand Down Expand Up @@ -756,6 +754,100 @@ describe('MatTabGroup', () => {
}));
});

describe('tabs with custom css classes', () => {
let fixture: ComponentFixture<TabsWithClassesTestApp>;

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
Expand Down Expand Up @@ -881,6 +973,7 @@ class SimpleTabsTestApp {
animationDone() { }
}


@Component({
template: `
<mat-tab-group class="tab-group"
Expand Down Expand Up @@ -911,6 +1004,7 @@ class SimpleDynamicTabsTestApp {
}
}


@Component({
template: `
<mat-tab-group class="tab-group" [(selectedIndex)]="selectedIndex">
Expand All @@ -936,8 +1030,8 @@ class BindedTabsTestApp {
}
}


@Component({
selector: 'test-app',
template: `
<mat-tab-group class="tab-group">
<mat-tab>
Expand All @@ -960,6 +1054,7 @@ class DisabledTabsTestApp {
isDisabled = false;
}


@Component({
template: `
<mat-tab-group class="tab-group">
Expand Down Expand Up @@ -1023,6 +1118,7 @@ class NestedTabs {
@ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
}


@Component({
selector: 'template-tabs',
template: `
Expand All @@ -1037,11 +1133,11 @@ class NestedTabs {
</mat-tab>
</mat-tab-group>
`,
})
class TemplateTabs {}
})
class TemplateTabs {}


@Component({
@Component({
template: `
<mat-tab-group>
<mat-tab [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"></mat-tab>
Expand Down Expand Up @@ -1093,6 +1189,7 @@ class TabGroupWithIndirectDescendantTabs {
@ViewChild(MatTabGroup) tabGroup: MatTabGroup;
}


@Component({
template: `
<div style="height: 300px; background-color: aqua">
Expand Down Expand Up @@ -1135,3 +1232,31 @@ class TabGroupWithSpaceAbove {
})
class NestedTabGroupWithLabel {
}


@Component({
template: `
<mat-tab-group class="tab-group">
<mat-tab label="Tab One">
Tab one content
</mat-tab>
<mat-tab label="Tab Two" [class]="labelClassList">
Tab two content
</mat-tab>
<mat-tab label="Tab Three" [bodyClass]="bodyClassList">
Tab three content
</mat-tab>
<mat-tab label="Tab Four" [class]="labelClassList" [bodyClass]="bodyClassList">
Tab four content
</mat-tab>
<mat-tab label="Tab Five" class="hardcoded label classes" bodyClass="hardcoded body classes">
Tab five content
</mat-tab>
</mat-tab-group>
`,
})
class TabsWithClassesTestApp {
@ViewChildren(MatTab) tabs: QueryList<MatTab>;
labelClassList?: string | string[];
bodyClassList?: string | string[];
}
36 changes: 35 additions & 1 deletion src/material/tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down
12 changes: 11 additions & 1 deletion tools/public_api_guard/material/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
_implicitContent: TemplateRef<any>;
isActive: boolean;
set labelClass(value: string | string[]);
// (undocumented)
labelClassList: {
[key: string]: boolean;
};
// (undocumented)
static ngAcceptInputType_disabled: BooleanInput;
// (undocumented)
Expand All @@ -125,7 +135,7 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
protected _templateLabel: MatTabLabel;
textLabel: string;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatTab, "mat-tab", ["matTab"], { "disabled": "disabled"; "textLabel": "label"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; }, {}, ["templateLabel", "_explicitContent"], ["*"]>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatTab, "mat-tab", ["matTab"], { "disabled": "disabled"; "textLabel": "label"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "labelClass": "class"; "bodyClass": "bodyClass"; }, {}, ["templateLabel", "_explicitContent"], ["*"]>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatTab, [null, { optional: true; }]>;
}
Expand Down

0 comments on commit 04df98b

Please sign in to comment.