Skip to content

Commit b9bfaee

Browse files
authored
fix(material/tabs): focus wrapping back to selected label when using shift + tab (#14194)
Currently the `tabindex` of each of the tab labels is determined by the selected index. This means that if the user tabbed into the header, pressed the right arrow and then pressed shift + tab, their focus would end up on the selected tab. These changes switch to basing the `tabindex` on the focused index which is tied both to the selected index and the user's keyboard navigation.
1 parent 81ff8c8 commit b9bfaee

File tree

4 files changed

+46
-5
lines changed

4 files changed

+46
-5
lines changed

src/material-experimental/mdc-tabs/tab-group.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {LEFT_ARROW} from '@angular/cdk/keycodes';
1+
import {LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
22
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
33
import {Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
44
import {
@@ -378,6 +378,23 @@ describe('MDC-based MatTabGroup', () => {
378378

379379
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
380380
});
381+
382+
it('should update the tabindex of the labels when navigating via keyboard', () => {
383+
fixture.detectChanges();
384+
385+
const tabLabels = fixture.debugElement
386+
.queryAll(By.css('.mat-mdc-tab'))
387+
.map(label => label.nativeElement);
388+
const tabLabelContainer = fixture.debugElement.query(By.css('.mat-mdc-tab-label-container'))
389+
.nativeElement as HTMLElement;
390+
391+
expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '0', '-1']);
392+
393+
dispatchKeyboardEvent(tabLabelContainer, 'keydown', RIGHT_ARROW);
394+
fixture.detectChanges();
395+
396+
expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '-1', '0']);
397+
});
381398
});
382399

383400
describe('aria labelling', () => {

src/material/tabs/tab-group.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {LEFT_ARROW} from '@angular/cdk/keycodes';
1+
import {LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
22
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
33
import {Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
44
import {
@@ -376,6 +376,23 @@ describe('MatTabGroup', () => {
376376

377377
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
378378
});
379+
380+
it('should update the tabindex of the labels when navigating via keyboard', () => {
381+
fixture.detectChanges();
382+
383+
const tabLabels = fixture.debugElement
384+
.queryAll(By.css('.mat-tab-label'))
385+
.map(label => label.nativeElement);
386+
const tabLabelContainer = fixture.debugElement.query(By.css('.mat-tab-label-container'))
387+
.nativeElement as HTMLElement;
388+
389+
expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '0', '-1']);
390+
391+
dispatchKeyboardEvent(tabLabelContainer, 'keydown', RIGHT_ARROW);
392+
fixture.detectChanges();
393+
394+
expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '-1', '0']);
395+
});
379396
});
380397

381398
describe('aria labelling', () => {

src/material/tabs/tab-group.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ export abstract class _MatTabGroupBase
9999
/** The tab index that should be selected after the content has been checked. */
100100
private _indexToSelect: number | null = 0;
101101

102+
/** Index of the tab that was focused last. */
103+
private _lastFocusedTabIndex: number | null = null;
104+
102105
/** Snapshot of the height of the tab body wrapper before another tab is activated. */
103106
private _tabBodyWrapperHeight: number = 0;
104107

@@ -288,6 +291,7 @@ export abstract class _MatTabGroupBase
288291

289292
if (this._selectedIndex !== indexToSelect) {
290293
this._selectedIndex = indexToSelect;
294+
this._lastFocusedTabIndex = null;
291295
this._changeDetectorRef.markForCheck();
292296
}
293297
}
@@ -313,6 +317,7 @@ export abstract class _MatTabGroupBase
313317
// event, otherwise the consumer may end up in an infinite loop in some edge cases like
314318
// adding a tab within the `selectedIndexChange` event.
315319
this._indexToSelect = this._selectedIndex = i;
320+
this._lastFocusedTabIndex = null;
316321
selectedTab = tabs[i];
317322
break;
318323
}
@@ -387,6 +392,7 @@ export abstract class _MatTabGroupBase
387392
}
388393

389394
_focusChanged(index: number) {
395+
this._lastFocusedTabIndex = index;
390396
this.focusChange.emit(this._createChangeEvent(index));
391397
}
392398

@@ -469,11 +475,12 @@ export abstract class _MatTabGroupBase
469475
}
470476

471477
/** Retrieves the tabindex for the tab. */
472-
_getTabIndex(tab: MatTab, idx: number): number | null {
478+
_getTabIndex(tab: MatTab, index: number): number | null {
473479
if (tab.disabled) {
474480
return null;
475481
}
476-
return this.selectedIndex === idx ? 0 : -1;
482+
const targetIndex = this._lastFocusedTabIndex ?? this.selectedIndex;
483+
return index === targetIndex ? 0 : -1;
477484
}
478485

479486
/** Callback for when the focused state of a tab has changed. */

tools/public_api_guard/material/tabs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
242242
_focusChanged(index: number): void;
243243
focusTab(index: number): void;
244244
_getTabContentId(i: number): string;
245-
_getTabIndex(tab: MatTab, idx: number): number | null;
245+
_getTabIndex(tab: MatTab, index: number): number | null;
246246
_getTabLabelId(i: number): string;
247247
_handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number): void;
248248
headerPosition: MatTabHeaderPosition;

0 commit comments

Comments
 (0)