Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
Copy link
Member

@jelbourn jelbourn Jul 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add TODO

TODO: Replace this when BooleanFieldValue is removed.

}
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't have any tabs, why is it true instead of false? Wouldn't any index be "invalid", then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I should add a comment explaining why. When the component is first loaded, the tabs aren't ready yet, so we allow any value to pass through. In the case of async tabs, we don't know when, exactly, the tabs will be ready, so I handle it in a generic way that does nothing while the component is in an invalid state (ie. no tabs).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it is kind of explained in the method comment

}
}

/** 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