Skip to content

Commit 9d51deb

Browse files
robertmesserlejelbourn
authored andcommitted
feat(tabs): adds support for disabled tabs (#934)
closes #880
1 parent 22d70ae commit 9d51deb

File tree

6 files changed

+172
-23
lines changed

6 files changed

+172
-23
lines changed

e2e/components/tabs/tabs.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function getFocusStates(elements: ElementArrayFinder) {
7878
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
7979
*/
8080
function getActiveStates(elements: ElementArrayFinder) {
81-
return getClassStates(elements, 'md-active');
81+
return getClassStates(elements, 'md-tab-active');
8282
}
8383

8484
/**

src/components/tabs/tab-group.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
[tabIndex]="selectedIndex == i ? 0 : -1"
77
[attr.aria-controls]="_getTabContentId(i)"
88
[attr.aria-selected]="selectedIndex == i"
9-
[class.md-active]="selectedIndex == i"
9+
[class.md-tab-active]="selectedIndex == i"
10+
[class.md-tab-disabled]="tab.disabled"
1011
(click)="focusIndex = selectedIndex = i">
1112
<template [portalHost]="tab.label"></template>
1213
</div>
@@ -17,7 +18,7 @@
1718
role="tabpanel"
1819
*ngFor="let tab of _tabs; let i = index"
1920
[id]="_getTabContentId(i)"
20-
[class.md-active]="selectedIndex == i"
21+
[class.md-tab-active]="selectedIndex == i"
2122
[attr.aria-labelledby]="_getTabLabelId(i)">
2223
<template [ngIf]="selectedIndex == i">
2324
<template [portalHost]="tab.content"></template>

src/components/tabs/tab-group.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ $md-tab-bar-height: 48px !default;
4040
}
4141
}
4242

43+
.md-tab-disabled {
44+
cursor: default;
45+
pointer-events: none;
46+
}
47+
4348
/** The bottom section of the view; contains the tab bodies */
4449
.md-tab-body-wrapper {
4550
position: relative;

src/components/tabs/tab-group.spec.ts

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,92 @@ describe('MdTabGroup', () => {
151151
}));
152152
});
153153

154+
describe('disabled tabs', () => {
155+
let fixture: ComponentFixture<DisabledTabsTestApp>;
156+
157+
beforeEach(async(() => {
158+
builder.createAsync(DisabledTabsTestApp).then(f => {
159+
fixture = f;
160+
fixture.detectChanges();
161+
});
162+
}));
163+
164+
it('should disable the second tab', () => {
165+
let labels = fixture.debugElement.queryAll(By.css('.md-tab-label'));
166+
167+
expect(labels[1].nativeElement.classList.contains('md-tab-disabled')).toBeTruthy();
168+
});
169+
170+
it('should skip over disabled tabs when navigating by keyboard', () => {
171+
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
172+
.componentInstance;
173+
174+
component.focusIndex = 0;
175+
component.focusNextTab();
176+
177+
expect(component.focusIndex).toBe(2);
178+
179+
component.focusNextTab();
180+
expect(component.focusIndex).toBe(2);
181+
182+
component.focusPreviousTab();
183+
expect(component.focusIndex).toBe(0);
184+
185+
component.focusPreviousTab();
186+
expect(component.focusIndex).toBe(0);
187+
});
188+
189+
it('should ignore attempts to select a disabled tab', () => {
190+
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
191+
.componentInstance;
192+
193+
component.selectedIndex = 0;
194+
expect(component.selectedIndex).toBe(0);
195+
196+
component.selectedIndex = 1;
197+
expect(component.selectedIndex).toBe(0);
198+
});
199+
200+
it('should ignore attempts to focus a disabled tab', () => {
201+
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
202+
.componentInstance;
203+
204+
component.focusIndex = 0;
205+
expect(component.focusIndex).toBe(0);
206+
207+
component.focusIndex = 1;
208+
expect(component.focusIndex).toBe(0);
209+
});
210+
211+
it('should ignore attempts to set invalid selectedIndex', () => {
212+
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
213+
.componentInstance;
214+
215+
component.selectedIndex = 0;
216+
expect(component.selectedIndex).toBe(0);
217+
218+
component.selectedIndex = -1;
219+
expect(component.selectedIndex).toBe(0);
220+
221+
component.selectedIndex = 4;
222+
expect(component.selectedIndex).toBe(0);
223+
});
224+
225+
it('should ignore attempts to set invalid focusIndex', () => {
226+
let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group'))
227+
.componentInstance;
228+
229+
component.focusIndex = 0;
230+
expect(component.focusIndex).toBe(0);
231+
232+
component.focusIndex = -1;
233+
expect(component.focusIndex).toBe(0);
234+
235+
component.focusIndex = 4;
236+
expect(component.focusIndex).toBe(0);
237+
});
238+
});
239+
154240
describe('async tabs', () => {
155241
let fixture: ComponentFixture<AsyncTabsTestApp>;
156242

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

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

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

189275
let tabContentElement = fixture.debugElement
190276
.query(By.css(`#${tabLabelElement.id}`)).nativeElement;
191-
expect(tabContentElement.classList.contains('md-active')).toBe(true);
277+
expect(tabContentElement.classList.contains('md-tab-active')).toBe(true);
192278
}
193279
});
194280

