Skip to content

Commit

Permalink
feat(tab-nav-bar): support disabling tab links (#5257)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
devversion authored and jelbourn committed Jun 23, 2017
1 parent 8474671 commit fc809ed
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/demo-app/tabs/tabs-demo.html
Expand Up @@ -13,6 +13,7 @@ <h1>Tab Nav Bar</h1>
[active]="rla.isActive">
{{tabLink.label}}
</a>
<a md-tab-link disabled>Disabled Link</a>
</nav>
<router-outlet></router-outlet>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/lib/tabs/_tabs-common.scss
Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions src/lib/tabs/index.ts
Expand Up @@ -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';
Expand All @@ -38,7 +38,6 @@ import {ScrollDispatchModule} from '../core/overlay/scroll/index';
MdTab,
MdTabNav,
MdTabLink,
MdTabLinkRipple
],
declarations: [
MdTabGroup,
Expand All @@ -49,7 +48,6 @@ import {ScrollDispatchModule} from '../core/overlay/scroll/index';
MdTabNav,
MdTabLink,
MdTabBody,
MdTabLinkRipple,
MdTabHeader
],
providers: [VIEWPORT_RULER_PROVIDER],
Expand Down
6 changes: 0 additions & 6 deletions src/lib/tabs/tab-group.scss
Expand Up @@ -55,9 +55,3 @@
overflow-y: hidden;
}
}

// Styling for any tab that is marked disabled
.mat-tab-disabled {
cursor: default;
pointer-events: none;
}
54 changes: 54 additions & 0 deletions src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
Expand Up @@ -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;

Expand Down Expand Up @@ -125,6 +177,7 @@ describe('MdTabNavBar', () => {
<a md-tab-link
*ngFor="let tab of tabs; let index = index"
[active]="activeIndex === index"
[disabled]="disabled"
(click)="activeIndex = index">
Tab link {{label}}
</a>
Expand All @@ -135,6 +188,7 @@ class SimpleTabNavBarTestApp {
@ViewChild(MdTabNav) tabNavBar: MdTabNav;

label = '';
disabled: boolean = false;
tabs = [0, 1, 2];

activeIndex = 0;
Expand Down
63 changes: 42 additions & 21 deletions src/lib/tabs/tab-nav-bar/tab-nav-bar.ts
Expand Up @@ -11,6 +11,7 @@ import {
Component,
Directive,
ElementRef,
HostBinding,
Inject,
Input,
NgZone,
Expand All @@ -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.
Expand Down Expand Up @@ -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; }
Expand All @@ -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();
}
}

0 comments on commit fc809ed

Please sign in to comment.