Skip to content

Commit 1b7b8ab

Browse files
devversionjosephperrott
authored andcommitted
fix(tab-group): focus change event not firing for keyboard navigation (#12192)
1 parent 944caf9 commit 1b7b8ab

File tree

2 files changed

+63
-10
lines changed

2 files changed

+63
-10
lines changed

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {dispatchFakeEvent} from '@angular/cdk/testing';
1+
import {LEFT_ARROW} from '@angular/cdk/keycodes';
2+
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
23
import {Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
34
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
45
import {By} from '@angular/platform-browser';
@@ -232,6 +233,46 @@ describe('MatTabGroup', () => {
232233
expect(labels.every(label => label.getAttribute('aria-setsize') === '3')).toBe(true);
233234
});
234235

236+
it('should emit focusChange event on click', () => {
237+
spyOn(fixture.componentInstance, 'handleFocus');
238+
fixture.detectChanges();
239+
240+
const tabLabels = fixture.debugElement.queryAll(By.css('.mat-tab-label'));
241+
242+
expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(0);
243+
244+
tabLabels[1].nativeElement.click();
245+
fixture.detectChanges();
246+
247+
expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(1);
248+
expect(fixture.componentInstance.handleFocus)
249+
.toHaveBeenCalledWith(jasmine.objectContaining({index: 1}));
250+
});
251+
252+
it('should emit focusChange on arrow key navigation', () => {
253+
spyOn(fixture.componentInstance, 'handleFocus');
254+
fixture.detectChanges();
255+
256+
const tabLabels = fixture.debugElement.queryAll(By.css('.mat-tab-label'));
257+
const tabLabelContainer = fixture.debugElement
258+
.query(By.css('.mat-tab-label-container')).nativeElement as HTMLElement;
259+
260+
expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(0);
261+
262+
// In order to verify that the `focusChange` event also fires with the correct
263+
// index, we focus the second tab before testing the keyboard navigation.
264+
tabLabels[1].nativeElement.click();
265+
fixture.detectChanges();
266+
267+
expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(1);
268+
269+
dispatchKeyboardEvent(tabLabelContainer, 'keydown', LEFT_ARROW);
270+
271+
expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(2);
272+
expect(fixture.componentInstance.handleFocus)
273+
.toHaveBeenCalledWith(jasmine.objectContaining({index: 0}));
274+
});
275+
235276
});
236277

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

src/lib/tabs/tab-header.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import {
2828
ViewEncapsulation,
2929
} from '@angular/core';
3030
import {CanDisableRipple, mixinDisableRipple} from '@angular/material/core';
31-
import {merge, of as observableOf, Subscription} from 'rxjs';
31+
import {merge, of as observableOf, Subject} from 'rxjs';
32+
import {takeUntil} from 'rxjs/operators';
3233
import {MatInkBar} from './ink-bar';
3334
import {MatTabLabelWrapper} from './tab-label-wrapper';
3435
import {FocusKeyManager} from '@angular/cdk/a11y';
@@ -87,8 +88,8 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
8788
/** Whether the header should scroll to the selected index after the view has been checked. */
8889
private _selectedIndexChanged = false;
8990

90-
/** Combines listeners that will re-align the ink bar whenever they're invoked. */
91-
private _realignInkBar = Subscription.EMPTY;
91+
/** Emits when the component is destroyed. */
92+
private readonly _destroyed = new Subject<void>();
9293

9394
/** Whether the controls for pagination should be displayed */
9495
_showPaginationControls = false;
@@ -201,20 +202,31 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
201202
.withHorizontalOrientation(this._getLayoutDirection())
202203
.withWrap();
203204

204-
this._keyManager.updateActiveItemIndex(0);
205+
this._keyManager.updateActiveItem(0);
205206

206207
// Defer the first call in order to allow for slower browsers to lay out the elements.
207208
// This helps in cases where the user lands directly on a page with paginated tabs.
208209
typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(realign) : realign();
209210

210-
this._realignInkBar = merge(dirChange, resize).subscribe(() => {
211+
// On dir change or window resize, realign the ink bar and update the orientation of
212+
// the key manager if the direction has changed.
213+
merge(dirChange, resize).pipe(takeUntil(this._destroyed)).subscribe(() => {
211214
realign();
212215
this._keyManager.withHorizontalOrientation(this._getLayoutDirection());
213216
});
217+
218+
// If there is a change in the focus key manager we need to emit the `indexFocused`
219+
// event in order to provide a public event that notifies about focus changes. Also we realign
220+
// the tabs container by scrolling the new focused tab into the visible section.
221+
this._keyManager.change.pipe(takeUntil(this._destroyed)).subscribe(newFocusIndex => {
222+
this.indexFocused.emit(newFocusIndex);
223+
this._setTabFocus(newFocusIndex);
224+
});
214225
}
215226

216227
ngOnDestroy() {
217-
this._realignInkBar.unsubscribe();
228+
this._destroyed.next();
229+
this._destroyed.complete();
218230
}
219231

220232
/**
@@ -242,11 +254,11 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
242254

243255
/** When the focus index is set, we must manually send focus to the correct label */
244256
set focusIndex(value: number) {
245-
if (!this._isValidIndex(value) || this.focusIndex == value || !this._keyManager) { return; }
257+
if (!this._isValidIndex(value) || this.focusIndex === value || !this._keyManager) {
258+
return;
259+
}
246260

247261
this._keyManager.setActiveItem(value);
248-
this.indexFocused.emit(value);
249-
this._setTabFocus(value);
250262
}
251263

252264
/**

0 commit comments

Comments
 (0)