diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html
index 87a1e52e8830..e96d3b67e9cc 100644
--- a/src/demo-app/tabs/tabs-demo.html
+++ b/src/demo-app/tabs/tabs-demo.html
@@ -13,6 +13,7 @@
Tab Nav Bar
[active]="rla.isActive">
{{tabLink.label}}
+ Disabled Link
diff --git a/src/lib/tabs/_tabs-common.scss b/src/lib/tabs/_tabs-common.scss
index 56b5ff09ffac..c1ebd8464fc5 100644
--- a/src/lib/tabs/_tabs-common.scss
+++ b/src/lib/tabs/_tabs-common.scss
@@ -14,10 +14,16 @@ $mat-tab-animation-duration: 500ms !default;
opacity: 0.6;
min-width: 160px;
text-align: center;
+
&:focus {
outline: none;
opacity: 1;
}
+
+ &.mat-tab-disabled {
+ cursor: default;
+ pointer-events: none;
+ }
}
// Mixin styles for the top section of the view; contains the tab labels.
diff --git a/src/lib/tabs/index.ts b/src/lib/tabs/index.ts
index ec129b58e0e9..baeec59fce44 100644
--- a/src/lib/tabs/index.ts
+++ b/src/lib/tabs/index.ts
@@ -15,7 +15,7 @@ import {MdTab} from './tab';
import {MdTabGroup} from './tab-group';
import {MdTabLabel} from './tab-label';
import {MdTabLabelWrapper} from './tab-label-wrapper';
-import {MdTabNav, MdTabLink, MdTabLinkRipple} from './tab-nav-bar/tab-nav-bar';
+import {MdTabNav, MdTabLink} from './tab-nav-bar/tab-nav-bar';
import {MdInkBar} from './ink-bar';
import {MdTabBody} from './tab-body';
import {VIEWPORT_RULER_PROVIDER} from '../core/overlay/position/viewport-ruler';
@@ -38,7 +38,6 @@ import {ScrollDispatchModule} from '../core/overlay/scroll/index';
MdTab,
MdTabNav,
MdTabLink,
- MdTabLinkRipple
],
declarations: [
MdTabGroup,
@@ -49,7 +48,6 @@ import {ScrollDispatchModule} from '../core/overlay/scroll/index';
MdTabNav,
MdTabLink,
MdTabBody,
- MdTabLinkRipple,
MdTabHeader
],
providers: [VIEWPORT_RULER_PROVIDER],
diff --git a/src/lib/tabs/tab-group.scss b/src/lib/tabs/tab-group.scss
index 5ef96404f2b5..fb09e2b3aeef 100644
--- a/src/lib/tabs/tab-group.scss
+++ b/src/lib/tabs/tab-group.scss
@@ -55,9 +55,3 @@
overflow-y: hidden;
}
}
-
-// Styling for any tab that is marked disabled
-.mat-tab-disabled {
- cursor: default;
- pointer-events: none;
-}
diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
index dda7ef91e2c5..91a9be1357e7 100644
--- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
+++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
@@ -53,6 +53,58 @@ describe('MdTabNavBar', () => {
expect(fixture.componentInstance.activeIndex).toBe(2);
});
+ it('should add the disabled class if disabled', () => {
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ expect(tabLinkElements.every(tabLinkEl => !tabLinkEl.classList.contains('mat-tab-disabled')))
+ .toBe(true, 'Expected every tab link to not have the disabled class initially');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(tabLinkElements.every(tabLinkEl => tabLinkEl.classList.contains('mat-tab-disabled')))
+ .toBe(true, 'Expected every tab link to have the disabled class if set through binding');
+ });
+
+ it('should update aria-disabled if disabled', () => {
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'false'))
+ .toBe(true, 'Expected aria-disabled to be set to "false" by default.');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'true'))
+ .toBe(true, 'Expected aria-disabled to be set to "true" if link is disabled.');
+ });
+
+ it('should update the tabindex if links are disabled', () => {
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ expect(tabLinkElements.every(tabLink => tabLink.tabIndex === 0))
+ .toBe(true, 'Expected element to be keyboard focusable by default');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(tabLinkElements.every(tabLink => tabLink.tabIndex === -1))
+ .toBe(true, 'Expected element to no longer be keyboard focusable if disabled.');
+ });
+
+ it('should show ripples for tab links', () => {
+ const tabLink = fixture.debugElement.nativeElement.querySelector('.mat-tab-link');
+
+ dispatchMouseEvent(tabLink, 'mousedown');
+ dispatchMouseEvent(tabLink, 'mouseup');
+
+ expect(tabLink.querySelectorAll('.mat-ripple-element').length)
+ .toBe(1, 'Expected one ripple to show up if user clicks on tab link.');
+ });
+
it('should re-align the ink bar when the direction changes', () => {
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
@@ -125,6 +177,7 @@ describe('MdTabNavBar', () => {
Tab link {{label}}
@@ -135,6 +188,7 @@ class SimpleTabNavBarTestApp {
@ViewChild(MdTabNav) tabNavBar: MdTabNav;
label = '';
+ disabled: boolean = false;
tabs = [0, 1, 2];
activeIndex = 0;
diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts
index 34689baceacc..f870f34cadb8 100644
--- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts
+++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts
@@ -11,6 +11,7 @@ import {
Component,
Directive,
ElementRef,
+ HostBinding,
Inject,
Input,
NgZone,
@@ -20,15 +21,16 @@ import {
ViewEncapsulation
} from '@angular/core';
import {MdInkBar} from '../ink-bar';
-import {MdRipple} from '../../core/ripple/index';
+import {CanDisable, mixinDisabled} from '../../core/common-behaviors/disabled';
+import {MdRipple} from '../../core';
import {ViewportRuler} from '../../core/overlay/position/viewport-ruler';
import {Directionality, MD_RIPPLE_GLOBAL_OPTIONS, Platform, RippleGlobalOptions} from '../../core';
import {Observable} from 'rxjs/Observable';
+import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/auditTime';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/merge';
-import {Subject} from 'rxjs/Subject';
/**
* Navigation component matching the styles of the tab group header.
@@ -92,16 +94,30 @@ export class MdTabNav implements AfterContentInit, OnDestroy {
}
}
+
+// Boilerplate for applying mixins to MdTabLink.
+export class MdTabLinkBase {}
+export const _MdTabLinkMixinBase = mixinDisabled(MdTabLinkBase);
+
/**
* Link inside of a `md-tab-nav-bar`.
*/
@Directive({
selector: '[md-tab-link], [mat-tab-link], [mdTabLink], [matTabLink]',
- host: {'class': 'mat-tab-link'}
+ inputs: ['disabled'],
+ host: {
+ 'class': 'mat-tab-link',
+ '[attr.aria-disabled]': 'disabled.toString()',
+ '[class.mat-tab-disabled]': 'disabled'
+ }
})
-export class MdTabLink {
+export class MdTabLink extends _MdTabLinkMixinBase implements OnDestroy, CanDisable {
+ /** Whether the tab link is active or not. */
private _isActive: boolean = false;
+ /** Reference to the instance of the ripple for the tab link. */
+ private _tabLinkRipple: MdRipple;
+
/** Whether the link is active. */
@Input()
get active(): boolean { return this._isActive; }
@@ -112,23 +128,28 @@ export class MdTabLink {
}
}
- constructor(private _mdTabNavBar: MdTabNav, private _elementRef: ElementRef) {}
-}
+ /** @docs-private */
+ @HostBinding('tabIndex')
+ get tabIndex(): number {
+ return this.disabled ? -1 : 0;
+ }
-/**
- * Simple directive that extends the ripple and matches the selector of the MdTabLink. This
- * adds the ripple behavior to nav bar labels.
- */
-@Directive({
- selector: '[md-tab-link], [mat-tab-link], [mdTabLink], [matTabLink]',
-})
-export class MdTabLinkRipple extends MdRipple {
- constructor(
- elementRef: ElementRef,
- ngZone: NgZone,
- ruler: ViewportRuler,
- platform: Platform,
- @Optional() @Inject(MD_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {
- super(elementRef, ngZone, ruler, platform, globalOptions);
+ constructor(private _mdTabNavBar: MdTabNav,
+ private _elementRef: ElementRef,
+ ngZone: NgZone,
+ ruler: ViewportRuler,
+ platform: Platform,
+ @Optional() @Inject(MD_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {
+ super();
+
+ // Manually create a ripple instance that uses the tab link element as trigger element.
+ // Notice that the lifecycle hooks for the ripple config won't be called anymore.
+ this._tabLinkRipple = new MdRipple(_elementRef, ngZone, ruler, platform, globalOptions);
+ }
+
+ ngOnDestroy() {
+ // Manually call the ngOnDestroy lifecycle hook of the ripple instance because it won't be
+ // called automatically since its instance is not created by Angular.
+ this._tabLinkRipple.ngOnDestroy();
}
}