diff --git a/src/lib/core/keyboard/ListKeyManager.ts b/src/lib/core/keyboard/ListKeyManager.ts new file mode 100644 index 000000000000..e6d4674a0b54 --- /dev/null +++ b/src/lib/core/keyboard/ListKeyManager.ts @@ -0,0 +1,64 @@ +import {EventEmitter, Output, QueryList} from '@angular/core'; +import {UP_ARROW, DOWN_ARROW, TAB} from '../core'; + +export interface MdFocusable { + focus(): void; + disabled: boolean; +} + +export class ListKeyManager { + private _focusedItemIndex: number; + + @Output() tabOut: EventEmitter = new EventEmitter(); + + constructor(private _items: QueryList) {} + + set focusedItemIndex(value: number) { + this._focusedItemIndex = value; + } + + // TODO(kara): update this when (keydown.downArrow) testability is fixed + onKeydown(event: KeyboardEvent): void { + if (event.keyCode === DOWN_ARROW) { + this._focusNextItem(); + } else if (event.keyCode === UP_ARROW) { + this._focusPreviousItem(); + } else if (event.keyCode === TAB) { + this.tabOut.emit(null); + this._focusedItemIndex = null; + } + } + + private _focusNextItem(): void { + const items = this._items.toArray(); + this._updateFocusedItemIndex(1, items); + items[this._focusedItemIndex].focus(); + } + + private _focusPreviousItem(): void { + const items = this._items.toArray(); + this._updateFocusedItemIndex(-1, items); + items[this._focusedItemIndex].focus(); + } + + /** + * This method sets focus to the correct menu item, given a list of menu items and the delta + * between the currently focused menu item and the new menu item to be focused. 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 menu. + * + * @param delta the desired change in focus index + */ + private _updateFocusedItemIndex(delta: number, items: MdFocusable[]) { + // 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 or the menu closes for overreaching bounds + while (items[this._focusedItemIndex].disabled) { + this._updateFocusedItemIndex(delta, items); + } + } + +} diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index 2a3eeaa36b7c..1a88c793c9c6 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -15,7 +15,7 @@ import { import {MenuPositionX, MenuPositionY} from './menu-positions'; import {MdMenuInvalidPositionX, MdMenuInvalidPositionY} from './menu-errors'; import {MdMenuItem} from './menu-item'; -import {UP_ARROW, DOWN_ARROW, TAB} from '../core'; +import {ListKeyManager} from '../core/keyboard/ListKeyManager'; @Component({ moduleId: module.id, @@ -27,7 +27,7 @@ import {UP_ARROW, DOWN_ARROW, TAB} from '../core'; exportAs: 'mdMenu' }) export class MdMenu { - private _focusedItemIndex: number = 0; + private _keyManager: ListKeyManager; // config object to be passed into the menu's ngClass _classList: Object; @@ -44,6 +44,11 @@ export class MdMenu { if (posY) { this._setPositionY(posY); } } + ngAfterContentInit() { + this._keyManager = new ListKeyManager(this.items); + this._keyManager.tabOut.subscribe(() => this._emitCloseEvent()); + } + /** * This method takes classes set on the host md-menu element and applies them on the * menu template that displays in the overlay container. Otherwise, it's difficult @@ -67,61 +72,16 @@ export class MdMenu { */ _focusFirstItem() { this.items.first.focus(); + this._keyManager.focusedItemIndex = 0; } - - // TODO(kara): update this when (keydown.downArrow) testability is fixed - // TODO: internal - _handleKeydown(event: KeyboardEvent): void { - if (event.keyCode === DOWN_ARROW) { - this._focusNextItem(); - } else if (event.keyCode === UP_ARROW) { - this._focusPreviousItem(); - } else if (event.keyCode === TAB) { - this._emitCloseEvent(); - } - } - /** * This emits a close event to which the trigger is subscribed. When emitted, the * trigger will close the menu. */ private _emitCloseEvent(): void { - this._focusedItemIndex = 0; this.close.emit(null); } - private _focusNextItem(): void { - this._updateFocusedItemIndex(1); - this.items.toArray()[this._focusedItemIndex].focus(); - } - - private _focusPreviousItem(): void { - this._updateFocusedItemIndex(-1); - this.items.toArray()[this._focusedItemIndex].focus(); - } - - /** - * This method sets focus to the correct menu item, given a list of menu items and the delta - * between the currently focused menu item and the new menu item to be focused. 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 menu. - * - * @param delta the desired change in focus index - * @param menuItems the menu items that should be focused - * @private - */ - private _updateFocusedItemIndex(delta: number, menuItems: MdMenuItem[] = this.items.toArray()) { - // when focus would leave menu, wrap to beginning or end - this._focusedItemIndex = (this._focusedItemIndex + delta + this.items.length) - % this.items.length; - - // skip all disabled menu items recursively until an active one - // is reached or the menu closes for overreaching bounds - while (menuItems[this._focusedItemIndex].disabled) { - this._updateFocusedItemIndex(delta, menuItems); - } - } - private _setPositionX(pos: MenuPositionX): void { if ( pos !== 'before' && pos !== 'after') { throw new MdMenuInvalidPositionX(); diff --git a/src/lib/menu/menu-item.ts b/src/lib/menu/menu-item.ts index 2b70779ebbf6..c9c8d93ca324 100644 --- a/src/lib/menu/menu-item.ts +++ b/src/lib/menu/menu-item.ts @@ -1,4 +1,5 @@ import {Directive, ElementRef, Input, HostBinding, Renderer} from '@angular/core'; +import {MdFocusable} from '../core/keyboard/ListKeyManager'; /** * This directive is intended to be used inside an md-menu tag. @@ -13,7 +14,7 @@ import {Directive, ElementRef, Input, HostBinding, Renderer} from '@angular/core }, exportAs: 'mdMenuItem' }) -export class MdMenuItem { +export class MdMenuItem implements MdFocusable { _disabled: boolean; constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} diff --git a/src/lib/menu/menu.html b/src/lib/menu/menu.html index 58721749878e..f23266c05da9 100644 --- a/src/lib/menu/menu.html +++ b/src/lib/menu/menu.html @@ -1,6 +1,6 @@