Skip to content

Commit

Permalink
feat(tabs): adds support for disabled tabs (#934)
Browse files Browse the repository at this point in the history
closes #880
  • Loading branch information
robertmesserle authored and jelbourn committed Aug 12, 2016
1 parent 22d70ae commit 9d51deb
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 23 deletions.
2 changes: 1 addition & 1 deletion e2e/components/tabs/tabs.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function getFocusStates(elements: ElementArrayFinder) {
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
*/
function getActiveStates(elements: ElementArrayFinder) {
return getClassStates(elements, 'md-active');
return getClassStates(elements, 'md-tab-active');
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/components/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
[tabIndex]="selectedIndex == i ? 0 : -1"
[attr.aria-controls]="_getTabContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[class.md-active]="selectedIndex == i"
[class.md-tab-active]="selectedIndex == i"
[class.md-tab-disabled]="tab.disabled"
(click)="focusIndex = selectedIndex = i">
<template [portalHost]="tab.label"></template>
</div>
Expand All @@ -17,7 +18,7 @@
role="tabpanel"
*ngFor="let tab of _tabs; let i = index"
[id]="_getTabContentId(i)"
[class.md-active]="selectedIndex == i"
[class.md-tab-active]="selectedIndex == i"
[attr.aria-labelledby]="_getTabLabelId(i)">
<template [ngIf]="selectedIndex == i">
<template [portalHost]="tab.content"></template>
Expand Down
5 changes: 5 additions & 0 deletions src/components/tabs/tab-group.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ $md-tab-bar-height: 48px !default;
}
}

.md-tab-disabled {
cursor: default;
pointer-events: none;
}

/** The bottom section of the view; contains the tab bodies */
.md-tab-body-wrapper {
position: relative;
Expand Down
113 changes: 110 additions & 3 deletions src/components/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,92 @@ describe('MdTabGroup', () => {
}));
});

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

beforeEach(async(() => {
builder.createAsync(DisabledTabsTestApp).then(f => {
fixture = f;
fixture.detectChanges();
});
}));

it('should disable the second tab', () => {
let labels = fixture.debugElement.queryAll(By.css('.md-tab-label'));

expect(labels[1].nativeElement.classList.contains('md-tab-disabled')).toBeTruthy();
});

it('should skip over disabled tabs when navigating by keyboard', () => {
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
.componentInstance;

component.focusIndex = 0;
component.focusNextTab();

expect(component.focusIndex).toBe(2);

component.focusNextTab();
expect(component.focusIndex).toBe(2);

component.focusPreviousTab();
expect(component.focusIndex).toBe(0);

component.focusPreviousTab();
expect(component.focusIndex).toBe(0);
});

it('should ignore attempts to select a disabled tab', () => {
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
.componentInstance;

component.selectedIndex = 0;
expect(component.selectedIndex).toBe(0);

component.selectedIndex = 1;
expect(component.selectedIndex).toBe(0);
});

it('should ignore attempts to focus a disabled tab', () => {
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
.componentInstance;

component.focusIndex = 0;
expect(component.focusIndex).toBe(0);

component.focusIndex = 1;
expect(component.focusIndex).toBe(0);
});

it('should ignore attempts to set invalid selectedIndex', () => {
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
.componentInstance;

component.selectedIndex = 0;
expect(component.selectedIndex).toBe(0);

component.selectedIndex = -1;
expect(component.selectedIndex).toBe(0);

component.selectedIndex = 4;
expect(component.selectedIndex).toBe(0);
});

it('should ignore attempts to set invalid focusIndex', () => {
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
.componentInstance;

component.focusIndex = 0;
expect(component.focusIndex).toBe(0);

component.focusIndex = -1;
expect(component.focusIndex).toBe(0);

component.focusIndex = 4;
expect(component.focusIndex).toBe(0);
});
});

