Skip to content

Commit

Permalink
fix(chips): unable to tab out of chip list (#4605)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
crisbeto authored and jelbourn committed May 19, 2017
1 parent 45e0879 commit 07a82ed
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 15 deletions.
21 changes: 16 additions & 5 deletions src/lib/chips/chip-list.spec.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -26,9 +28,7 @@ describe('MdChipList', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdChipsModule],
declarations: [
StaticChipList
]
declarations: [StaticChipList]
});

TestBed.compileComponents();
Expand Down Expand Up @@ -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', () => {
Expand Down
36 changes: 26 additions & 10 deletions src/lib/chips/chip-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -30,7 +31,7 @@ import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
template: `<div class="mat-chip-list-wrapper"><ng-content></ng-content></div>`,
host: {
// Properties
'tabindex': '0',
'[attr.tabindex]': '_tabIndex',
'role': 'listbox',
'[class.mat-chip-list]': 'true',

Expand All @@ -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<MdChip, boolean> = new WeakMap();

/** Subscription to tabbing out from the chip list. */
private _tabOutSubscription: Subscription;

/** Whether or not the chip is selectable. */
protected _selectable: boolean = true;

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

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);

Expand All @@ -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);
}
Expand Down

0 comments on commit 07a82ed

Please sign in to comment.