From b216a53a8159fe62f7e76820cfd67d5f1ec4449c Mon Sep 17 00:00:00 2001 From: crisbeto Date: Tue, 16 May 2017 22:10:56 +0200 Subject: [PATCH] fix(chips): unable to tab out of chip list * 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. --- src/lib/chips/chip-list.spec.ts | 21 ++++++++++++++----- src/lib/chips/chip-list.ts | 36 ++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 0d2407c495ba..e4ed12a247cd 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -1,10 +1,12 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; import {Component, DebugElement, QueryList} from '@angular/core'; import {By} from '@angular/platform-browser'; import {MdChip, MdChipList, MdChipsModule} from './index'; import {FocusKeyManager} from '../core/a11y/focus-key-manager'; import {FakeEvent} from '../core/a11y/list-key-manager.spec'; -import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes'; +import {SPACE, LEFT_ARROW, RIGHT_ARROW, TAB} from '../core/keyboard/keycodes'; +import {createKeyboardEvent} from '../core/testing/event-objects'; + class FakeKeyboardEvent extends FakeEvent { constructor(keyCode: number, protected target: HTMLElement) { @@ -26,9 +28,7 @@ describe('MdChipList', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MdChipsModule], - declarations: [ - StaticChipList - ] + declarations: [StaticChipList] }); TestBed.compileComponents(); @@ -189,6 +189,17 @@ describe('MdChipList', () => { expect(testComponent.chipDeselect).toHaveBeenCalledTimes(1); expect(testComponent.chipDeselect).toHaveBeenCalledWith(0); }); + + it('allow focus to escape when tabbing away', fakeAsync(() => { + chipListInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB)); + + expect(chipListInstance._tabIndex) + .toBe(-1, 'Expected tabIndex to be set to -1 temporarily.'); + + tick(); + + expect(chipListInstance._tabIndex).toBe(0, 'Expected tabIndex to be reset back to 0'); + })); }); describe('when selectable is false', () => { diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index 7c4a396cf84c..276258f699e2 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -3,16 +3,17 @@ import { ChangeDetectionStrategy, Component, ContentChildren, - ElementRef, Input, QueryList, - ViewEncapsulation + ViewEncapsulation, + OnDestroy, } from '@angular/core'; import {MdChip} from './chip'; import {FocusKeyManager} from '../core/a11y/focus-key-manager'; import {coerceBooleanProperty} from '../core/coercion/boolean-property'; -import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes'; +import {SPACE, LEFT_ARROW, RIGHT_ARROW, TAB} from '../core/keyboard/keycodes'; +import {Subscription} from 'rxjs/Subscription'; /** * 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'; template: `
`, host: { // Properties - 'tabindex': '0', + '[attr.tabindex]': '_tabIndex', 'role': 'listbox', '[class.mat-chip-list]': 'true', @@ -45,11 +46,14 @@ import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes'; encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) -export class MdChipList implements AfterContentInit { +export class MdChipList implements AfterContentInit, OnDestroy { /** Track which chips we're listening to for focus/destruction. */ private _subscribed: WeakMap = new WeakMap(); + /** Subscription to tabbing out from the chip list. */ + private _tabOutSubscription: Subscription; + /** Whether or not the chip is selectable. */ protected _selectable: boolean = true; @@ -59,11 +63,19 @@ export class MdChipList implements AfterContentInit { /** The chip components contained within this chip list. */ chips: QueryList; - constructor(private _elementRef: ElementRef) { } + /** Tab index for the chip list. */ + _tabIndex = 0; ngAfterContentInit(): void { this._keyManager = new FocusKeyManager(this.chips).withWrap(); + // Prevents the chip list from capturing focus and redirecting + // it back to the first chip when the user tabs out. + this._tabOutSubscription = this._keyManager.tabOut.subscribe(() => { + this._tabIndex = -1; + setTimeout(() => this._tabIndex = 0); + }); + // Go ahead and subscribe all of the initial chips this._subscribeChips(this.chips); @@ -73,14 +85,18 @@ export class MdChipList implements AfterContentInit { }); } + ngOnDestroy(): void { + if (this._tabOutSubscription) { + this._tabOutSubscription.unsubscribe(); + } + } + /** * Whether or not this chip is selectable. When a chip is not selectable, * it's selected state is always ignored. */ - @Input() get selectable(): boolean { - return this._selectable; - } - + @Input() + get selectable(): boolean { return this._selectable; } set selectable(value: boolean) { this._selectable = coerceBooleanProperty(value); }