Skip to content

Commit fc809ed

Browse files
devversionjelbourn
authored andcommitted
feat(tab-nav-bar): support disabling tab links (#5257)
Adds support for disabling tab links inside of the tab-nav-bar No longer requires having an extra directive for the ripples of tab links (no exposion of attributes like `mdRippleColor` - which could be flexible but should not be public API here) Closes #5208
1 parent 8474671 commit fc809ed

File tree

6 files changed

+104
-30
lines changed

6 files changed

+104
-30
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ <h1>Tab Nav Bar</h1>
1313
[active]="rla.isActive">
1414
{{tabLink.label}}
1515
</a>
16+
<a md-tab-link disabled>Disabled Link</a>
1617
</nav>
1718
<router-outlet></router-outlet>
1819
</div>

src/lib/tabs/_tabs-common.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ $mat-tab-animation-duration: 500ms !default;
1414
opacity: 0.6;
1515
min-width: 160px;
1616
text-align: center;
17+
1718
&:focus {
1819
outline: none;
1920
opacity: 1;
2021
}
22+
23+
&.mat-tab-disabled {
24+
cursor: default;
25+
pointer-events: none;
26+
}
2127
}
2228

2329
// Mixin styles for the top section of the view; contains the tab labels.

src/lib/tabs/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {MdTab} from './tab';
1515
import {MdTabGroup} from './tab-group';
1616
import {MdTabLabel} from './tab-label';
1717
import {MdTabLabelWrapper} from './tab-label-wrapper';
18-
import {MdTabNav, MdTabLink, MdTabLinkRipple} from './tab-nav-bar/tab-nav-bar';
18+
import {MdTabNav, MdTabLink} from './tab-nav-bar/tab-nav-bar';
1919
import {MdInkBar} from './ink-bar';
2020
import {MdTabBody} from './tab-body';
2121
import {VIEWPORT_RULER_PROVIDER} from '../core/overlay/position/viewport-ruler';
@@ -38,7 +38,6 @@ import {ScrollDispatchModule} from '../core/overlay/scroll/index';
3838
MdTab,
3939
MdTabNav,
4040
MdTabLink,
41-
MdTabLinkRipple
4241
],
4342
declarations: [
4443
MdTabGroup,
@@ -49,7 +48,6 @@ import {ScrollDispatchModule} from '../core/overlay/scroll/index';
4948
MdTabNav,
5049
MdTabLink,
5150
MdTabBody,
52-
MdTabLinkRipple,
5351
MdTabHeader
5452
],
5553
providers: [VIEWPORT_RULER_PROVIDER],

src/lib/tabs/tab-group.scss

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,3 @@
5555
overflow-y: hidden;
5656
}
5757
}
58-
59-
// Styling for any tab that is marked disabled
60-
.mat-tab-disabled {
61-
cursor: default;
62-
pointer-events: none;
63-
}

src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,58 @@ describe('MdTabNavBar', () => {
5353
expect(fixture.componentInstance.activeIndex).toBe(2);
5454
});
5555