@@ -226,6 +312,27 @@ class SimpleTabsTestApp {
226312
}
227313
}
228314

315+
@Component({
316+
selector: 'test-app',
317+
template: `
318+
<md-tab-group class="tab-group">
319+
<md-tab>
320+
<template md-tab-label>Tab One</template>
321+
<template md-tab-content>Tab one content</template>
322+
</md-tab>
323+
<md-tab disabled>
324+
<template md-tab-label>Tab Two</template>
325+
<template md-tab-content>Tab two content</template>
326+
</md-tab>
327+
<md-tab>
328+
<template md-tab-label>Tab Three</template>
329+
<template md-tab-content>Tab three content</template>
330+
</md-tab>
331+
</md-tab-group>
332+
`,
333+
})
334+
class DisabledTabsTestApp {}
335+
229336
@Component({
230337
selector: 'test-app',
231338
template: `

src/components/tabs/tabs.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ export class MdTabChangeEvent {
4343
export class MdTab {
4444
@ContentChild(MdTabLabel) label: MdTabLabel;
4545
@ContentChild(MdTabContent) content: MdTabContent;
46+
47+
// TODO: Replace this when BooleanFieldValue is removed.
48+
private _disabled = false;
49+
@Input('disabled')
50+
set disabled(value: boolean) {
51+
this._disabled = (value != null && `${value}` !== 'false');
52+
}
53+
get disabled(): boolean {
54+
return this._disabled;
55+
}
4656
}
4757

4858
/**
@@ -67,7 +77,7 @@ export class MdTabGroup {
6777
private _selectedIndex: number = 0;
6878
@Input()
6979
set selectedIndex(value: number) {
70-
if (value != this._selectedIndex) {
80+
if (value != this._selectedIndex && this.isValidIndex(value)) {
7181
this._selectedIndex = value;
7282

7383
if (this._isInitialized) {
@@ -79,6 +89,19 @@ export class MdTabGroup {
7989
return this._selectedIndex;
8090
}
8191

92+
/**
93+
* Determines if an index is valid. If the tabs are not ready yet, we assume that the user is
94+
* providing a valid index and return true.
95+
*/
96+
isValidIndex(index: number): boolean {
97+
if (this._tabs) {
98+
const tab = this._tabs.toArray()[index];
99+
return tab && !tab.disabled;
100+
} else {
101+
return true;
102+
}
103+
}
104+
82105
/** Output to enable support for two-way binding on `selectedIndex`. */
83106
@Output('selectedIndexChange') private get _selectedIndexChange(): Observable<number> {
84107
return this.selectChange.map(event => event.index);
@@ -137,14 +160,16 @@ export class MdTabGroup {
137160

138161
/** When the focus index is set, we must manually send focus to the correct label */
139162
set focusIndex(value: number) {
140-
this._focusIndex = value;
163+
if (this.isValidIndex(value)) {
164+
this._focusIndex = value;
141165

142-
if (this._isInitialized) {
143-
this._onFocusChange.emit(this._createChangeEvent(value));
144-
}
166+
if (this._isInitialized) {
167+
this._onFocusChange.emit(this._createChangeEvent(value));
168+
}
145169

146-
if (this._labelWrappers && this._labelWrappers.length) {
147-
this._labelWrappers.toArray()[value].focus();
170+
if (this._labelWrappers && this._labelWrappers.length) {
171+
this._labelWrappers.toArray()[value].focus();
172+
}
148173
}
149174
}
150175

@@ -181,18 +206,29 @@ export class MdTabGroup {
181206
}
182207
}
183208

184-
/** Increment the focus index by 1; prevent going over the number of tabs */
185-
focusNextTab(): void {
186-
if (this._labelWrappers && this.focusIndex < this._labelWrappers.length - 1) {
187-
this.focusIndex++;
209+
/**
210+
* Moves the focus left or right depending on the offset provided. Valid offsets are 1 and -1.
211+
*/
212+
moveFocus(offset: number) {
213+
if (this._labelWrappers) {
214+
const tabs: MdTab[] = this._tabs.toArray();
215+
for (let i = this.focusIndex + offset; i < tabs.length && i >= 0; i += offset) {
216+
if (this.isValidIndex(i)) {
217+
this.focusIndex = i;
218+
return;
219+
}
220+
}
188221
}
189222
}
190223

191-
/** Decrement the focus index by 1; prevent going below 0 */
224+
/** Increment the focus index by 1 until a valid tab is found. */
225+
focusNextTab(): void {
226+
this.moveFocus(1);
227+
}
228+
229+
/** Decrement the focus index by 1 until a valid tab is found. */
192230
focusPreviousTab(): void {
193-
if (this.focusIndex > 0) {
194-
this.focusIndex--;
195-
}
231+
this.moveFocus(-1);
196232
}
197233
}
198234

src/demo-app/tabs/tab-group-demo.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<h1>Tab Group Demo</h1>
22

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

1818
<md-tab-group class="demo-tab-group">
19-
<md-tab *ngFor="let tab of asyncTabs | async">
19+
<md-tab *ngFor="let tab of asyncTabs | async; let i = index" [disabled]="i == 1">
2020
<template md-tab-label>{{tab.label}}</template>
2121
<template md-tab-content>
2222
{{tab.content}}

0 commit comments

Comments
 (0)