Skip to content

Commit a041253

Browse files
devversionjelbourn
authored andcommitted
feat(tab-nav-bar): allow setting tabindex for links (#7809)
* 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 8e9dade commit a041253

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
@@ -12,6 +12,7 @@ import {takeUntil} from 'rxjs/operators/takeUntil';
1212
import {ViewportRuler} from '@angular/cdk/scrolling';
1313
import {
1414
AfterContentInit,
15+
Attribute,
1516
ChangeDetectionStrategy,
1617
ChangeDetectorRef,
1718
Component,
@@ -33,11 +34,13 @@ import {
3334
CanColor,
3435
CanDisable,
3536
CanDisableRipple,
37+
HasTabIndex,
3638
MAT_RIPPLE_GLOBAL_OPTIONS,
3739
MatRipple,
3840
mixinColor,
3941
mixinDisabled,
4042
mixinDisableRipple,
43+
mixinTabIndex,
4144
RippleGlobalOptions,
4245
ThemePalette,
4346
} from '@angular/material/core';
@@ -170,15 +173,15 @@ export class MatTabNav extends _MatTabNavMixinBase implements AfterContentInit,
170173

171174
// Boilerplate for applying mixins to MatTabLink.
172175
export class MatTabLinkBase {}
173-
export const _MatTabLinkMixinBase = mixinDisabled(MatTabLinkBase);
176+
export const _MatTabLinkMixinBase = mixinTabIndex(mixinDisabled(MatTabLinkBase));
174177

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

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

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

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

235238
ngOnDestroy() {

0 commit comments

Comments
 (0)