Skip to content

Commit 1e826ff

Browse files
committed
fix(material/tabs): avoid not having any focusable tabs
In #26397 we made it possible to navigate to disabled tabs with the keyboard, however in the tabs nav bar they're still set to `tabindex="-1"` unless they're active. This means that if the nav bar only has disabled tabs, there's no way to enter it. These changes make it so the tab index is based on the focus index which ensures that at least one tab is always focusable. It also aligns the behavior with `mat-tab-group`.
1 parent 83006db commit 1e826ff

File tree

4 files changed

+35
-27
lines changed

4 files changed

+35
-27
lines changed

goldens/material/tabs/index.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Direction } from '@angular/cdk/bidi';
1515
import { ElementRef } from '@angular/core';
1616
import { EventEmitter } from '@angular/core';
1717
import { FocusableOption } from '@angular/cdk/a11y';
18+
import { FocusKeyManager } from '@angular/cdk/a11y';
1819
import { FocusOrigin } from '@angular/cdk/a11y';
1920
import * as i0 from '@angular/core';
2021
import * as i1 from '@angular/cdk/bidi';
@@ -98,6 +99,7 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
9899
// (undocumented)
99100
abstract _items: QueryList<MatPaginatedTabHeaderItem>;
100101
protected abstract _itemSelected(event: KeyboardEvent): void;
102+
protected _keyManager: FocusKeyManager<MatPaginatedTabHeaderItem> | undefined;
101103
// (undocumented)
102104
abstract _nextPaginator: ElementRef<HTMLElement>;
103105
// (undocumented)
@@ -458,6 +460,8 @@ export class MatTabNav extends MatPaginatedTabHeader implements AfterContentInit
458460
// (undocumented)
459461
_getRole(): string | null;
460462
// (undocumented)
463+
_hasFocus(link: MatTabLink): boolean;
464+
// (undocumented)
461465
_inkBar: MatInkBar;
462466
_items: QueryList<MatTabLink>;
463467
// (undocumented)

src/material/tabs/paginated-tab-header.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export abstract class MatPaginatedTabHeader
119119
private _scrollDistanceChanged: boolean;
120120

121121
/** Used to manage focus between the tabs. */
122-
private _keyManager: FocusKeyManager<MatPaginatedTabHeaderItem>;
122+
protected _keyManager: FocusKeyManager<MatPaginatedTabHeaderItem> | undefined;
123123

124124
/** Cached text content of the header. */
125125
private _currentTextContent: string;
@@ -218,7 +218,9 @@ export abstract class MatPaginatedTabHeader
218218
// Allow focus to land on disabled tabs, as per https://w3c.github.io/aria-practices/#kbd_disabled_controls
219219
.skipPredicate(() => false);
220220

221-
this._keyManager.updateActiveItem(this._selectedIndex);
221+
// Fall back to the first link as being active if there isn't a selected one.
222+
// This is relevant primarily for the tab nav bar.
223+
this._keyManager.updateActiveItem(Math.max(this._selectedIndex, 0));
222224

223225
// Note: We do not need to realign after the first render for proper functioning of the tabs
224226
// the resize events above should fire when we first start observing the element. However,
@@ -243,7 +245,7 @@ export abstract class MatPaginatedTabHeader
243245
realign();
244246
});
245247
});
246-
this._keyManager.withHorizontalOrientation(this._getLayoutDirection());
248+
this._keyManager?.withHorizontalOrientation(this._getLayoutDirection());
247249
});
248250

249251
// If there is a change in the focus key manager we need to emit the `indexFocused`
@@ -339,7 +341,7 @@ export abstract class MatPaginatedTabHeader
339341
}
340342
break;
341343
default:
342-
this._keyManager.onKeydown(event);
344+
this._keyManager?.onKeydown(event);
343345
}
344346
}
345347

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

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,6 @@ describe('MatTabNavBar', () => {
107107
.toBe(true);
108108
});
109109

110-
it('should update the tabindex if links are disabled', () => {
111-
const tabLinkElements = fixture.debugElement
112-
.queryAll(By.css('a'))
113-
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
114-
115-
expect(tabLinkElements.map(tabLink => tabLink.tabIndex))
116-
.withContext('Expected first element to be keyboard focusable by default')
117-
.toEqual([0, -1, -1]);
118-
119-
fixture.componentInstance.disabled = true;
120-
fixture.changeDetectorRef.markForCheck();
121-
fixture.detectChanges();
122-
123-
expect(tabLinkElements.every(tabLink => tabLink.tabIndex === -1))
124-
.withContext('Expected element to no longer be keyboard focusable if disabled.')
125-
.toBe(true);
126-
});
127-
128110
it('should mark disabled links', () => {
129111
const tabLinkElement = fixture.debugElement.query(By.css('a')).nativeElement;
130112

@@ -303,6 +285,15 @@ describe('MatTabNavBar', () => {
303285
expect(tabLinks[2].tabIndex).toBe(-1);
304286
});
305287

288+
it('should set a tabindex even if the only tab is disabled', () => {
289+
const fixture = TestBed.createComponent(TabBarWithDisabledTabOnInit);
290+
fixture.detectChanges();
291+
292+
const tab: HTMLElement = fixture.nativeElement.querySelector('.mat-mdc-tab-link');
293+
expect(tab.getAttribute('aria-disabled')).toBe('true');
294+
expect(tab.tabIndex).toBe(0);
295+
});
296+
306297
it('should setup aria-controls properly', () => {
307298
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
308299
fixture.detectChanges();
@@ -642,3 +633,14 @@ class TabBarWithInactiveTabsOnInit {
642633
class TabsWithCustomAnimationDuration {
643634
links = ['First', 'Second', 'Third'];
644635
}
636+
637+
@Component({
638+
template: `
639+
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
640+
<a mat-tab-link disabled>Hello</a>
641+
</nav>
642+
<mat-tab-nav-panel #tabPanel>Tab panel</mat-tab-nav-panel>
643+
`,
644+
imports: [MatTabsModule],
645+
})
646+
class TabBarWithDisabledTabOnInit {}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export class MatTabNav extends MatPaginatedTabHeader implements AfterContentInit
219219
_getRole(): string | null {
220220
return this.tabPanel ? 'tablist' : this._elementRef.nativeElement.getAttribute('role');
221221
}
222+
223+
_hasFocus(link: MatTabLink): boolean {
224+
return this._keyManager?.activeItem === link;
225+
}
222226
}
223227

224228
/**
@@ -395,11 +399,7 @@ export class MatTabLink
395399
}
396400

397401
_getTabIndex(): number {
398-
if (this._tabNavBar.tabPanel) {
399-
return this._isActive && !this.disabled ? 0 : -1;
400-
} else {
401-
return this.disabled ? -1 : this.tabIndex;
402-
}
402+
return this._tabNavBar._hasFocus(this) ? this.tabIndex : -1;
403403
}
404404
}
405405

0 commit comments

Comments
 (0)