Skip to content

Commit

Permalink
feat(a11y): add wrap mode to key manager (#1796)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored and jelbourn committed Nov 11, 2016
1 parent 068fa85 commit 3d4abac
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 122 deletions.
326 changes: 247 additions & 79 deletions src/lib/core/a11y/list-key-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,293 @@
import {QueryList} from '@angular/core';
import {ListKeyManager, MdFocusable} from './list-key-manager';
import {DOWN_ARROW, UP_ARROW, TAB} from '../keyboard/keycodes';
import {ListKeyManager} from './list-key-manager';
import {DOWN_ARROW, UP_ARROW, TAB, HOME, END} from '../keyboard/keycodes';

class FakeFocusable {
disabled = false;
focus() {}
}

class FakeQueryList<T> extends QueryList<T> {
get length() { return this.items.length; }
items: T[];
toArray() {
return this.items;
}
}

const DOWN_ARROW_EVENT = { keyCode: DOWN_ARROW } as KeyboardEvent;
const UP_ARROW_EVENT = { keyCode: UP_ARROW } as KeyboardEvent;
const TAB_EVENT = { keyCode: TAB } as KeyboardEvent;
const HOME_EVENT = { keyCode: HOME } as KeyboardEvent;
const END_EVENT = { keyCode: END } as KeyboardEvent;

describe('ListKeyManager', () => {
let keyManager: ListKeyManager;
let itemList: QueryList<MdFocusable>;
let items: MdFocusable[];
let itemList: FakeQueryList<FakeFocusable>;

beforeEach(() => {
itemList = new QueryList<MdFocusable>();
items = [
itemList = new FakeQueryList<FakeFocusable>();
itemList.items = [
new FakeFocusable(),
new FakeFocusable(),
new FakeFocusable()
];

itemList.toArray = () => items;

keyManager = new ListKeyManager(itemList);

// first item is already focused
keyManager.focusedItemIndex = 0;
keyManager.focusFirstItem();

spyOn(items[0], 'focus');
spyOn(items[1], 'focus');
spyOn(items[2], 'focus');
spyOn(itemList.items[0], 'focus');
spyOn(itemList.items[1], 'focus');
spyOn(itemList.items[2], 'focus');
});

it('should focus subsequent items when down arrow is pressed', () => {
keyManager.onKeydown(DOWN_ARROW_EVENT);
describe('key events', () => {
it('should focus subsequent items when down arrow is pressed', () => {
keyManager.onKeydown(DOWN_ARROW_EVENT);

expect(items[0].focus).not.toHaveBeenCalled();
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(items[2].focus).not.toHaveBeenCalled();
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(items[0].focus).not.toHaveBeenCalled();
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(items[2].focus).toHaveBeenCalledTimes(1);
});
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);
it('should focus previous items when up arrow is pressed', () => {
keyManager.onKeydown(DOWN_ARROW_EVENT);

expect(items[0].focus).not.toHaveBeenCalled();
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(itemList.items[0].focus).not.toHaveBeenCalled();
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);

keyManager.onKeydown(UP_ARROW_EVENT);
keyManager.onKeydown(UP_ARROW_EVENT);

expect(items[0].focus).toHaveBeenCalledTimes(1);
expect(items[1].focus).toHaveBeenCalledTimes(1);
});
expect(itemList.items[0].focus).toHaveBeenCalledTimes(1);
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
});

it('should skip disabled items using arrow keys', () => {
items[1].disabled = true;
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(items[0].focus).not.toHaveBeenCalled();
expect(items[1].focus).not.toHaveBeenCalled();
expect(items[2].focus).toHaveBeenCalledTimes(1);
// 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(items[0].focus).toHaveBeenCalledTimes(1);
expect(items[1].focus).not.toHaveBeenCalled();
expect(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;

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

it('should work normally when disabled property does not exist', () => {
items[0].disabled = undefined;
items[1].disabled = undefined;
items[2].disabled = undefined;
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.`);

keyManager.onKeydown(DOWN_ARROW_EVENT);
expect(items[0].focus).not.toHaveBeenCalled();
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(items[2].focus).not.toHaveBeenCalled();
// 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 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.`);

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

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.`);

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

keyManager.onKeydown(DOWN_ARROW_EVENT);
expect(items[0].focus).not.toHaveBeenCalled();
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(items[2].focus).toHaveBeenCalledTimes(1);
});

it('should wrap back to menu when arrow keying past items', () => {
keyManager.onKeydown(DOWN_ARROW_EVENT);
keyManager.onKeydown(DOWN_ARROW_EVENT);

expect(items[0].focus).not.toHaveBeenCalled();
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(items[2].focus).toHaveBeenCalledTimes(1);

// this down arrow moves down past the end of the list
keyManager.onKeydown(DOWN_ARROW_EVENT);
expect(items[0].focus).toHaveBeenCalledTimes(1);
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(items[2].focus).toHaveBeenCalledTimes(1);

// this up arrow moves up past the beginning of the list
keyManager.onKeydown(UP_ARROW_EVENT);
expect(items[0].focus).toHaveBeenCalledTimes(1);
expect(items[1].focus).toHaveBeenCalledTimes(1);
expect(items[2].focus).toHaveBeenCalledTimes(2);
describe('programmatic focus', () => {

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

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.`);

keyManager.focusFirstItem();
expect(keyManager.focusedItemIndex)
.toBe(0, `Expected focusFirstItem() to move the focus back to the first item.`);
});

it('should focus the second item if the first one is disabled', () => {
itemList.items[0].disabled = true;

keyManager.focusFirstItem();
expect(keyManager.focusedItemIndex)
.toBe(1, `Expected the second item to be focused if the first was disabled.`);
});

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.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;

keyManager.focusLastItem();
expect(keyManager.focusedItemIndex)
.toBe(1, `Expected the second to last item to be focused if the last was disabled.`);
});

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.focusNextItem();
expect(keyManager.focusedItemIndex)
.toBe(1, `Expected focusNextItem() to move the focus to the next item.`);
});

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.`);

keyManager.focusNextItem();
expect(keyManager.focusedItemIndex)
.toBe(2, `Expected focusNextItem() to focus only enabled items.`);
});

it('should focus the previous item when focusPreviousItem() is called', () => {
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.`);
});

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

});

it('should emit tabOut when the tab key is pressed', () => {
let tabOutEmitted = false;
keyManager.tabOut.first().subscribe(() => tabOutEmitted = true);
keyManager.onKeydown(TAB_EVENT);
describe('wrap mode', () => {

it('should return itself to allow chaining', () => {
expect(keyManager.withFocusWrap())
.toEqual(keyManager, `Expected withFocusWrap() to return an instance of ListKeyManager`);
});

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

// 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(tabOutEmitted).toBe(true);
});

});
Loading

0 comments on commit 3d4abac

Please sign in to comment.