From fc809ede4bf949caf413f51cad9648bd0ba2860a Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 23 Jun 2017 21:57:55 +0200 Subject: [PATCH] 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 --- src/demo-app/tabs/tabs-demo.html | 1 + src/lib/tabs/_tabs-common.scss | 6 ++ src/lib/tabs/index.ts | 4 +- src/lib/tabs/tab-group.scss | 6 -- src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts | 54 +++++++++++++++++ src/lib/tabs/tab-nav-bar/tab-nav-bar.ts | 63 +++++++++++++------- 6 files changed, 104 insertions(+), 30 deletions(-) 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(); } }