diff --git a/e2e/components/list/list.e2e.ts b/e2e/components/list/list.e2e.ts
index 708ff9943ef0..6133737ad21e 100644
--- a/e2e/components/list/list.e2e.ts
+++ b/e2e/components/list/list.e2e.ts
@@ -9,4 +9,38 @@ describe('list', () => {
let container = element(by.css('md-list'));
expect(container.isElementPresent(by.css('md-list-item'))).toBe(true);
});
+
+ it('should be tabbable', () => {
+ pressKey(protractor.Key.TAB);
+ expectFocusOn(element(by.css('md-list')));
+ });
+
+ it('should shift focus between the list items', () => {
+ let items = element.all(by.css('md-list-item'));
+
+ pressKey(protractor.Key.TAB);
+ pressKey(protractor.Key.DOWN);
+ expectFocusOn(items.get(0));
+
+ pressKey(protractor.Key.DOWN);
+ expectFocusOn(items.get(1));
+
+ pressKey(protractor.Key.DOWN);
+ expectFocusOn(items.get(2));
+
+ pressKey(protractor.Key.UP);
+ expectFocusOn(items.get(1));
+
+ pressKey(protractor.Key.UP);
+ expectFocusOn(items.get(0));
+ });
+
+ // TODO: move to utility file. this was taken from the menu-page.ts
+ function expectFocusOn(el: any): void {
+ expect(browser.driver.switchTo().activeElement().getInnerHtml()).toBe(el.getInnerHtml());
+ }
+
+ function pressKey(key: string) {
+ browser.actions().sendKeys(key).perform();
+ }
});
diff --git a/src/e2e-app/list/list-e2e.html b/src/e2e-app/list/list-e2e.html
index 676a29d5dd92..12ff3ccdd5ca 100644
--- a/src/e2e-app/list/list-e2e.html
+++ b/src/e2e-app/list/list-e2e.html
@@ -2,4 +2,5 @@
Items
Item one
Item two
+ Item three
diff --git a/src/lib/list/index.ts b/src/lib/list/index.ts
index 71825137f468..4e413160dd25 100644
--- a/src/lib/list/index.ts
+++ b/src/lib/list/index.ts
@@ -1 +1,20 @@
-export * from './list';
+import {NgModule, ModuleWithProviders} from '@angular/core';
+import {MdLineModule} from '../core';
+import {MdList} from './list';
+import {MdListItem} from './list-item';
+import {MdListDivider, MdListAvatar} from './list-directives';
+
+
+@NgModule({
+ imports: [MdLineModule],
+ exports: [MdList, MdListItem, MdListDivider, MdListAvatar, MdLineModule],
+ declarations: [MdList, MdListItem, MdListDivider, MdListAvatar],
+})
+export class MdListModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: MdListModule,
+ providers: []
+ };
+ }
+}
diff --git a/src/lib/list/list-directives.ts b/src/lib/list/list-directives.ts
new file mode 100644
index 000000000000..0749995f3138
--- /dev/null
+++ b/src/lib/list/list-directives.ts
@@ -0,0 +1,9 @@
+import {Directive} from '@angular/core';
+
+
+@Directive({ selector: 'md-divider' })
+export class MdListDivider { }
+
+/* Need directive for a ContentChild query in list-item */
+@Directive({ selector: '[md-list-avatar]' })
+export class MdListAvatar { }
diff --git a/src/lib/list/list-item.ts b/src/lib/list/list-item.ts
new file mode 100644
index 000000000000..0501b36f14a2
--- /dev/null
+++ b/src/lib/list/list-item.ts
@@ -0,0 +1,58 @@
+import {
+ Component,
+ ViewEncapsulation,
+ ContentChildren,
+ ContentChild,
+ QueryList,
+ ElementRef,
+ Renderer,
+ AfterContentInit,
+} from '@angular/core';
+import {MdLine, MdLineSetter} from '../core';
+import {MdListAvatar} from './list-directives';
+import {MdFocusable} from '../core/a11y/list-key-manager';
+
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-list-item, a[md-list-item]',
+ host: {
+ 'role': 'listitem',
+ '(focus)': '_handleFocus()',
+ '(blur)': '_handleBlur()',
+ 'tabIndex': '-1'
+ },
+ templateUrl: 'list-item.html',
+ encapsulation: ViewEncapsulation.None
+})
+export class MdListItem implements AfterContentInit, MdFocusable {
+ _hasFocus: boolean = false;
+
+ private _lineSetter: MdLineSetter;
+
+ @ContentChildren(MdLine) _lines: QueryList;
+
+ @ContentChild(MdListAvatar)
+ set _hasAvatar(avatar: MdListAvatar) {
+ this._renderer.setElementClass(this._element.nativeElement, 'md-list-avatar', avatar != null);
+ }
+
+ constructor(private _renderer: Renderer, private _element: ElementRef) { }
+
+ /** TODO: internal */
+ ngAfterContentInit() {
+ this._lineSetter = new MdLineSetter(this._lines, this._renderer, this._element);
+ }
+
+ _handleFocus() {
+ this._hasFocus = true;
+ }
+
+ _handleBlur() {
+ this._hasFocus = false;
+ }
+
+ focus() {
+ this._renderer.invokeElementMethod(this._element.nativeElement, 'focus');
+ }
+}
diff --git a/src/lib/list/list.scss b/src/lib/list/list.scss
index 064c7e6dd6af..dcde4a9643f7 100644
--- a/src/lib/list/list.scss
+++ b/src/lib/list/list.scss
@@ -106,6 +106,7 @@ $md-dense-three-line-height: 76px;
md-list, md-nav-list {
padding-top: $md-list-top-padding;
display: block;
+ outline: 0;
[md-subheader] {
@include md-subheader-base(
diff --git a/src/lib/list/list.spec.ts b/src/lib/list/list.spec.ts
index d8ebef1c604b..f4a41e1ccb45 100644
--- a/src/lib/list/list.spec.ts
+++ b/src/lib/list/list.spec.ts
@@ -1,7 +1,8 @@
import {async, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {By} from '@angular/platform-browser';
-import {MdListItem, MdListModule} from './list';
+import {MdListModule} from './index';
+import {MdListItem} from './list-item';
describe('MdList', () => {
@@ -19,6 +20,7 @@ describe('MdList', () => {
ListWithDynamicNumberOfLines,
ListWithMultipleItems,
ListWithManyLines,
+ ListWithTabIndex,
],
});
@@ -114,6 +116,15 @@ describe('MdList', () => {
expect(list.nativeElement.getAttribute('role')).toBe('list');
expect(listItem.nativeElement.getAttribute('role')).toBe('listitem');
});
+
+ it('should forward the tabindex to the list element', () => {
+ let fixture = TestBed.createComponent(ListWithTabIndex);
+ let list = fixture.debugElement.children[0].nativeElement;
+
+ fixture.detectChanges();
+
+ expect(list.getAttribute('tabindex')).toBe('1');
+ });
});
@@ -211,3 +222,11 @@ class ListWithDynamicNumberOfLines extends BaseTestList { }
`})
class ListWithMultipleItems extends BaseTestList { }
+
+@Component({template: `
+
+
+ Paprika
+
+ `})
+class ListWithTabIndex extends BaseTestList { }
diff --git a/src/lib/list/list.ts b/src/lib/list/list.ts
index 755b40ed0721..0faab15afcbf 100644
--- a/src/lib/list/list.ts
+++ b/src/lib/list/list.ts
@@ -2,86 +2,49 @@ import {
Component,
ViewEncapsulation,
ContentChildren,
- ContentChild,
QueryList,
- Directive,
ElementRef,
- Renderer,
AfterContentInit,
- NgModule,
- ModuleWithProviders,
+ Input,
} from '@angular/core';
-import {MdLine, MdLineSetter, MdLineModule} from '../core';
+import {ListKeyManager} from '../core/a11y/list-key-manager';
+import {DOWN_ARROW} from '../core/keyboard/keycodes';
+import {MdListItem} from './list-item';
-@Directive({
- selector: 'md-divider'
-})
-export class MdListDivider {}
@Component({
moduleId: module.id,
selector: 'md-list, md-nav-list',
- host: {'role': 'list'},
- template: '',
- styleUrls: ['list.css'],
- encapsulation: ViewEncapsulation.None
-})
-export class MdList {}
-
-/* Need directive for a ContentChild query in list-item */
-@Directive({ selector: '[md-list-avatar]' })
-export class MdListAvatar {}
-
-@Component({
- moduleId: module.id,
- selector: 'md-list-item, a[md-list-item]',
host: {
- 'role': 'listitem',
- '(focus)': '_handleFocus()',
- '(blur)': '_handleBlur()',
+ 'role': 'list',
+ '[attr.tabIndex]': 'tabindex',
+ '(keydown)': '_handleKeydown($event)'
},
- templateUrl: 'list-item.html',
+ template: '',
+ styleUrls: ['list.css'],
encapsulation: ViewEncapsulation.None
})
-export class MdListItem implements AfterContentInit {
- _hasFocus: boolean = false;
+export class MdList implements AfterContentInit {
+ @ContentChildren(MdListItem) _items: QueryList;
+ @Input() tabindex: number = 0;
- private _lineSetter: MdLineSetter;
+ /** Manages the keyboard events between list items. */
+ private _keyManager: ListKeyManager;
- @ContentChildren(MdLine) _lines: QueryList;
+ constructor(private _elementRef: ElementRef) { }
- @ContentChild(MdListAvatar)
- set _hasAvatar(avatar: MdListAvatar) {
- this._renderer.setElementClass(this._element.nativeElement, 'md-list-avatar', avatar != null);
- }
-
- constructor(private _renderer: Renderer, private _element: ElementRef) {}
-
- /** TODO: internal */
ngAfterContentInit() {
- this._lineSetter = new MdLineSetter(this._lines, this._renderer, this._element);
- }
-
- _handleFocus() {
- this._hasFocus = true;
+ this._keyManager = new ListKeyManager(this._items);
}
- _handleBlur() {
- this._hasFocus = false;
- }
-}
-
-
-@NgModule({
- imports: [MdLineModule],
- exports: [MdList, MdListItem, MdListDivider, MdListAvatar, MdLineModule],
- declarations: [MdList, MdListItem, MdListDivider, MdListAvatar],
-})
-export class MdListModule {
- static forRoot(): ModuleWithProviders {
- return {
- ngModule: MdListModule,
- providers: []
- };
+ /**
+ * Shifts focus to the appropriate list item.
+ */
+ _handleKeydown(event: KeyboardEvent) {
+ if (event.target === this._elementRef.nativeElement && event.keyCode === DOWN_ARROW) {
+ this._keyManager.focusFirstItem();
+ } else {
+ this._keyManager.onKeydown(event);
+ }
}
}