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 d9ed100
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 58 deletions.
32 changes: 17 additions & 15 deletions src/material-experimental/mdc-tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
[attr.aria-posinset]="i + 1"
[attr.aria-setsize]="_tabs.length"
[attr.aria-controls]="_getTabContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[attr.aria-selected]="selectedIndex === i"
[attr.aria-label]="tab.ariaLabel || null"
[attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null"
[class.mdc-tab--active]="selectedIndex == i"
[class.mdc-tab--active]="selectedIndex === i"
[ngClass]="tab.labelClassList"
[disabled]="tab.disabled"
[fitInkBarToContent]="fitInkBarToContent"
(click)="_handleClick(tab, tabHeader, i)"
Expand All @@ -36,12 +37,12 @@
<span class="mdc-tab__content">
<span class="mdc-tab__text-label">
<!-- 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>
</span>
</span>
</div>
Expand All @@ -52,16 +53,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-mdc-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-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)">
</mat-tab-body>
</div>
150 changes: 138 additions & 12 deletions src/material-experimental/mdc-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('MDC-based MatTabGroup', () => {
TabGroupWithIndirectDescendantTabs,
TabGroupWithSpaceAbove,
NestedTabGroupWithLabel,
TabsWithClassesTestApp,
],
});

Expand Down Expand Up @@ -364,7 +365,6 @@ describe('MDC-based MatTabGroup', () => {

expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
});

});

describe('aria labelling', () => {
Expand Down Expand Up @@ -404,11 +404,16 @@ describe('MDC-based 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<DisabledTabsTestApp>;

beforeEach(() => {
fixture = TestBed.createComponent(DisabledTabsTestApp);
});
Expand Down Expand Up @@ -482,7 +487,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;
Expand All @@ -500,7 +504,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 =
Expand All @@ -517,7 +520,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;
Expand Down Expand Up @@ -565,7 +567,6 @@ describe('MDC-based MatTabGroup', () => {

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

});

describe('async tabs', () => {
Expand Down Expand Up @@ -756,6 +757,100 @@ describe('MDC-based 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('.mdc-tab.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('.mdc-tab.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('.mdc-tab.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('.mdc-tab.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('.mdc-tab.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('.mdc-tab.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('.mdc-tab.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 @@ -935,6 +1030,7 @@ class SimpleTabsTestApp {
animationDone() { }
}


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


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


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


@Component({
template: `
<mat-tab-group class="tab-group">
Expand Down Expand Up @@ -1059,7 +1157,6 @@ class TabGroupWithSimpleApi {


@Component({
selector: 'nested-tabs',
template: `
<mat-tab-group>
<mat-tab label="One">Tab one content</mat-tab>
Expand All @@ -1077,8 +1174,8 @@ class NestedTabs {
@ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
}


@Component({
selector: 'template-tabs',
template: `
<mat-tab-group>
<mat-tab label="One">
Expand All @@ -1091,11 +1188,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 @@ -1160,6 +1257,7 @@ class TabGroupWithInkBarFitToContent {
fitInkBarToContent = true;
}


@Component({
template: `
<div style="height: 300px; background-color: aqua">
Expand Down Expand Up @@ -1202,3 +1300,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[];
}
16 changes: 15 additions & 1 deletion src/material-experimental/mdc-tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
ViewEncapsulation,
TemplateRef,
ContentChild,
OnChanges,
SimpleChanges,
} from '@angular/core';
import {MatTab as BaseMatTab, MAT_TAB} from '@angular/material/tabs';
import {MatTabContent} from './tab-content';
Expand All @@ -30,7 +32,7 @@ import {MatTabLabel} from './tab-label';
exportAs: 'matTab',
providers: [{provide: MAT_TAB, useExisting: MatTab}]
})
export class MatTab extends BaseMatTab {
export class MatTab extends BaseMatTab implements OnChanges {
/**
* Template provided in the tab content that will be used if present, used to enable lazy-loading
*/
Expand All @@ -41,4 +43,16 @@ export class MatTab extends BaseMatTab {
@ContentChild(MatTabLabel)
override get templateLabel(): MatTabLabel { return this._templateLabel; }
override set templateLabel(value: MatTabLabel) { this._setTemplateLabelInput(value); }

override ngOnChanges(changes: SimpleChanges): void {
super.ngOnChanges(changes);

// Triggering ChangeDetectorRef.markForCheck()
if (changes.hasOwnProperty('ariaLabel')
|| changes.hasOwnProperty('ariaLabelledby')
|| changes.hasOwnProperty('labelClass')
|| changes.hasOwnProperty('bodyClass')) {
this._stateChanges.next();
}
}
}
Loading

0 comments on commit d9ed100

Please sign in to comment.