56+
it('should add the disabled class if disabled', () => {
57+
const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
58+
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
59+
60+
expect(tabLinkElements.every(tabLinkEl => !tabLinkEl.classList.contains('mat-tab-disabled')))
61+
.toBe(true, 'Expected every tab link to not have the disabled class initially');
62+
63+
fixture.componentInstance.disabled = true;
64+
fixture.detectChanges();
65+
66+
expect(tabLinkElements.every(tabLinkEl => tabLinkEl.classList.contains('mat-tab-disabled')))
67+
.toBe(true, 'Expected every tab link to have the disabled class if set through binding');
68+
});
69+
70+
it('should update aria-disabled if disabled', () => {
71+
const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
72+
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
73+
74+
expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'false'))
75+
.toBe(true, 'Expected aria-disabled to be set to "false" by default.');
76+
77+
fixture.componentInstance.disabled = true;
78+
fixture.detectChanges();
79+
80+
expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'true'))
81+
.toBe(true, 'Expected aria-disabled to be set to "true" if link is disabled.');
82+
});
83+
84+
it('should update the tabindex if links are disabled', () => {
85+
const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
86+
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
87+
88+
expect(tabLinkElements.every(tabLink => tabLink.tabIndex === 0))
89+
.toBe(true, 'Expected element to be keyboard focusable by default');
90+
91+
fixture.componentInstance.disabled = true;
92+
fixture.detectChanges();
93+
94+
expect(tabLinkElements.every(tabLink => tabLink.tabIndex === -1))
95+
.toBe(true, 'Expected element to no longer be keyboard focusable if disabled.');
96+
});
97+
98+
it('should show ripples for tab links', () => {
99+
const tabLink = fixture.debugElement.nativeElement.querySelector('.mat-tab-link');
100+
101+
dispatchMouseEvent(tabLink, 'mousedown');
102+
dispatchMouseEvent(tabLink, 'mouseup');
103+
104+
expect(tabLink.querySelectorAll('.mat-ripple-element').length)
105+
.toBe(1, 'Expected one ripple to show up if user clicks on tab link.');
106+
});
107+
56108
it('should re-align the ink bar when the direction changes', () => {
57109
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
58110

@@ -125,6 +177,7 @@ describe('MdTabNavBar', () => {
125177
<a md-tab-link
126178
*ngFor="let tab of tabs; let index = index"
127179
[active]="activeIndex === index"
180+
[disabled]="disabled"
128181
(click)="activeIndex = index">
129182
Tab link {{label}}
130183
</a>
@@ -135,6 +188,7 @@ class SimpleTabNavBarTestApp {
135188
@ViewChild(MdTabNav) tabNavBar: MdTabNav;
136189

137190
label = '';
191+
disabled: boolean = false;
138192
tabs = [0, 1, 2];
139193

140194
activeIndex = 0;

src/lib/tabs/tab-nav-bar/tab-nav-bar.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Component,
1212
Directive,
1313
ElementRef,
14+
HostBinding,
1415
Inject,
1516
Input,
1617
NgZone,
@@ -20,15 +21,16 @@ import {
2021
ViewEncapsulation
2122
} from '@angular/core';
2223
import {MdInkBar} from '../ink-bar';
23-
import {MdRipple} from '../../core/ripple/index';
24+
import {CanDisable, mixinDisabled} from '../../core/common-behaviors/disabled';
25+
import {MdRipple} from '../../core';
2426
import {ViewportRuler} from '../../core/overlay/position/viewport-ruler';
2527
import {Directionality, MD_RIPPLE_GLOBAL_OPTIONS, Platform, RippleGlobalOptions} from '../../core';
2628
import {Observable} from 'rxjs/Observable';
29+
import {Subject} from 'rxjs/Subject';
2730
import 'rxjs/add/operator/auditTime';
2831
import 'rxjs/add/operator/takeUntil';
2932
import 'rxjs/add/observable/of';
3033
import 'rxjs/add/observable/merge';
31-
import {Subject} from 'rxjs/Subject';
3234

3335
/**
3436
* Navigation component matching the styles of the tab group header.
@@ -92,16 +94,30 @@ export class MdTabNav implements AfterContentInit, OnDestroy {
9294
}
9395
}
9496

97+
98+
// Boilerplate for applying mixins to MdTabLink.
99+
export class MdTabLinkBase {}
100+
export const _MdTabLinkMixinBase = mixinDisabled(MdTabLinkBase);
101+
95102
/**
96103
* Link inside of a `md-tab-nav-bar`.
97104
*/
98105
@Directive({
99106
selector: '[md-tab-link], [mat-tab-link], [mdTabLink], [matTabLink]',
100-
host: {'class': 'mat-tab-link'}
107+
inputs: ['disabled'],
108+
host: {
109+
'class': 'mat-tab-link',
110+
'[attr.aria-disabled]': 'disabled.toString()',
111+
'[class.mat-tab-disabled]': 'disabled'
112+
}
101113
})
102-
export class MdTabLink {
114+
export class MdTabLink extends _MdTabLinkMixinBase implements OnDestroy, CanDisable {
115+
/** Whether the tab link is active or not. */
103116
private _isActive: boolean = false;
104117

118+
/** Reference to the instance of the ripple for the tab link. */
119+
private _tabLinkRipple: MdRipple;
120+
105121
/** Whether the link is active. */
106122
@Input()
107123
get active(): boolean { return this._isActive; }
@@ -112,23 +128,28 @@ export class MdTabLink {
112128
}
113129
}
114130

115-
constructor(private _mdTabNavBar: MdTabNav, private _elementRef: ElementRef) {}
116-
}
131+
/** @docs-private */
132+
@HostBinding('tabIndex')
133+
get tabIndex(): number {
134+
return this.disabled ? -1 : 0;
135+
}
117136

118-
/**
119-
* Simple directive that extends the ripple and matches the selector of the MdTabLink. This
120-
* adds the ripple behavior to nav bar labels.
121-
*/
122-
@Directive({
123-
selector: '[md-tab-link], [mat-tab-link], [mdTabLink], [matTabLink]',
124-
})
125-
export class MdTabLinkRipple extends MdRipple {
126-
constructor(
127-
elementRef: ElementRef,
128-
ngZone: NgZone,
129-
ruler: ViewportRuler,
130-
platform: Platform,
131-
@Optional() @Inject(MD_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {
132-
super(elementRef, ngZone, ruler, platform, globalOptions);
137+
constructor(private _mdTabNavBar: MdTabNav,
138+
private _elementRef: ElementRef,
139+
ngZone: NgZone,
140+
ruler: ViewportRuler,
141+
platform: Platform,
142+
@Optional() @Inject(MD_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {
143+
super();
144+
145+
// Manually create a ripple instance that uses the tab link element as trigger element.
146+
// Notice that the lifecycle hooks for the ripple config won't be called anymore.
147+
this._tabLinkRipple = new MdRipple(_elementRef, ngZone, ruler, platform, globalOptions);
148+
}
149+
150+
ngOnDestroy() {
151+
// Manually call the ngOnDestroy lifecycle hook of the ripple instance because it won't be
152+
// called automatically since its instance is not created by Angular.
153+
this._tabLinkRipple.ngOnDestroy();
133154
}
134155
}

0 commit comments

Comments
 (0)