describe('async tabs', () => {
let fixture: ComponentFixture<AsyncTabsTestApp>;

Expand All @@ -173,7 +259,7 @@ describe('MdTabGroup', () => {

/**
* Checks that the `selectedIndex` has been updated; checks that the label and body have the
* `md-active` class
* `md-tab-active` class
*/
function checkSelectedIndex(index: number, fixture: ComponentFixture<any>) {
fixture.detectChanges();
Expand All @@ -184,11 +270,11 @@ describe('MdTabGroup', () => {

let tabLabelElement = fixture.debugElement
.query(By.css(`.md-tab-label:nth-of-type(${index + 1})`)).nativeElement;
expect(tabLabelElement.classList.contains('md-active')).toBe(true);
expect(tabLabelElement.classList.contains('md-tab-active')).toBe(true);

let tabContentElement = fixture.debugElement
.query(By.css(`#${tabLabelElement.id}`)).nativeElement;
expect(tabContentElement.classList.contains('md-active')).toBe(true);
expect(tabContentElement.classList.contains('md-tab-active')).toBe(true);
}
});

Expand Down Expand Up @@ -226,6 +312,27 @@ class SimpleTabsTestApp {
}
}

@Component({
selector: 'test-app',
template: `
<md-tab-group class="tab-group">
<md-tab>
<template md-tab-label>Tab One</template>
<template md-tab-content>Tab one content</template>
</md-tab>
<md-tab disabled>
<template md-tab-label>Tab Two</template>
<template md-tab-content>Tab two content</template>
</md-tab>
<md-tab>
<template md-tab-label>Tab Three</template>
<template md-tab-content>Tab three content</template>
</md-tab>
</md-tab-group>
`,
})
class DisabledTabsTestApp {}

@Component({
selector: 'test-app',
template: `
Expand Down
66 changes: 51 additions & 15 deletions src/components/tabs/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ export class MdTabChangeEvent {
export class MdTab {
@ContentChild(MdTabLabel) label: MdTabLabel;
@ContentChild(MdTabContent) content: MdTabContent;

// TODO: Replace this when BooleanFieldValue is removed.
private _disabled = false;
@Input('disabled')
set disabled(value: boolean) {
this._disabled = (value != null && `${value}` !== 'false');
}
get disabled(): boolean {
return this._disabled;
}
}

/**
Expand All @@ -67,7 +77,7 @@ export class MdTabGroup {
private _selectedIndex: number = 0;
@Input()
set selectedIndex(value: number) {
if (value != this._selectedIndex) {
if (value != this._selectedIndex && this.isValidIndex(value)) {
this._selectedIndex = value;

if (this._isInitialized) {
Expand All @@ -79,6 +89,19 @@ export class MdTabGroup {
return this._selectedIndex;
}

/**
* Determines if an index is valid. If the tabs are not ready yet, we assume that the user is
* providing a valid index and return true.
*/
isValidIndex(index: number): boolean {
if (this._tabs) {
const tab = this._tabs.toArray()[index];
return tab && !tab.disabled;
} else {
return true;
}
}

/** Output to enable support for two-way binding on `selectedIndex`. */
@Output('selectedIndexChange') private get _selectedIndexChange(): Observable<number> {
return this.selectChange.map(event => event.index);
Expand Down Expand Up @@ -137,14 +160,16 @@ export class MdTabGroup {

/** When the focus index is set, we must manually send focus to the correct label */
set focusIndex(value: number) {
this._focusIndex = value;
if (this.isValidIndex(value)) {
this._focusIndex = value;

if (this._isInitialized) {
this._onFocusChange.emit(this._createChangeEvent(value));
}
if (this._isInitialized) {
this._onFocusChange.emit(this._createChangeEvent(value));
}

if (this._labelWrappers && this._labelWrappers.length) {
this._labelWrappers.toArray()[value].focus();
if (this._labelWrappers && this._labelWrappers.length) {
this._labelWrappers.toArray()[value].focus();
}
}
}

Expand Down Expand Up @@ -181,18 +206,29 @@ export class MdTabGroup {
}
}

/** Increment the focus index by 1; prevent going over the number of tabs */
focusNextTab(): void {
if (this._labelWrappers && this.focusIndex < this._labelWrappers.length - 1) {
this.focusIndex++;
/**
* Moves the focus left or right depending on the offset provided. Valid offsets are 1 and -1.
*/
moveFocus(offset: number) {
if (this._labelWrappers) {
const tabs: MdTab[] = this._tabs.toArray();
for (let i = this.focusIndex + offset; i < tabs.length && i >= 0; i += offset) {
if (this.isValidIndex(i)) {
this.focusIndex = i;
return;
}
}
}
}

/** Decrement the focus index by 1; prevent going below 0 */
/** Increment the focus index by 1 until a valid tab is found. */
focusNextTab(): void {
this.moveFocus(1);
}

/** Decrement the focus index by 1 until a valid tab is found. */
focusPreviousTab(): void {
if (this.focusIndex > 0) {
this.focusIndex--;
}
this.moveFocus(-1);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/demo-app/tabs/tab-group-demo.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<h1>Tab Group Demo</h1>

<md-tab-group class="demo-tab-group">
<md-tab *ngFor="let tab of tabs">
<md-tab *ngFor="let tab of tabs; let i = index" [disabled]="i == 1">
<template md-tab-label>{{tab.label}}</template>
<template md-tab-content>
{{tab.content}}
Expand All @@ -16,7 +16,7 @@ <h1>Tab Group Demo</h1>
<h1>Async Tabs</h1>

<md-tab-group class="demo-tab-group">
<md-tab *ngFor="let tab of asyncTabs | async">
<md-tab *ngFor="let tab of asyncTabs | async; let i = index" [disabled]="i == 1">
<template md-tab-label>{{tab.label}}</template>
<template md-tab-content>
{{tab.content}}
Expand Down

0 comments on commit 9d51deb

Please sign in to comment.