Skip to content

Commit

Permalink
feat(a11y): manager for list keyboard events
Browse files Browse the repository at this point in the history
  • Loading branch information
kara committed Oct 25, 2016
1 parent c0e6f83 commit 54d2359
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 50 deletions.
64 changes: 64 additions & 0 deletions src/lib/core/keyboard/ListKeyManager.ts
Original file line number Diff line number Diff line change
@@ -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<null> = new EventEmitter();

constructor(private _items: QueryList<MdFocusable>) {}

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

}
56 changes: 8 additions & 48 deletions src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion src/lib/menu/menu-item.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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) {}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/menu/menu.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="md-menu-panel" [ngClass]="_classList"
(click)="_emitCloseEvent()" (keydown)="_handleKeydown($event)">
(click)="_emitCloseEvent()" (keydown)="_keyManager.onKeydown($event)">
<ng-content></ng-content>
</div>
</template>
Expand Down

0 comments on commit 54d2359

Please sign in to comment.