Skip to content

Commit 54d2359

Browse files
committed
feat(a11y): manager for list keyboard events
1 parent c0e6f83 commit 54d2359

File tree

4 files changed

+75
-50
lines changed

4 files changed

+75
-50
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {EventEmitter, Output, QueryList} from '@angular/core';
2+
import {UP_ARROW, DOWN_ARROW, TAB} from '../core';
3+
4+
export interface MdFocusable {
5+
focus(): void;
6+
disabled: boolean;
7+
}
8+
9+
export class ListKeyManager {
10+
private _focusedItemIndex: number;
11+
12+
@Output() tabOut: EventEmitter<null> = new EventEmitter();
13+
14+
constructor(private _items: QueryList<MdFocusable>) {}
15+
16+
set focusedItemIndex(value: number) {
17+
this._focusedItemIndex = value;
18+
}
19+
20+
// TODO(kara): update this when (keydown.downArrow) testability is fixed
21+
onKeydown(event: KeyboardEvent): void {
22+
if (event.keyCode === DOWN_ARROW) {
23+
this._focusNextItem();
24+
} else if (event.keyCode === UP_ARROW) {
25+
this._focusPreviousItem();
26+
} else if (event.keyCode === TAB) {
27+
this.tabOut.emit(null);
28+
this._focusedItemIndex = null;
29+
}
30+
}
31+
32+
private _focusNextItem(): void {
33+
const items = this._items.toArray();
34+
this._updateFocusedItemIndex(1, items);
35+
items[this._focusedItemIndex].focus();
36+
}
37+
38+
private _focusPreviousItem(): void {
39+
const items = this._items.toArray();
40+
this._updateFocusedItemIndex(-1, items);
41+
items[this._focusedItemIndex].focus();
42+
}
43+
44+
/**
45+
* This method sets focus to the correct menu item, given a list of menu items and the delta
46+
* between the currently focused menu item and the new menu item to be focused. It will
47+
* continue to move down the list until it finds an item that is not disabled, and it will wrap
48+
* if it encounters either end of the menu.
49+
*
50+
* @param delta the desired change in focus index
51+
*/
52+
private _updateFocusedItemIndex(delta: number, items: MdFocusable[]) {
53+
// when focus would leave menu, wrap to beginning or end
54+
this._focusedItemIndex = (this._focusedItemIndex + delta + items.length)
55+
% items.length;
56+
57+
// skip all disabled menu items recursively until an active one
58+
// is reached or the menu closes for overreaching bounds
59+
while (items[this._focusedItemIndex].disabled) {
60+
this._updateFocusedItemIndex(delta, items);
61+
}
62+
}
63+
64+
}

src/lib/menu/menu-directive.ts

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import {MenuPositionX, MenuPositionY} from './menu-positions';
1616
import {MdMenuInvalidPositionX, MdMenuInvalidPositionY} from './menu-errors';
1717
import {MdMenuItem} from './menu-item';
18-
import {UP_ARROW, DOWN_ARROW, TAB} from '../core';
18+
import {ListKeyManager} from '../core/keyboard/ListKeyManager';
1919

2020
@Component({
2121
moduleId: module.id,
@@ -27,7 +27,7 @@ import {UP_ARROW, DOWN_ARROW, TAB} from '../core';
2727
exportAs: 'mdMenu'
2828
})
2929
export class MdMenu {
30-
private _focusedItemIndex: number = 0;
30+
private _keyManager: ListKeyManager;
3131

3232
// config object to be passed into the menu's ngClass
3333
_classList: Object;
@@ -44,6 +44,11 @@ export class MdMenu {
4444
if (posY) { this._setPositionY(posY); }
4545
}
4646

47+
ngAfterContentInit() {
48+
this._keyManager = new ListKeyManager(this.items);
49+
this._keyManager.tabOut.subscribe(() => this._emitCloseEvent());
50+
}
51+
4752
/**
4853
* This method takes classes set on the host md-menu element and applies them on the
4954
* menu template that displays in the overlay container. Otherwise, it's difficult
@@ -67,61 +72,16 @@ export class MdMenu {
6772
*/
6873
_focusFirstItem() {
6974
this.items.first.focus();
75+
this._keyManager.focusedItemIndex = 0;
7076
}
71-
72-
// TODO(kara): update this when (keydown.downArrow) testability is fixed
73-
// TODO: internal
74-
_handleKeydown(event: KeyboardEvent): void {
75-
if (event.keyCode === DOWN_ARROW) {
76-
this._focusNextItem();
77-
} else if (event.keyCode === UP_ARROW) {
78-
this._focusPreviousItem();
79-
} else if (event.keyCode === TAB) {
80-
this._emitCloseEvent();
81-
}
82-
}
83-
8477
/**
8578
* This emits a close event to which the trigger is subscribed. When emitted, the
8679
* trigger will close the menu.
8780
*/
8881
private _emitCloseEvent(): void {
89-
this._focusedItemIndex = 0;
9082
this.close.emit(null);
9183
}
9284

93-
private _focusNextItem(): void {
94-
this._updateFocusedItemIndex(1);
95-
this.items.toArray()[this._focusedItemIndex].focus();
96-
}
97-
98-
private _focusPreviousItem(): void {
99-
this._updateFocusedItemIndex(-1);
100-
this.items.toArray()[this._focusedItemIndex].focus();
101-
}
102-
103-
/**
104-
* This method sets focus to the correct menu item, given a list of menu items and the delta
105-
* between the currently focused menu item and the new menu item to be focused. It will
106-
* continue to move down the list until it finds an item that is not disabled, and it will wrap
107-
* if it encounters either end of the menu.
108-
*
109-
* @param delta the desired change in focus index
110-
* @param menuItems the menu items that should be focused
111-
* @private
112-
*/
113-
private _updateFocusedItemIndex(delta: number, menuItems: MdMenuItem[] = this.items.toArray()) {
114-
// when focus would leave menu, wrap to beginning or end
115-
this._focusedItemIndex = (this._focusedItemIndex + delta + this.items.length)
116-
% this.items.length;
117-
118-
// skip all disabled menu items recursively until an active one
119-
// is reached or the menu closes for overreaching bounds
120-
while (menuItems[this._focusedItemIndex].disabled) {
121-
this._updateFocusedItemIndex(delta, menuItems);
122-
}
123-
}
124-
12585
private _setPositionX(pos: MenuPositionX): void {
12686
if ( pos !== 'before' && pos !== 'after') {
12787
throw new MdMenuInvalidPositionX();

src/lib/menu/menu-item.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Directive, ElementRef, Input, HostBinding, Renderer} from '@angular/core';
2+
import {MdFocusable} from '../core/keyboard/ListKeyManager';
23

34
/**
45
* 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
1314
},
1415
exportAs: 'mdMenuItem'
1516
})
16-
export class MdMenuItem {
17+
export class MdMenuItem implements MdFocusable {
1718
_disabled: boolean;
1819

1920
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}

src/lib/menu/menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="md-menu-panel" [ngClass]="_classList"
3-
(click)="_emitCloseEvent()" (keydown)="_handleKeydown($event)">
3+
(click)="_emitCloseEvent()" (keydown)="_keyManager.onKeydown($event)">
44
<ng-content></ng-content>
55
</div>
66
</template>

0 commit comments

Comments
 (0)