From c8199efcdbb8246b352bc13ae9bac6dd08c7ba05 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Wed, 11 Jan 2017 17:58:50 -0800 Subject: [PATCH] feat(list-key-manager): active descendant support --- src/lib/chips/chip-list.spec.ts | 20 +- src/lib/chips/chip-list.ts | 22 +- src/lib/chips/chip.ts | 2 +- .../core/a11y/activedescendant-key-manager.ts | 35 ++ src/lib/core/a11y/focus-key-manager.ts | 29 + src/lib/core/a11y/list-key-manager.spec.ts | 593 +++++++++++------- src/lib/core/a11y/list-key-manager.ts | 134 ++-- src/lib/menu/menu-directive.ts | 8 +- src/lib/menu/menu-item.ts | 2 +- src/lib/select/select.spec.ts | 4 +- src/lib/select/select.ts | 10 +- 11 files changed, 532 insertions(+), 327 deletions(-) create mode 100644 src/lib/core/a11y/activedescendant-key-manager.ts create mode 100644 src/lib/core/a11y/focus-key-manager.ts diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 927fa1ab19e2..b683fa1c0e8b 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -2,7 +2,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Component, DebugElement, QueryList} from '@angular/core'; import {By} from '@angular/platform-browser'; import {MdChip, MdChipList, MdChipsModule} from './index'; -import {ListKeyManager} from '../core/a11y/list-key-manager'; +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'; @@ -21,7 +21,7 @@ describe('MdChipList', () => { let chipListInstance: MdChipList; let testComponent: StaticChipList; let chips: QueryList; - let manager: ListKeyManager; + let manager: FocusKeyManager; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -60,7 +60,7 @@ describe('MdChipList', () => { chipListInstance.focus(); fixture.detectChanges(); - expect(manager.focusedItemIndex).toBe(0); + expect(manager.activeItemIndex).toBe(0); }); it('watches for chip focus', () => { @@ -71,7 +71,7 @@ describe('MdChipList', () => { lastItem.focus(); fixture.detectChanges(); - expect(manager.focusedItemIndex).toBe(lastIndex); + expect(manager.activeItemIndex).toBe(lastIndex); }); describe('on chip destroy', () => { @@ -87,7 +87,7 @@ describe('MdChipList', () => { fixture.detectChanges(); // It focuses the 4th item (now at index 2) - expect(manager.focusedItemIndex).toEqual(2); + expect(manager.activeItemIndex).toEqual(2); }); it('focuses the previous item', () => { @@ -103,7 +103,7 @@ describe('MdChipList', () => { fixture.detectChanges(); // It focuses the next-to-last item - expect(manager.focusedItemIndex).toEqual(lastIndex - 1); + expect(manager.activeItemIndex).toEqual(lastIndex - 1); }); }); }); @@ -124,14 +124,14 @@ describe('MdChipList', () => { // Focus the last item in the array lastItem.focus(); - expect(manager.focusedItemIndex).toEqual(lastIndex); + expect(manager.activeItemIndex).toEqual(lastIndex); // Press the LEFT arrow chipListInstance._keydown(LEFT_EVENT); fixture.detectChanges(); // It focuses the next-to-last item - expect(manager.focusedItemIndex).toEqual(lastIndex - 1); + expect(manager.activeItemIndex).toEqual(lastIndex - 1); }); it('right arrow focuses next item', () => { @@ -144,14 +144,14 @@ describe('MdChipList', () => { // Focus the last item in the array firstItem.focus(); - expect(manager.focusedItemIndex).toEqual(0); + expect(manager.activeItemIndex).toEqual(0); // Press the RIGHT arrow chipListInstance._keydown(RIGHT_EVENT); fixture.detectChanges(); // It focuses the next-to-last item - expect(manager.focusedItemIndex).toEqual(1); + expect(manager.activeItemIndex).toEqual(1); }); describe('when selectable is true', () => { diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index 0d3ff0b91261..e0f4ebc5ed78 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -12,7 +12,7 @@ import { } from '@angular/core'; import {MdChip} from './chip'; -import {ListKeyManager} from '../core/a11y/list-key-manager'; +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'; @@ -55,8 +55,8 @@ export class MdChipList implements AfterContentInit { /** Whether or not the chip is selectable. */ protected _selectable: boolean = true; - /** The ListKeyManager which handles focus. */ - _keyManager: ListKeyManager; + /** The FocusKeyManager which handles focus. */ + _keyManager: FocusKeyManager; /** The chip components contained within this chip list. */ chips: QueryList; @@ -64,7 +64,7 @@ export class MdChipList implements AfterContentInit { constructor(private _elementRef: ElementRef) { } ngAfterContentInit(): void { - this._keyManager = new ListKeyManager(this.chips).withFocusWrap(); + this._keyManager = new FocusKeyManager(this.chips).withWrap(); // Go ahead and subscribe all of the initial chips this._subscribeChips(this.chips); @@ -93,7 +93,7 @@ export class MdChipList implements AfterContentInit { */ focus() { // TODO: ARIA says this should focus the first `selected` chip. - this._keyManager.focusFirstItem(); + this._keyManager.setFirstItemActive(); } /** Passes relevant key presses to our key manager. */ @@ -113,11 +113,11 @@ export class MdChipList implements AfterContentInit { event.preventDefault(); break; case LEFT_ARROW: - this._keyManager.focusPreviousItem(); + this._keyManager.setPreviousItemActive(); event.preventDefault(); break; case RIGHT_ARROW: - this._keyManager.focusNextItem(); + this._keyManager.setNextItemActive(); event.preventDefault(); break; default: @@ -133,7 +133,7 @@ export class MdChipList implements AfterContentInit { return; } - let focusedIndex = this._keyManager.focusedItemIndex; + let focusedIndex = this._keyManager.activeItemIndex; if (this._isValidIndex(focusedIndex)) { let focusedChip: MdChip = this.chips.toArray()[focusedIndex]; @@ -173,7 +173,7 @@ export class MdChipList implements AfterContentInit { let chipIndex: number = this.chips.toArray().indexOf(chip); if (this._isValidIndex(chipIndex)) { - this._keyManager.updateFocusedItemIndex(chipIndex); + this._keyManager.updateActiveItemIndex(chipIndex); } }); @@ -184,9 +184,9 @@ export class MdChipList implements AfterContentInit { if (this._isValidIndex(chipIndex)) { // Check whether the chip is the last item if (chipIndex < this.chips.length - 1) { - this._keyManager.setFocus(chipIndex); + this._keyManager.setActiveItem(chipIndex); } else if (chipIndex - 1 >= 0) { - this._keyManager.setFocus(chipIndex - 1); + this._keyManager.setActiveItem(chipIndex - 1); } } diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts index 2c9fdae96fbc..905a90079329 100644 --- a/src/lib/chips/chip.ts +++ b/src/lib/chips/chip.ts @@ -9,7 +9,7 @@ import { Renderer } from '@angular/core'; -import {Focusable} from '../core/a11y/list-key-manager'; +import {Focusable} from '../core/a11y/focus-key-manager'; import {coerceBooleanProperty} from '../core/coercion/boolean-property'; export interface MdChipEvent { diff --git a/src/lib/core/a11y/activedescendant-key-manager.ts b/src/lib/core/a11y/activedescendant-key-manager.ts new file mode 100644 index 000000000000..0c9a49bec242 --- /dev/null +++ b/src/lib/core/a11y/activedescendant-key-manager.ts @@ -0,0 +1,35 @@ +import {QueryList} from '@angular/core'; +import {ListKeyManager, CanDisable} from './list-key-manager'; + +/** + * This is the interface for highlightable items (used by the ActiveDescendantKeyManager). + * Each item must know how to style itself as active or inactive and whether or not it is + * currently disabled. + */ +export interface Highlightable extends CanDisable { + setActiveStyles(): void; + setInactiveStyles(): void; +} + +export class ActiveDescendantKeyManager extends ListKeyManager { + + constructor(items: QueryList) { + super(items); + } + + /** + * This method sets the active item to the item at the specified index. + * It also adds active styles to the newly active item and removes active + * styles from the previously active item. + */ + setActiveItem(index: number): void { + if (this.activeItem) { + this.activeItem.setInactiveStyles(); + } + super.setActiveItem(index); + if (this.activeItem) { + this.activeItem.setActiveStyles(); + } + } + +} diff --git a/src/lib/core/a11y/focus-key-manager.ts b/src/lib/core/a11y/focus-key-manager.ts new file mode 100644 index 000000000000..d92160b07c15 --- /dev/null +++ b/src/lib/core/a11y/focus-key-manager.ts @@ -0,0 +1,29 @@ + +import {QueryList} from '@angular/core'; +import {ListKeyManager, CanDisable} from './list-key-manager'; + +/** + * This is the interface for focusable items (used by the FocusKeyManager). + * Each item must know how to focus itself and whether or not it is currently disabled. + */ +export interface Focusable extends CanDisable { + focus(): void; +} + + +export class FocusKeyManager extends ListKeyManager { + + constructor(items: QueryList) { + super(items); + } + + /** + * This method sets the active item to the item at the specified index. + * It also adds focuses the newly active item. + */ + setActiveItem(index: number): void { + super.setActiveItem(index); + this.activeItem.focus(); + } + +} diff --git a/src/lib/core/a11y/list-key-manager.spec.ts b/src/lib/core/a11y/list-key-manager.spec.ts index 6a75c1be87ce..da36d2e8b99f 100644 --- a/src/lib/core/a11y/list-key-manager.spec.ts +++ b/src/lib/core/a11y/list-key-manager.spec.ts @@ -1,12 +1,20 @@ import {QueryList} from '@angular/core'; -import {ListKeyManager} from './list-key-manager'; +import {FocusKeyManager} from './focus-key-manager'; import {DOWN_ARROW, UP_ARROW, TAB, HOME, END} from '../keyboard/keycodes'; +import {ListKeyManager} from './list-key-manager'; +import {ActiveDescendantKeyManager} from './activedescendant-key-manager'; class FakeFocusable { disabled = false; focus() {} } +class FakeHighlightable { + disabled = false; + setActiveStyles() {} + setInactiveStyles() {} +} + class FakeQueryList extends QueryList { get length() { return this.items.length; } items: T[]; @@ -23,9 +31,8 @@ export class FakeEvent { } } -describe('ListKeyManager', () => { - let keyManager: ListKeyManager; - let itemList: FakeQueryList; +describe('Key managers', () => { + let itemList: FakeQueryList; let DOWN_ARROW_EVENT: KeyboardEvent; let UP_ARROW_EVENT: KeyboardEvent; let TAB_EVENT: KeyboardEvent; @@ -33,14 +40,7 @@ describe('ListKeyManager', () => { let END_EVENT: KeyboardEvent; beforeEach(() => { - itemList = new FakeQueryList(); - itemList.items = [ - new FakeFocusable(), - new FakeFocusable(), - new FakeFocusable() - ]; - - keyManager = new ListKeyManager(itemList); + itemList = new FakeQueryList(); DOWN_ARROW_EVENT = new FakeEvent(DOWN_ARROW) as KeyboardEvent; UP_ARROW_EVENT = new FakeEvent(UP_ARROW) as KeyboardEvent; @@ -48,285 +48,420 @@ describe('ListKeyManager', () => { HOME_EVENT = new FakeEvent(HOME) as KeyboardEvent; END_EVENT = new FakeEvent(END) as KeyboardEvent; - // first item is already focused - keyManager.focusFirstItem(); - - spyOn(itemList.items[0], 'focus'); - spyOn(itemList.items[1], 'focus'); - spyOn(itemList.items[2], 'focus'); }); - describe('key events', () => { - it('should focus subsequent items when down arrow is pressed', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(itemList.items[0].focus).not.toHaveBeenCalled(); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[2].focus).not.toHaveBeenCalled(); + describe('ListKeyManager', () => { + let keyManager: ListKeyManager; - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(itemList.items[0].focus).not.toHaveBeenCalled(); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); - }); - - it('should focus previous items when up arrow is pressed', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + beforeEach(() => { + itemList.items = [ + new FakeFocusable(), + new FakeFocusable(), + new FakeFocusable() + ]; - expect(itemList.items[0].focus).not.toHaveBeenCalled(); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); + keyManager = new ListKeyManager(itemList); - keyManager.onKeydown(UP_ARROW_EVENT); + // first item is already focused + keyManager.setFirstItemActive(); - expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); + spyOn(keyManager, 'setActiveItem').and.callThrough(); }); - it('should skip disabled items using arrow keys', () => { - itemList.items[1].disabled = true; + describe('Key events', () => { + + it('should set subsequent items as active when down arrow is pressed', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); + + expect(keyManager.activeItemIndex) + .toBe(1, 'Expected active item to be 1 after 1 down arrow event.'); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(1); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2); + + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, 'Expected active item to be 2 after 2 down arrow events.'); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(2); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); + }); + + it('should set previous items as active when up arrow is pressed', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); + + expect(keyManager.activeItemIndex) + .toBe(1, 'Expected active item to be 1 after 1 down arrow event.'); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(1); + + keyManager.onKeydown(UP_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(0, 'Expected active item to be 0 after 1 down and 1 up arrow event.'); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(0); + }); + + it('should skip disabled items using arrow keys', () => { + itemList.items[1].disabled = true; + + // down arrow should skip past disabled item from 0 to 2 + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, 'Expected active item to skip past disabled item on down arrow.'); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(1); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(2); + + // up arrow should skip past disabled item from 2 to 0 + keyManager.onKeydown(UP_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(0, 'Expected active item to skip past disabled item on up arrow.'); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(0); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(1); + }); + + it('should work normally when disabled property does not exist', () => { + itemList.items[0].disabled = undefined; + itemList.items[1].disabled = undefined; + itemList.items[2].disabled = undefined; + + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(1, 'Expected active item to be 1 after 1 down arrow when disabled not set.'); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(1); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2); + + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, 'Expected active item to be 2 after 2 down arrows when disabled not set.'); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); + expect(keyManager.setActiveItem).toHaveBeenCalledWith(2); + }); + + it('should not move active item past either end of the list', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected last item of the list to be active.`); + + // this down arrow would move active item past the end of the list + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected active item to remain at the end of the list.`); + + keyManager.onKeydown(UP_ARROW_EVENT); + keyManager.onKeydown(UP_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(0, `Expected first item of the list to be active.`); + + // this up arrow would move active item past the beginning of the list + keyManager.onKeydown(UP_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(0, `Expected active item to remain at the beginning of the list.`); + }); + + it('should not move active item to end when the last item is disabled', () => { + itemList.items[2].disabled = true; + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected second item of the list to be active.`); + + // this down arrow would set active item to the last item, which is disabled + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected the second item to remain active.`); + expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2); + }); + + it('should set the active item to the first item when HOME is pressed', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected last item of the list to be active.`); + + keyManager.onKeydown(HOME_EVENT); + expect(keyManager.activeItemIndex) + .toBe(0, `Expected the HOME key to set the active item to the first item.`); + }); + + it('should set the active item to the last item when END is pressed', () => { + expect(keyManager.activeItemIndex) + .toBe(0, `Expected first item of the list to be active.`); + + keyManager.onKeydown(END_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected the END key to set the active item to the last item.`); + }); + + it('should emit tabOut when the tab key is pressed', () => { + let tabOutEmitted = false; + keyManager.tabOut.first().subscribe(() => tabOutEmitted = true); + keyManager.onKeydown(TAB_EVENT); + + expect(tabOutEmitted).toBe(true); + }); + + it('should prevent the default keyboard action', () => { + expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(false); + + keyManager.onKeydown(DOWN_ARROW_EVENT); + + expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(true); + }); + + it('should not prevent the default keyboard action when pressing tab', () => { + expect(TAB_EVENT.defaultPrevented).toBe(false); + + keyManager.onKeydown(TAB_EVENT); + + expect(TAB_EVENT.defaultPrevented).toBe(false); + }); - // down arrow should skip past disabled item from 0 to 2 - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(itemList.items[0].focus).not.toHaveBeenCalled(); - expect(itemList.items[1].focus).not.toHaveBeenCalled(); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); - - // up arrow should skip past disabled item from 2 to 0 - keyManager.onKeydown(UP_ARROW_EVENT); - expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[1].focus).not.toHaveBeenCalled(); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); }); - it('should work normally when disabled property does not exist', () => { - itemList.items[0].disabled = undefined; - itemList.items[1].disabled = undefined; - itemList.items[2].disabled = undefined; + describe('programmatic focus', () => { + + it('should setActiveItem()', () => { + expect(keyManager.activeItemIndex) + .toBe(0, `Expected first item of the list to be active.`); + + keyManager.setActiveItem(1); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected activeItemIndex to be updated when setActiveItem() was called.`); + }); + + it('should expose the active item correctly', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); + + expect(keyManager.activeItemIndex).toBe(1, 'Expected active item to be the second option.'); + expect(keyManager.activeItem) + .toBe(itemList.items[1], 'Expected the active item to match the second option.'); + + + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex).toBe(2, 'Expected active item to be the third option.'); + expect(keyManager.activeItem) + .toBe(itemList.items[2], 'Expected the active item ID to match the third option.'); + }); + + it('should setFirstItemActive()', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected last item of the list to be active.`); + + keyManager.setFirstItemActive(); + expect(keyManager.activeItemIndex) + .toBe(0, `Expected setFirstItemActive() to set the active item to the first item.`); + }); + + it('should set the active item to the second item if the first one is disabled', () => { + itemList.items[0].disabled = true; + + keyManager.setFirstItemActive(); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected the second item to be active if the first was disabled.`); + }); + + it('should setLastItemActive()', () => { + expect(keyManager.activeItemIndex) + .toBe(0, `Expected first item of the list to be active.`); + + keyManager.setLastItemActive(); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected setLastItemActive() to set the active item to the last item.`); + }); + + it('should set the active item to the second to last item if the last is disabled', () => { + itemList.items[2].disabled = true; + + keyManager.setLastItemActive(); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected the second to last item to be active if the last was disabled.`); + }); + + it('should setNextItemActive()', () => { + expect(keyManager.activeItemIndex) + .toBe(0, `Expected first item of the list to be active.`); + + keyManager.setNextItemActive(); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected setNextItemActive() to set the active item to the next item.`); + }); + + it('should set the active item to the next enabled item if next is disabled', () => { + itemList.items[1].disabled = true; + expect(keyManager.activeItemIndex) + .toBe(0, `Expected first item of the list to be active.`); + + keyManager.setNextItemActive(); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected setNextItemActive() to only set enabled items as active.`); + }); + + it('should setPreviousItemActive()', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected second item of the list to be active.`); + + keyManager.setPreviousItemActive(); + expect(keyManager.activeItemIndex) + .toBe(0, `Expected setPreviousItemActive() to set the active item to the previous.`); + }); + + it('should skip disabled items when setPreviousItemActive() is called', () => { + itemList.items[1].disabled = true; + keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex) + .toBe(2, `Expected third item of the list to be active.`); + + keyManager.setPreviousItemActive(); + expect(keyManager.activeItemIndex) + .toBe(0, `Expected setPreviousItemActive() to skip the disabled item.`); + }); - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(itemList.items[0].focus).not.toHaveBeenCalled(); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[2].focus).not.toHaveBeenCalled(); - - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(itemList.items[0].focus).not.toHaveBeenCalled(); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); }); - it('should not move focus past either end of the list', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected focus to be on the last item of the list.`); - - // this down arrow would move focus past the end of the list - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected focus to remain at the end of the list.`); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); + describe('wrap mode', () => { - keyManager.onKeydown(UP_ARROW_EVENT); - keyManager.onKeydown(UP_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to be on the first item of the list.`); + it('should return itself to allow chaining', () => { + expect(keyManager.withWrap()) + .toEqual(keyManager, `Expected withWrap() to return an instance of ListKeyManager.`); + }); - // this up arrow would move focus past the beginning of the list - keyManager.onKeydown(UP_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to remain at the beginning of the list.`); - expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); - }); + it('should wrap focus when arrow keying past items while in wrap mode', () => { + keyManager.withWrap(); + keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(DOWN_ARROW_EVENT); - it('should not move focus when the last item is disabled', () => { - itemList.items[2].disabled = true; - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected focus to be on the second item of the list.`); + expect(keyManager.activeItemIndex).toBe(2, 'Expected last item to be active.'); - // this down arrow would move focus the last item, which is disabled - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected focus to remain on the second item.`); - expect(itemList.items[2].focus).not.toHaveBeenCalled(); - }); + // this down arrow moves down past the end of the list + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(keyManager.activeItemIndex).toBe(0, 'Expected active item to wrap to beginning.'); - it('should focus the first item when HOME is pressed', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected focus to be on the last item of the list.`); + // this up arrow moves up past the beginning of the list + keyManager.onKeydown(UP_ARROW_EVENT); + expect(keyManager.activeItemIndex).toBe(2, 'Expected active item to wrap to end.'); + }); - keyManager.onKeydown(HOME_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected the HOME key to move the focus back to the first item.`); }); - it('should focus the last item when END is pressed', () => { - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to be on the first item of the list.`); - - keyManager.onKeydown(END_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected the END key to move the focus to the last item in the list.`); - }); - - it('should emit tabOut when the tab key is pressed', () => { - let tabOutEmitted = false; - keyManager.tabOut.first().subscribe(() => tabOutEmitted = true); - keyManager.onKeydown(TAB_EVENT); - - expect(tabOutEmitted).toBe(true); - }); - - it('should prevent the default keyboard action', () => { - expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(false); + }); - keyManager.onKeydown(DOWN_ARROW_EVENT); + describe('FocusKeyManager', () => { + let keyManager: FocusKeyManager; - expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(true); - }); + beforeEach(() => { + itemList.items = [ + new FakeFocusable(), + new FakeFocusable(), + new FakeFocusable() + ]; - it('should not prevent the default keyboard action when pressing tab', () => { - expect(TAB_EVENT.defaultPrevented).toBe(false); + keyManager = new FocusKeyManager(itemList); - keyManager.onKeydown(TAB_EVENT); + // first item is already focused + keyManager.setFirstItemActive(); - expect(TAB_EVENT.defaultPrevented).toBe(false); + spyOn(itemList.items[0], 'focus'); + spyOn(itemList.items[1], 'focus'); + spyOn(itemList.items[2], 'focus'); }); - }); - - describe('programmatic focus', () => { + it('should focus subsequent items when down arrow is pressed', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); - it('should setFocus()', () => { - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to be on the first item of the list.`); - - keyManager.setFocus(1); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected focusedItemIndex to be updated when setFocus() was called.`); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - }); + expect(itemList.items[0].focus).not.toHaveBeenCalled(); + expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); + expect(itemList.items[2].focus).not.toHaveBeenCalled(); - it('should allow setting the focused item without calling focus', () => { - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to be on the first item of the list.`); + keyManager.onKeydown(DOWN_ARROW_EVENT); + expect(itemList.items[0].focus).not.toHaveBeenCalled(); + expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); + expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); + }); - keyManager.updateFocusedItemIndex(1); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected focusedItemIndex to be updated after calling updateFocusedItemIndex().`); - expect(itemList.items[1].focus).not.toHaveBeenCalledTimes(1); - }); + it('should focus previous items when up arrow is pressed', () => { + keyManager.onKeydown(DOWN_ARROW_EVENT); - it('should focus the first item when focusFirstItem() is called', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected focus to be on the last item of the list.`); + expect(itemList.items[0].focus).not.toHaveBeenCalled(); + expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - keyManager.focusFirstItem(); - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focusFirstItem() to move the focus back to the first item.`); - }); + keyManager.onKeydown(UP_ARROW_EVENT); - it('should focus the second item if the first one is disabled', () => { - itemList.items[0].disabled = true; + expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); + expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); + }); - keyManager.focusFirstItem(); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected the second item to be focused if the first was disabled.`); - }); + it('should allow setting the focused item without calling focus', () => { + expect(keyManager.activeItemIndex) + .toBe(0, `Expected first item of the list to be active.`); - it('should focus the last item when focusLastItem() is called', () => { - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to be on the first item of the list.`); + keyManager.updateActiveItemIndex(1); + expect(keyManager.activeItemIndex) + .toBe(1, `Expected activeItemIndex to update after calling updateActiveItemIndex().`); + expect(itemList.items[1].focus).not.toHaveBeenCalledTimes(1); + }); - keyManager.focusLastItem(); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected focusLastItem() to move the focus to the last item in the list.`); - }); + }); - it('should focus the second to last item if the last one is disabled', () => { - itemList.items[2].disabled = true; + describe('ActiveDescendantKeyManager', () => { + let keyManager: ActiveDescendantKeyManager; - keyManager.focusLastItem(); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected the second to last item to be focused if the last was disabled.`); - }); + beforeEach(() => { + itemList.items = [ + new FakeHighlightable(), + new FakeHighlightable(), + new FakeHighlightable() + ]; - it('should focus the next item when focusNextItem() is called', () => { - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to be on the first item of the list.`); + keyManager = new ActiveDescendantKeyManager(itemList); - keyManager.focusNextItem(); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected focusNextItem() to move the focus to the next item.`); - }); + // first item is already focused + keyManager.setFirstItemActive(); - it('should focus the next enabled item if next is disabled', () => { - itemList.items[1].disabled = true; - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focus to be on the first item of the list.`); + spyOn(itemList.items[0], 'setActiveStyles'); + spyOn(itemList.items[1], 'setActiveStyles'); + spyOn(itemList.items[2], 'setActiveStyles'); - keyManager.focusNextItem(); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected focusNextItem() to focus only enabled items.`); + spyOn(itemList.items[0], 'setInactiveStyles'); + spyOn(itemList.items[1], 'setInactiveStyles'); + spyOn(itemList.items[2], 'setInactiveStyles'); }); - it('should focus the previous item when focusPreviousItem() is called', () => { + it('should set subsequent items as active with the DOWN arrow', () => { keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(1, `Expected focus to be on the second item of the list.`); - keyManager.focusPreviousItem(); - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focusPreviousItem() to move the focus to the last item.`); - }); + expect(itemList.items[1].setActiveStyles).toHaveBeenCalled(); + expect(itemList.items[2].setActiveStyles).not.toHaveBeenCalled(); - it('should skip disabled items when focusPreviousItem() is called', () => { - itemList.items[1].disabled = true; keyManager.onKeydown(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(keyManager.focusedItemIndex) - .toBe(2, `Expected focus to be on the third item of the list.`); - - keyManager.focusPreviousItem(); - expect(keyManager.focusedItemIndex) - .toBe(0, `Expected focusPreviousItem() to skip the disabled item.`); + expect(itemList.items[2].setActiveStyles).toHaveBeenCalled(); }); - }); + it('should set previous items as active with the UP arrow', () => { + keyManager.setLastItemActive(); - describe('wrap mode', () => { + keyManager.onKeydown(UP_ARROW_EVENT); + expect(itemList.items[1].setActiveStyles).toHaveBeenCalled(); + expect(itemList.items[0].setActiveStyles).not.toHaveBeenCalled(); - it('should return itself to allow chaining', () => { - expect(keyManager.withFocusWrap()) - .toEqual(keyManager, `Expected withFocusWrap() to return an instance of ListKeyManager`); + keyManager.onKeydown(UP_ARROW_EVENT); + expect(itemList.items[0].setActiveStyles).toHaveBeenCalled(); }); - it('should wrap focus when arrow keying past items while in wrap mode', () => { - keyManager.withFocusWrap(); - keyManager.onKeydown(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); - - expect(itemList.items[0].focus).not.toHaveBeenCalled(); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); - - // this down arrow moves down past the end of the list + it('should set inactive styles on previously active items', () => { keyManager.onKeydown(DOWN_ARROW_EVENT); - expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(1); + expect(itemList.items[0].setInactiveStyles).toHaveBeenCalled(); - // this up arrow moves up past the beginning of the list keyManager.onKeydown(UP_ARROW_EVENT); - expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - expect(itemList.items[2].focus).toHaveBeenCalledTimes(2); + expect(itemList.items[1].setInactiveStyles).toHaveBeenCalled(); }); }); + }); diff --git a/src/lib/core/a11y/list-key-manager.ts b/src/lib/core/a11y/list-key-manager.ts index a7196f9483a4..01004d2239d4 100644 --- a/src/lib/core/a11y/list-key-manager.ts +++ b/src/lib/core/a11y/list-key-manager.ts @@ -4,63 +4,64 @@ import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; /** - * This is the interface for focusable items (used by the ListKeyManager). - * Each item must know how to focus itself and whether or not it is currently disabled. + * This interface is for items that can be disabled. The type passed into + * ListKeyManager must extend this interface. */ -export interface Focusable { - focus(): void; +export interface CanDisable { disabled?: boolean; } /** * This class manages keyboard events for selectable lists. If you pass it a query list - * of focusable items, it will focus the correct item when arrow events occur. + * of items, it will set the active item correctly when arrow events occur. */ -export class ListKeyManager { - private _focusedItemIndex: number; +export class ListKeyManager { + private _activeItemIndex: number; + private _activeItem: T; private _tabOut: Subject = new Subject(); private _wrap: boolean = false; - constructor(private _items: QueryList) {} + constructor(private _items: QueryList) { + } /** - * Turns on focus wrapping mode, which ensures that the focus will wrap to + * Turns on wrapping mode, which ensures that the active item will wrap to * the other end of list when there are no more items in the given direction. * * @returns The ListKeyManager that the method was called on. */ - withFocusWrap(): this { + withWrap(): this { this._wrap = true; return this; } /** - * Sets the focus of the list to the item at the index specified. + * Sets the active item to the item at the index specified. * - * @param index The index of the item to be focused. + * @param index The index of the item to be set as active. */ - setFocus(index: number): void { - this._focusedItemIndex = index; - this._items.toArray()[index].focus(); + setActiveItem(index: number): void { + this._activeItemIndex = index; + this._activeItem = this._items.toArray()[index]; } /** - * Sets the focus depending on the key event passed in. - * @param event Keyboard event to be used for determining which element to focus. + * Sets the active item depending on the key event passed in. + * @param event Keyboard event to be used for determining which element should be active. */ onKeydown(event: KeyboardEvent): void { switch (event.keyCode) { case DOWN_ARROW: - this.focusNextItem(); + this.setNextItemActive(); break; case UP_ARROW: - this.focusPreviousItem(); + this.setPreviousItemActive(); break; case HOME: - this.focusFirstItem(); + this.setFirstItemActive(); break; case END: - this.focusLastItem(); + this.setLastItemActive(); break; case TAB: // Note that we shouldn't prevent the default action on tab. @@ -73,37 +74,42 @@ export class ListKeyManager { event.preventDefault(); } - /** Focuses the first enabled item in the list. */ - focusFirstItem(): void { - this._setFocusByIndex(0, 1); + /** Returns the index of the currently active item. */ + get activeItemIndex(): number { + return this._activeItemIndex; + } + + /** Returns the currently active item. */ + get activeItem(): T { + return this._activeItem; } - /** Focuses the last enabled item in the list. */ - focusLastItem(): void { - this._setFocusByIndex(this._items.length - 1, -1); + /** Sets the active item to the first enabled item in the list. */ + setFirstItemActive(): void { + this._setActiveItemByIndex(0, 1); } - /** Focuses the next enabled item in the list. */ - focusNextItem(): void { - this._setFocusByDelta(1); + /** Sets the active item to the last enabled item in the list. */ + setLastItemActive(): void { + this._setActiveItemByIndex(this._items.length - 1, -1); } - /** Focuses a previous enabled item in the list. */ - focusPreviousItem(): void { - this._setFocusByDelta(-1); + /** Sets the active item to the next enabled item in the list. */ + setNextItemActive(): void { + this._setActiveItemByDelta(1); } - /** Returns the index of the currently focused item. */ - get focusedItemIndex(): number { - return this._focusedItemIndex; + /** Sets the active item to a previous enabled item in the list. */ + setPreviousItemActive(): void { + this._setActiveItemByDelta(-1); } /** - * Allows setting of the focusedItemIndex without focusing the item. - * @param index The new focusedItemIndex. + * Allows setting of the activeItemIndex without any other effects. + * @param index The new activeItemIndex. */ - updateFocusedItemIndex(index: number) { - this._focusedItemIndex = index; + updateActiveItemIndex(index: number) { + this._activeItemIndex = index; } /** @@ -115,56 +121,56 @@ export class ListKeyManager { } /** - * This method sets focus to the correct item, given a list of items and the delta - * between the currently focused item and the new item to be focused. It will calculate - * the proper focus differently depending on whether wrap mode is turned on. + * This method sets the active item, given a list of items and the delta between the + * currently active item and the new active item. It will calculate differently + * depending on whether wrap mode is turned on. */ - private _setFocusByDelta(delta: number, items = this._items.toArray()): void { - this._wrap ? this._setWrapModeFocus(delta, items) - : this._setDefaultModeFocus(delta, items); + private _setActiveItemByDelta(delta: number, items = this._items.toArray()): void { + this._wrap ? this._setActiveInWrapMode(delta, items) + : this._setActiveInDefaultMode(delta, items); } /** - * Sets the focus properly given "wrap" mode. In other words, it will continue to move + * Sets the active item properly given "wrap" mode. In other words, it will continue to move * down the list until it finds an item that is not disabled, and it will wrap if it * encounters either end of the list. */ - private _setWrapModeFocus(delta: number, items: Focusable[]): void { - // when focus would leave menu, wrap to beginning or end - this._focusedItemIndex = - (this._focusedItemIndex + delta + items.length) % items.length; - - // skip all disabled menu items recursively until an active one is reached - if (items[this._focusedItemIndex].disabled) { - this._setWrapModeFocus(delta, items); + private _setActiveInWrapMode(delta: number, items: T[]): void { + // when active item would leave menu, wrap to beginning or end + this._activeItemIndex = + (this._activeItemIndex + delta + items.length) % items.length; + + // skip all disabled menu items recursively until an enabled one is reached + if (items[this._activeItemIndex].disabled) { + this._setActiveInWrapMode(delta, items); } else { - items[this._focusedItemIndex].focus(); + this.setActiveItem(this._activeItemIndex); } } /** - * Sets the focus properly given the default mode. In other words, it will + * Sets the active item properly given the default mode. In other words, it will * continue to move down the list until it finds an item that is not disabled. If * it encounters either end of the list, it will stop and not wrap. */ - private _setDefaultModeFocus(delta: number, items: Focusable[]): void { - this._setFocusByIndex(this._focusedItemIndex + delta, delta, items); + private _setActiveInDefaultMode(delta: number, items: T[]): void { + this._setActiveItemByIndex(this._activeItemIndex + delta, delta, items); } /** - * Sets the focus to the first enabled item starting at the index specified. If the + * Sets the active item to the first enabled item starting at the index specified. If the * item is disabled, it will move in the fallbackDelta direction until it either * finds an enabled item or encounters the end of the list. */ - private _setFocusByIndex(index: number, fallbackDelta: number, - items = this._items.toArray()): void { + private _setActiveItemByIndex(index: number, fallbackDelta: number, + items = this._items.toArray()): void { if (!items[index]) { return; } while (items[index].disabled) { index += fallbackDelta; if (!items[index]) { return; } } - - this.setFocus(index); + this.setActiveItem(index); } } + diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index 07e9cded92df..765fb56030b0 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -17,7 +17,7 @@ import { import {MenuPositionX, MenuPositionY} from './menu-positions'; import {MdMenuInvalidPositionX, MdMenuInvalidPositionY} from './menu-errors'; import {MdMenuItem} from './menu-item'; -import {ListKeyManager} from '../core/a11y/list-key-manager'; +import {FocusKeyManager} from '../core/a11y/focus-key-manager'; import {MdMenuPanel} from './menu-panel'; import {Subscription} from 'rxjs/Subscription'; import {transformMenu, fadeInItems} from './menu-animations'; @@ -36,7 +36,7 @@ import {transformMenu, fadeInItems} from './menu-animations'; exportAs: 'mdMenu' }) export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { - private _keyManager: ListKeyManager; + private _keyManager: FocusKeyManager; /** Subscription to tab events on the menu panel */ private _tabSubscription: Subscription; @@ -61,7 +61,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { } ngAfterContentInit() { - this._keyManager = new ListKeyManager(this.items).withFocusWrap(); + this._keyManager = new FocusKeyManager(this.items).withWrap(); this._tabSubscription = this._keyManager.tabOut.subscribe(() => { this._emitCloseEvent(); }); @@ -94,7 +94,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { * to focus the first item when the menu is opened by the ENTER key. */ focusFirstItem() { - this._keyManager.focusFirstItem(); + this._keyManager.setFirstItemActive(); } /** diff --git a/src/lib/menu/menu-item.ts b/src/lib/menu/menu-item.ts index 0425c0c6dc3f..9889fc74ebf4 100644 --- a/src/lib/menu/menu-item.ts +++ b/src/lib/menu/menu-item.ts @@ -1,5 +1,5 @@ import {Component, ElementRef, Input, HostBinding, Renderer} from '@angular/core'; -import {Focusable} from '../core/a11y/list-key-manager'; +import {Focusable} from '../core/a11y/focus-key-manager'; /** * This directive is intended to be used inside an md-menu tag. diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index c2e3e6a12d53..075af4e2be12 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -128,7 +128,7 @@ describe('MdSelect', () => { fixture.detectChanges(); fixture.whenStable().then(() => { - expect(fixture.componentInstance.select._keyManager.focusedItemIndex).toEqual(0); + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(0); }); })); @@ -203,7 +203,7 @@ describe('MdSelect', () => { // must wait for animation to finish fixture.whenStable().then(() => { fixture.detectChanges(); - expect(fixture.componentInstance.select._keyManager.focusedItemIndex).toEqual(1); + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(1); }); }); })); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 8f408c1f15fa..4dd3277f2be5 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -15,7 +15,7 @@ import { } from '@angular/core'; import {MdOption} from '../core/option/option'; import {ENTER, SPACE} from '../core/keyboard/keycodes'; -import {ListKeyManager} from '../core/a11y/list-key-manager'; +import {FocusKeyManager} from '../core/a11y/focus-key-manager'; import {Dir} from '../core/rtl/dir'; import {Subscription} from 'rxjs/Subscription'; import {transformPlaceholder, transformPanel, fadeInContent} from './select-animations'; @@ -133,7 +133,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr _selectedValueWidth: number; /** Manages keyboard events for options in the panel. */ - _keyManager: ListKeyManager; + _keyManager: FocusKeyManager; /** View -> model callback called when value changes */ _onChange = (value: any) => {}; @@ -412,7 +412,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr /** Sets up a key manager to listen to keyboard events on the overlay panel. */ private _initKeyManager() { - this._keyManager = new ListKeyManager(this.options); + this._keyManager = new FocusKeyManager(this.options); this._tabSubscription = this._keyManager.tabOut.subscribe(() => { this.close(); }); @@ -483,9 +483,9 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr */ private _focusCorrectOption(): void { if (this.selected) { - this._keyManager.setFocus(this._getOptionIndex(this.selected)); + this._keyManager.setActiveItem(this._getOptionIndex(this.selected)); } else { - this._keyManager.focusFirstItem(); + this._keyManager.setFirstItemActive(); } }