mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(tabs): use common logic for handling keyboard focus (#9819)
Switches the tab header component to use the `FocusKeyManager` to manage the focused item, rather than implementing the logic itself.
This commit is contained in:
committed by
Andrew Seguin
parent
260afcf470
commit
6bd90e8141
@@ -15,6 +15,7 @@ ng_module(
|
||||
] + glob(["**/*.html"]),
|
||||
deps = [
|
||||
"//src/lib/core",
|
||||
"//src/cdk/a11y",
|
||||
"//src/cdk/bidi",
|
||||
"//src/cdk/coercion",
|
||||
"//src/cdk/observers",
|
||||
|
||||
@@ -47,11 +47,14 @@ describe('MatTabHeader', () => {
|
||||
}));
|
||||
|
||||
describe('focusing', () => {
|
||||
let tabListContainer: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
appComponent = fixture.componentInstance;
|
||||
tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
||||
});
|
||||
|
||||
it('should initialize to the selected index', () => {
|
||||
@@ -83,12 +86,12 @@ describe('MatTabHeader', () => {
|
||||
|
||||
// Move focus right, verify that the disabled tab is 1 and should be skipped
|
||||
expect(appComponent.disabledTabIndex).toBe(1);
|
||||
appComponent.tabHeader._focusNextTab();
|
||||
dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(2);
|
||||
|
||||
// Move focus right to index 3
|
||||
appComponent.tabHeader._focusNextTab();
|
||||
dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
||||
});
|
||||
@@ -99,13 +102,13 @@ describe('MatTabHeader', () => {
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
||||
|
||||
// Move focus left to index 3
|
||||
appComponent.tabHeader._focusPreviousTab();
|
||||
dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(2);
|
||||
|
||||
// Move focus left, verify that the disabled tab is 1 and should be skipped
|
||||
expect(appComponent.disabledTabIndex).toBe(1);
|
||||
appComponent.tabHeader._focusPreviousTab();
|
||||
dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
||||
});
|
||||
@@ -115,8 +118,6 @@ describe('MatTabHeader', () => {
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
||||
|
||||
let tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
||||
|
||||
// Move focus right to 2
|
||||
dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
@@ -147,7 +148,6 @@ describe('MatTabHeader', () => {
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
||||
|
||||
const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
||||
const event = dispatchKeyboardEvent(tabListContainer, 'keydown', HOME);
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -161,7 +161,6 @@ describe('MatTabHeader', () => {
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
||||
|
||||
const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
||||
dispatchKeyboardEvent(tabListContainer, 'keydown', HOME);
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -174,7 +173,6 @@ describe('MatTabHeader', () => {
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
||||
|
||||
const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
||||
const event = dispatchKeyboardEvent(tabListContainer, 'keydown', END);
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -188,7 +186,6 @@ describe('MatTabHeader', () => {
|
||||
fixture.detectChanges();
|
||||
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
||||
|
||||
const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
||||
dispatchKeyboardEvent(tabListContainer, 'keydown', END);
|
||||
fixture.detectChanges();
|
||||
|
||||
|
||||
+26
-64
@@ -8,7 +8,7 @@
|
||||
|
||||
import {Direction, Directionality} from '@angular/cdk/bidi';
|
||||
import {coerceNumberProperty} from '@angular/cdk/coercion';
|
||||
import {END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
|
||||
import {END, ENTER, HOME, SPACE} from '@angular/cdk/keycodes';
|
||||
import {ViewportRuler} from '@angular/cdk/scrolling';
|
||||
import {
|
||||
AfterContentChecked,
|
||||
@@ -31,6 +31,7 @@ import {CanDisableRipple, mixinDisableRipple} from '@angular/material/core';
|
||||
import {merge, of as observableOf, Subscription} from 'rxjs';
|
||||
import {MatInkBar} from './ink-bar';
|
||||
import {MatTabLabelWrapper} from './tab-label-wrapper';
|
||||
import {FocusKeyManager} from '@angular/cdk/a11y';
|
||||
|
||||
|
||||
/**
|
||||
@@ -80,9 +81,6 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
@ViewChild('tabListContainer') _tabListContainer: ElementRef;
|
||||
@ViewChild('tabList') _tabList: ElementRef;
|
||||
|
||||
/** The tab index that is focused. */
|
||||
private _focusIndex: number = 0;
|
||||
|
||||
/** The distance in pixels that the tab labels should be translated to the left. */
|
||||
private _scrollDistance = 0;
|
||||
|
||||
@@ -110,6 +108,9 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
/** Whether the scroll distance has changed and should be applied after the view is checked. */
|
||||
private _scrollDistanceChanged: boolean;
|
||||
|
||||
/** Used to manage focus between the tabs. */
|
||||
private _keyManager: FocusKeyManager<MatTabLabelWrapper>;
|
||||
|
||||
private _selectedIndex: number = 0;
|
||||
|
||||
/** The index of the active tab. */
|
||||
@@ -119,7 +120,10 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
value = coerceNumberProperty(value);
|
||||
this._selectedIndexChanged = this._selectedIndex != value;
|
||||
this._selectedIndex = value;
|
||||
this._focusIndex = value;
|
||||
|
||||
if (this._keyManager) {
|
||||
this._keyManager.updateActiveItemIndex(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** Event emitted when the option is selected. */
|
||||
@@ -164,18 +168,12 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
|
||||
_handleKeydown(event: KeyboardEvent) {
|
||||
switch (event.keyCode) {
|
||||
case RIGHT_ARROW:
|
||||
this._focusNextTab();
|
||||
break;
|
||||
case LEFT_ARROW:
|
||||
this._focusPreviousTab();
|
||||
break;
|
||||
case HOME:
|
||||
this._focusFirstTab();
|
||||
this._keyManager.setFirstItemActive();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case END:
|
||||
this._focusLastTab();
|
||||
this._keyManager.setLastItemActive();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case ENTER:
|
||||
@@ -183,6 +181,8 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
this.selectFocusedIndex.emit(this.focusIndex);
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
this._keyManager.onKeydown(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,10 +197,19 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
this._alignInkBarToSelectedTab();
|
||||
};
|
||||
|
||||
this._keyManager = new FocusKeyManager(this._labelWrappers)
|
||||
.withHorizontalOrientation(this._getLayoutDirection());
|
||||
|
||||
this._keyManager.updateActiveItemIndex(0);
|
||||
|
||||
// Defer the first call in order to allow for slower browsers to lay out the elements.
|
||||
// This helps in cases where the user lands directly on a page with paginated tabs.
|
||||
typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(realign) : realign();
|
||||
this._realignInkBar = merge(dirChange, resize).subscribe(realign);
|
||||
|
||||
this._realignInkBar = merge(dirChange, resize).subscribe(() => {
|
||||
realign();
|
||||
this._keyManager.withHorizontalOrientation(this._getLayoutDirection());
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -227,14 +236,14 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
|
||||
/** Tracks which element has focus; used for keyboard navigation */
|
||||
get focusIndex(): number {
|
||||
return this._focusIndex;
|
||||
return this._keyManager ? this._keyManager.activeItemIndex! : 0;
|
||||
}
|
||||
|
||||
/** When the focus index is set, we must manually send focus to the correct label */
|
||||
set focusIndex(value: number) {
|
||||
if (!this._isValidIndex(value) || this._focusIndex == value) { return; }
|
||||
if (!this._isValidIndex(value) || this.focusIndex == value || !this._keyManager) { return; }
|
||||
|
||||
this._focusIndex = value;
|
||||
this._keyManager.setActiveItem(value);
|
||||
this.indexFocused.emit(value);
|
||||
this._setTabFocus(value);
|
||||
}
|
||||
@@ -276,53 +285,6 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the focus towards the beginning or the end of the list depending on the offset provided.
|
||||
* Valid offsets are 1 and -1.
|
||||
*/
|
||||
_moveFocus(offset: number) {
|
||||
if (this._labelWrappers) {
|
||||
const tabs: MatTabLabelWrapper[] = this._labelWrappers.toArray();
|
||||
|
||||
for (let i = this.focusIndex + offset; i < tabs.length && i >= 0; i += offset) {
|
||||
if (this._isValidIndex(i)) {
|
||||
this.focusIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Increment the focus index by 1 until a valid tab is found. */
|
||||
_focusNextTab(): void {
|
||||
this._moveFocus(this._getLayoutDirection() == 'ltr' ? 1 : -1);
|
||||
}
|
||||
|
||||
/** Decrement the focus index by 1 until a valid tab is found. */
|
||||
_focusPreviousTab(): void {
|
||||
this._moveFocus(this._getLayoutDirection() == 'ltr' ? -1 : 1);
|
||||
}
|
||||
|
||||
/** Focuses the first tab. */
|
||||
private _focusFirstTab(): void {
|
||||
for (let i = 0; i < this._labelWrappers.length; i++) {
|
||||
if (this._isValidIndex(i)) {
|
||||
this.focusIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Focuses the last tab. */
|
||||
private _focusLastTab(): void {
|
||||
for (let i = this._labelWrappers.length - 1; i > -1; i--) {
|
||||
if (this._isValidIndex(i)) {
|
||||
this.focusIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The layout direction of the containing app. */
|
||||
_getLayoutDirection(): Direction {
|
||||
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
|
||||
|
||||
Reference in New Issue
Block a user