Skip to content

Commit 07a82ed

Browse files
crisbetojelbourn
authored andcommitted
fix(chips): unable to tab out of chip list (#4605)
* Fixes not being able to escape focus from an `md-chip-list` backwards via shift+tab on all browsers. * Fixes not being able to tab out of an `md-chip-list` at all on Firefox. Fixes #4593.
1 parent 45e0879 commit 07a82ed

File tree

2 files changed

+42
-15
lines changed

2 files changed

+42
-15
lines changed

src/lib/chips/chip-list.spec.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
22
import {Component, DebugElement, QueryList} from '@angular/core';
33
import {By} from '@angular/platform-browser';
44
import {MdChip, MdChipList, MdChipsModule} from './index';
55
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
66
import {FakeEvent} from '../core/a11y/list-key-manager.spec';
7-
import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
7+
import {SPACE, LEFT_ARROW, RIGHT_ARROW, TAB} from '../core/keyboard/keycodes';
8+
import {createKeyboardEvent} from '../core/testing/event-objects';
9+
810

911
class FakeKeyboardEvent extends FakeEvent {
1012
constructor(keyCode: number, protected target: HTMLElement) {
@@ -26,9 +28,7 @@ describe('MdChipList', () => {
2628
beforeEach(async(() => {
2729
TestBed.configureTestingModule({
2830
imports: [MdChipsModule],
29-
declarations: [
30-
StaticChipList
31-
]
31+
declarations: [StaticChipList]
3232
});
3333

3434
TestBed.compileComponents();
@@ -189,6 +189,17 @@ describe('MdChipList', () => {
189189
expect(testComponent.chipDeselect).toHaveBeenCalledTimes(1);
190190
expect(testComponent.chipDeselect).toHaveBeenCalledWith(0);
191191
});
192+
193+
it('allow focus to escape when tabbing away', fakeAsync(() => {
194+
chipListInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB));
195+
196+
expect(chipListInstance._tabIndex)
197+
.toBe(-1, 'Expected tabIndex to be set to -1 temporarily.');
198+
199+
tick();
200+
201+
expect(chipListInstance._tabIndex).toBe(0, 'Expected tabIndex to be reset back to 0');
202+
}));
192203
});
193204

194205
describe('when selectable is false', () => {

src/lib/chips/chip-list.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import {
33
ChangeDetectionStrategy,
44
Component,
55
ContentChildren,
6-
ElementRef,
76
Input,
87
QueryList,
9-
ViewEncapsulation
8+
ViewEncapsulation,
9+
OnDestroy,
1010
} from '@angular/core';
1111

1212
import {MdChip} from './chip';
1313
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
1414
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
15-
import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
15+
import {SPACE, LEFT_ARROW, RIGHT_ARROW, TAB} from '../core/keyboard/keycodes';
16+
import {Subscription} from 'rxjs/Subscription';
1617

1718
/**
1819
* A material design chips component (named ChipList for it's similarity to the List component).
@@ -30,7 +31,7 @@ import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
3031
template: `<div class="mat-chip-list-wrapper"><ng-content></ng-content></div>`,
3132
host: {
3233
// Properties
33-
'tabindex': '0',
34+
'[attr.tabindex]': '_tabIndex',
3435
'role': 'listbox',
3536
'[class.mat-chip-list]': 'true',
3637

@@ -45,11 +46,14 @@ import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
4546
encapsulation: ViewEncapsulation.None,
4647
changeDetection: ChangeDetectionStrategy.OnPush
4748
})
48-
export class MdChipList implements AfterContentInit {
49+
export class MdChipList implements AfterContentInit, OnDestroy {
4950

5051
/** Track which chips we're listening to for focus/destruction. */
5152
private _subscribed: WeakMap<MdChip, boolean> = new WeakMap();
5253

54+
/** Subscription to tabbing out from the chip list. */
55+
private _tabOutSubscription: Subscription;
56+
5357
/** Whether or not the chip is selectable. */
5458
protected _selectable: boolean = true;
5559

@@ -59,11 +63,19 @@ export class MdChipList implements AfterContentInit {
5963
/** The chip components contained within this chip list. */
6064
chips: QueryList<MdChip>;
6165

62-
constructor(private _elementRef: ElementRef) { }
66+
/** Tab index for the chip list. */
67+
_tabIndex = 0;
6368

6469
ngAfterContentInit(): void {
6570
this._keyManager = new FocusKeyManager(this.chips).withWrap();
6671

72+
// Prevents the chip list from capturing focus and redirecting
73+
// it back to the first chip when the user tabs out.
74+
this._tabOutSubscription = this._keyManager.tabOut.subscribe(() => {
75+
this._tabIndex = -1;
76+
setTimeout(() => this._tabIndex = 0);
77+
});
78+
6779
// Go ahead and subscribe all of the initial chips
6880
this._subscribeChips(this.chips);
6981

@@ -73,14 +85,18 @@ export class MdChipList implements AfterContentInit {
7385
});
7486
}
7587

88+
ngOnDestroy(): void {
89+
if (this._tabOutSubscription) {
90+
this._tabOutSubscription.unsubscribe();
91+
}
92+
}
93+
7694
/**
7795
* Whether or not this chip is selectable. When a chip is not selectable,
7896
* it's selected state is always ignored.
7997
*/
80-
@Input() get selectable(): boolean {
81-
return this._selectable;
82-
}
83-
98+
@Input()
99+
get selectable(): boolean { return this._selectable; }
84100
set selectable(value: boolean) {
85101
this._selectable = coerceBooleanProperty(value);
86102
}

0 commit comments

Comments
 (0)