Skip to content

Commit 90feac2

Browse files
committed
feat(tab-nav-bar): allow setting tabindex for links
* For navigation bars, navigation through the arrow keys is not supported. Since every element needs to be tabbable for accessibility reasons, the tabindex should be changeable. * Also uses the tabindex mixin to reduce payload size.
1 parent 9673f63 commit 90feac2

File tree

2 files changed

+60
-13
lines changed

2 files changed

+60
-13
lines changed

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

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {By} from '@angular/platform-browser';
44
import {dispatchFakeEvent, dispatchMouseEvent} from '@angular/cdk/testing';
55
import {Direction, Directionality} from '@angular/cdk/bidi';
66
import {Subject} from 'rxjs/Subject';
7-
import {MatTabNav, MatTabsModule, MatTabLink} from '../index';
7+
import {MatTabLink, MatTabNav, MatTabsModule} from '../index';
88

99

1010
describe('MatTabNavBar', () => {
@@ -17,6 +17,8 @@ describe('MatTabNavBar', () => {
1717
declarations: [
1818
SimpleTabNavBarTestApp,
1919
TabLinkWithNgIf,
20+
TabLinkWithTabIndexBinding,
21+
TabLinkWithNativeTabindexAttr,
2022
],
2123
providers: [
2224
{provide: Directionality, useFactory: () => ({
@@ -119,9 +121,7 @@ describe('MatTabNavBar', () => {
119121
fixture.componentInstance.disabled = true;
120122
fixture.detectChanges();
121123

122-
expect(tabLinkElements.every(tabLink => {
123-
return tabLink.getAttribute('tabIndex') === null;
124-
}))
124+
expect(tabLinkElements.every(tabLink => tabLink.tabIndex === -1))
125125
.toBe(true, 'Expected element to no longer be keyboard focusable if disabled.');
126126
});
127127

@@ -212,6 +212,30 @@ describe('MatTabNavBar', () => {
212212
expect(link.querySelector('.mat-ripple-element'))
213213
.toBeFalsy('Expected no ripple to be created when ripple target is destroyed.');
214214
});
215+
216+
it('should support the native tabindex attribute', () => {
217+
const fixture = TestBed.createComponent(TabLinkWithNativeTabindexAttr);
218+
fixture.detectChanges();
219+
220+
const tabLink = fixture.debugElement.query(By.directive(MatTabLink)).injector.get(MatTabLink);
221+
222+
expect(tabLink.tabIndex)
223+
.toBe(5, 'Expected the tabIndex to be set from the native tabindex attribute.');
224+
});
225+
226+
it('should support binding to the tabIndex', () => {
227+
const fixture = TestBed.createComponent(TabLinkWithTabIndexBinding);
228+
fixture.detectChanges();
229+
230+
const tabLink = fixture.debugElement.query(By.directive(MatTabLink)).injector.get(MatTabLink);
231+
232+
expect(tabLink.tabIndex).toBe(0, 'Expected the tabIndex to be set to 0 by default.');
233+
234+
fixture.componentInstance.tabIndex = 3;
235+
fixture.detectChanges();
236+
237+
expect(tabLink.tabIndex).toBe(3, 'Expected the tabIndex to be have been set to 3.');
238+
});
215239
});
216240

217241
@Component({
@@ -249,3 +273,23 @@ class SimpleTabNavBarTestApp {
249273
class TabLinkWithNgIf {
250274
isDestroyed = false;
251275
}
276+
277+
@Component({
278+
template: `
279+
<nav mat-tab-nav-bar>
280+
<a mat-tab-link [tabIndex]="tabIndex">TabIndex Link</a>
281+
</nav>
282+
`
283+
})
284+
class TabLinkWithTabIndexBinding {
285+
tabIndex = 0;
286+
}
287+
288+
@Component({
289+
template: `
290+
<nav mat-tab-nav-bar>
291+
<a mat-tab-link tabindex="5">Link</a>
292+
</nav>
293+
`
294+
})
295+
class TabLinkWithNativeTabindexAttr {}

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {takeUntil} from '@angular/cdk/rxjs';
1313
import {ViewportRuler} from '@angular/cdk/scrolling';
1414
import {
1515
AfterContentInit,
16+
Attribute,
1617
ChangeDetectionStrategy,
1718
ChangeDetectorRef,
1819
Component,
@@ -34,11 +35,13 @@ import {
3435
CanColor,
3536
CanDisable,
3637
CanDisableRipple,
38+
HasTabIndex,
3739
MAT_RIPPLE_GLOBAL_OPTIONS,
3840
MatRipple,
3941
mixinColor,
4042
mixinDisabled,
4143
mixinDisableRipple,
44+
mixinTabIndex,
4245
RippleGlobalOptions,
4346
ThemePalette,
4447
} from '@angular/material/core';
@@ -171,15 +174,15 @@ export class MatTabNav extends _MatTabNavMixinBase implements AfterContentInit,
171174

172175
// Boilerplate for applying mixins to MatTabLink.
173176
export class MatTabLinkBase {}
174-
export const _MatTabLinkMixinBase = mixinDisabled(MatTabLinkBase);
177+
export const _MatTabLinkMixinBase = mixinTabIndex(mixinDisabled(MatTabLinkBase));
175178

176179
/**
177180
* Link inside of a `mat-tab-nav-bar`.
178181
*/
179182
@Directive({
180183
selector: '[mat-tab-link], [matTabLink]',
181184
exportAs: 'matTabLink',
182-
inputs: ['disabled'],
185+
inputs: ['disabled', 'tabIndex'],
183186
host: {
184187
'class': 'mat-tab-link',
185188
'[attr.aria-disabled]': 'disabled.toString()',
@@ -188,7 +191,9 @@ export const _MatTabLinkMixinBase = mixinDisabled(MatTabLinkBase);
188191
'[class.mat-tab-label-active]': 'active',
189192
}
190193
})
191-
export class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDisable {
194+
export class MatTabLink extends _MatTabLinkMixinBase
195+
implements OnDestroy, CanDisable, HasTabIndex {
196+
192197
/** Whether the tab link is active or not. */
193198
private _isActive: boolean = false;
194199

@@ -216,21 +221,19 @@ export class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDi
216221
this._tabLinkRipple._updateRippleRenderer();
217222
}
218223

219-
/** @docs-private */
220-
get tabIndex(): number | null {
221-
return this.disabled ? null : 0;
222-
}
223-
224224
constructor(private _tabNavBar: MatTabNav,
225225
private _elementRef: ElementRef,
226226
ngZone: NgZone,
227227
platform: Platform,
228-
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {
228+
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions,
229+
@Attribute('tabindex') tabIndex: string) {
229230
super();
230231

231232
// Manually create a ripple instance that uses the tab link element as trigger element.
232233
// Notice that the lifecycle hooks for the ripple config won't be called anymore.
233234
this._tabLinkRipple = new MatRipple(_elementRef, ngZone, platform, globalOptions);
235+
236+
this.tabIndex = parseInt(tabIndex) || 0;
234237
}
235238

236239
ngOnDestroy() {

0 commit comments

Comments
 (0)