Skip to content

feat(tab-nav-bar): support disabling tab links #5257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 23, 2017
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
1 change: 1 addition & 0 deletions src/demo-app/tabs/tabs-demo.html
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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();
}
}