mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(list-key-manager): active descendant support (#2606)
This commit is contained in:
@@ -2,7 +2,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component, DebugElement, QueryList} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdChip, MdChipList, MdChipsModule} from './index';
|
||||
import {ListKeyManager} from '../core/a11y/list-key-manager';
|
||||
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
|
||||
import {FakeEvent} from '../core/a11y/list-key-manager.spec';
|
||||
import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('MdChipList', () => {
|
||||
let chipListInstance: MdChipList;
|
||||
let testComponent: StaticChipList;
|
||||
let chips: QueryList<MdChip>;
|
||||
let manager: ListKeyManager;
|
||||
let manager: FocusKeyManager;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -60,7 +60,7 @@ describe('MdChipList', () => {
|
||||
chipListInstance.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.focusedItemIndex).toBe(0);
|
||||
expect(manager.activeItemIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('watches for chip focus', () => {
|
||||
@@ -71,7 +71,7 @@ describe('MdChipList', () => {
|
||||
lastItem.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.focusedItemIndex).toBe(lastIndex);
|
||||
expect(manager.activeItemIndex).toBe(lastIndex);
|
||||
});
|
||||
|
||||
describe('on chip destroy', () => {
|
||||
@@ -87,7 +87,7 @@ describe('MdChipList', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the 4th item (now at index 2)
|
||||
expect(manager.focusedItemIndex).toEqual(2);
|
||||
expect(manager.activeItemIndex).toEqual(2);
|
||||
});
|
||||
|
||||
it('focuses the previous item', () => {
|
||||
@@ -103,7 +103,7 @@ describe('MdChipList', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.focusedItemIndex).toEqual(lastIndex - 1);
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -124,14 +124,14 @@ describe('MdChipList', () => {
|
||||
|
||||
// Focus the last item in the array
|
||||
lastItem.focus();
|
||||
expect(manager.focusedItemIndex).toEqual(lastIndex);
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex);
|
||||
|
||||
// Press the LEFT arrow
|
||||
chipListInstance._keydown(LEFT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.focusedItemIndex).toEqual(lastIndex - 1);
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
|
||||
it('right arrow focuses next item', () => {
|
||||
@@ -144,14 +144,14 @@ describe('MdChipList', () => {
|
||||
|
||||
// Focus the last item in the array
|
||||
firstItem.focus();
|
||||
expect(manager.focusedItemIndex).toEqual(0);
|
||||
expect(manager.activeItemIndex).toEqual(0);
|
||||
|
||||
// Press the RIGHT arrow
|
||||
chipListInstance._keydown(RIGHT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.focusedItemIndex).toEqual(1);
|
||||
expect(manager.activeItemIndex).toEqual(1);
|
||||
});
|
||||
|
||||
describe('when selectable is true', () => {
|
||||
|
||||
+11
-11
@@ -12,7 +12,7 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import {MdChip} from './chip';
|
||||
import {ListKeyManager} from '../core/a11y/list-key-manager';
|
||||
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
|
||||
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
|
||||
import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
|
||||
|
||||
@@ -55,8 +55,8 @@ export class MdChipList implements AfterContentInit {
|
||||
/** Whether or not the chip is selectable. */
|
||||
protected _selectable: boolean = true;
|
||||
|
||||
/** The ListKeyManager which handles focus. */
|
||||
_keyManager: ListKeyManager;
|
||||
/** The FocusKeyManager which handles focus. */
|
||||
_keyManager: FocusKeyManager;
|
||||
|
||||
/** The chip components contained within this chip list. */
|
||||
chips: QueryList<MdChip>;
|
||||
@@ -64,7 +64,7 @@ export class MdChipList implements AfterContentInit {
|
||||
constructor(private _elementRef: ElementRef) { }
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._keyManager = new ListKeyManager(this.chips).withFocusWrap();
|
||||
this._keyManager = new FocusKeyManager(this.chips).withWrap();
|
||||
|
||||
// Go ahead and subscribe all of the initial chips
|
||||
this._subscribeChips(this.chips);
|
||||
@@ -93,7 +93,7 @@ export class MdChipList implements AfterContentInit {
|
||||
*/
|
||||
focus() {
|
||||
// TODO: ARIA says this should focus the first `selected` chip.
|
||||
this._keyManager.focusFirstItem();
|
||||
this._keyManager.setFirstItemActive();
|
||||
}
|
||||
|
||||
/** Passes relevant key presses to our key manager. */
|
||||
@@ -113,11 +113,11 @@ export class MdChipList implements AfterContentInit {
|
||||
event.preventDefault();
|
||||
break;
|
||||
case LEFT_ARROW:
|
||||
this._keyManager.focusPreviousItem();
|
||||
this._keyManager.setPreviousItemActive();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case RIGHT_ARROW:
|
||||
this._keyManager.focusNextItem();
|
||||
this._keyManager.setNextItemActive();
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
@@ -133,7 +133,7 @@ export class MdChipList implements AfterContentInit {
|
||||
return;
|
||||
}
|
||||
|
||||
let focusedIndex = this._keyManager.focusedItemIndex;
|
||||
let focusedIndex = this._keyManager.activeItemIndex;
|
||||
|
||||
if (this._isValidIndex(focusedIndex)) {
|
||||
let focusedChip: MdChip = this.chips.toArray()[focusedIndex];
|
||||
@@ -173,7 +173,7 @@ export class MdChipList implements AfterContentInit {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(chip);
|
||||
|
||||
if (this._isValidIndex(chipIndex)) {
|
||||
this._keyManager.updateFocusedItemIndex(chipIndex);
|
||||
this._keyManager.updateActiveItemIndex(chipIndex);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -184,9 +184,9 @@ export class MdChipList implements AfterContentInit {
|
||||
if (this._isValidIndex(chipIndex)) {
|
||||
// Check whether the chip is the last item
|
||||
if (chipIndex < this.chips.length - 1) {
|
||||
this._keyManager.setFocus(chipIndex);
|
||||
this._keyManager.setActiveItem(chipIndex);
|
||||
} else if (chipIndex - 1 >= 0) {
|
||||
this._keyManager.setFocus(chipIndex - 1);
|
||||
this._keyManager.setActiveItem(chipIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Renderer
|
||||
} from '@angular/core';
|
||||
|
||||
import {Focusable} from '../core/a11y/list-key-manager';
|
||||
import {Focusable} from '../core/a11y/focus-key-manager';
|
||||
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
|
||||
|
||||
export interface MdChipEvent {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {QueryList} from '@angular/core';
|
||||
import {ListKeyManager, CanDisable} from './list-key-manager';
|
||||
|
||||
/**
|
||||
* This is the interface for highlightable items (used by the ActiveDescendantKeyManager).
|
||||
* Each item must know how to style itself as active or inactive and whether or not it is
|
||||
* currently disabled.
|
||||
*/
|
||||
export interface Highlightable extends CanDisable {
|
||||
setActiveStyles(): void;
|
||||
setInactiveStyles(): void;
|
||||
}
|
||||
|
||||
export class ActiveDescendantKeyManager extends ListKeyManager<Highlightable> {
|
||||
|
||||
constructor(items: QueryList<Highlightable>) {
|
||||
super(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sets the active item to the item at the specified index.
|
||||
* It also adds active styles to the newly active item and removes active
|
||||
* styles from the previously active item.
|
||||
*/
|
||||
setActiveItem(index: number): void {
|
||||
if (this.activeItem) {
|
||||
this.activeItem.setInactiveStyles();
|
||||
}
|
||||
super.setActiveItem(index);
|
||||
if (this.activeItem) {
|
||||
this.activeItem.setActiveStyles();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
import {QueryList} from '@angular/core';
|
||||
import {ListKeyManager, CanDisable} from './list-key-manager';
|
||||
|
||||
/**
|
||||
* This is the interface for focusable items (used by the FocusKeyManager).
|
||||
* Each item must know how to focus itself and whether or not it is currently disabled.
|
||||
*/
|
||||
export interface Focusable extends CanDisable {
|
||||
focus(): void;
|
||||
}
|
||||
|
||||
|
||||
export class FocusKeyManager extends ListKeyManager<Focusable> {
|
||||
|
||||
constructor(items: QueryList<Focusable>) {
|
||||
super(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sets the active item to the item at the specified index.
|
||||
* It also adds focuses the newly active item.
|
||||
*/
|
||||
setActiveItem(index: number): void {
|
||||
super.setActiveItem(index);
|
||||
this.activeItem.focus();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
import {QueryList} from '@angular/core';
|
||||
import {ListKeyManager} from './list-key-manager';
|
||||
import {FocusKeyManager} from './focus-key-manager';
|
||||
import {DOWN_ARROW, UP_ARROW, TAB, HOME, END} from '../keyboard/keycodes';
|
||||
import {ListKeyManager} from './list-key-manager';
|
||||
import {ActiveDescendantKeyManager} from './activedescendant-key-manager';
|
||||
|
||||
class FakeFocusable {
|
||||
disabled = false;
|
||||
focus() {}
|
||||
}
|
||||
|
||||
class FakeHighlightable {
|
||||
disabled = false;
|
||||
setActiveStyles() {}
|
||||
setInactiveStyles() {}
|
||||
}
|
||||
|
||||
class FakeQueryList<T> extends QueryList<T> {
|
||||
get length() { return this.items.length; }
|
||||
items: T[];
|
||||
@@ -23,9 +31,8 @@ export class FakeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
describe('ListKeyManager', () => {
|
||||
let keyManager: ListKeyManager;
|
||||
let itemList: FakeQueryList<FakeFocusable>;
|
||||
describe('Key managers', () => {
|
||||
let itemList: FakeQueryList<any>;
|
||||
let DOWN_ARROW_EVENT: KeyboardEvent;
|
||||
let UP_ARROW_EVENT: KeyboardEvent;
|
||||
let TAB_EVENT: KeyboardEvent;
|
||||
@@ -33,14 +40,7 @@ describe('ListKeyManager', () => {
|
||||
let END_EVENT: KeyboardEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
itemList = new FakeQueryList<FakeFocusable>();
|
||||
itemList.items = [
|
||||
new FakeFocusable(),
|
||||
new FakeFocusable(),
|
||||
new FakeFocusable()
|
||||
];
|
||||
|
||||
keyManager = new ListKeyManager(itemList);
|
||||
itemList = new FakeQueryList<any>();
|
||||
|
||||
DOWN_ARROW_EVENT = new FakeEvent(DOWN_ARROW) as KeyboardEvent;
|
||||
UP_ARROW_EVENT = new FakeEvent(UP_ARROW) as KeyboardEvent;
|
||||
@@ -48,285 +48,420 @@ describe('ListKeyManager', () => {
|
||||
HOME_EVENT = new FakeEvent(HOME) as KeyboardEvent;
|
||||
END_EVENT = new FakeEvent(END) as KeyboardEvent;
|
||||
|
||||
// first item is already focused
|
||||
keyManager.focusFirstItem();
|
||||
|
||||
spyOn(itemList.items[0], 'focus');
|
||||
spyOn(itemList.items[1], 'focus');
|
||||
spyOn(itemList.items[2], 'focus');
|
||||
});
|
||||
|
||||
describe('key events', () => {
|
||||
it('should focus subsequent items when down arrow is pressed', () => {
|
||||
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();
|
||||
describe('ListKeyManager', () => {
|
||||
let keyManager: ListKeyManager<FakeFocusable>;
|
||||
|
||||
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);
|
||||
beforeEach(() => {
|
||||
itemList.items = [
|
||||
new FakeFocusable(),
|
||||
new FakeFocusable(),
|
||||
new FakeFocusable()
|
||||
];
|
||||
|
||||
keyManager = new ListKeyManager<FakeFocusable>(itemList);
|
||||
|
||||
// first item is already focused
|
||||
keyManager.setFirstItemActive();
|
||||
|
||||
spyOn(keyManager, 'setActiveItem').and.callThrough();
|
||||
});
|
||||
|
||||
it('should focus previous items when up arrow is pressed', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
describe('Key events', () => {
|
||||
|
||||
expect(itemList.items[0].focus).not.toHaveBeenCalled();
|
||||
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
|
||||
it('should set subsequent items as active when down arrow is pressed', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, 'Expected active item to be 1 after 1 down arrow event.');
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(1);
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2);
|
||||
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, 'Expected active item to be 2 after 2 down arrow events.');
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(2);
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should set previous items as active when up arrow is pressed', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, 'Expected active item to be 1 after 1 down arrow event.');
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(1);
|
||||
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, 'Expected active item to be 0 after 1 down and 1 up arrow event.');
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
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(keyManager.activeItemIndex)
|
||||
.toBe(2, 'Expected active item to skip past disabled item on down arrow.');
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(1);
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(2);
|
||||
|
||||
// up arrow should skip past disabled item from 2 to 0
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, 'Expected active item to skip past disabled item on up arrow.');
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(0);
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(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(keyManager.activeItemIndex)
|
||||
.toBe(1, 'Expected active item to be 1 after 1 down arrow when disabled not set.');
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(1);
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2);
|
||||
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, 'Expected active item to be 2 after 2 down arrows when disabled not set.');
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
|
||||
expect(keyManager.setActiveItem).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('should not move active item past either end of the list', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected last item of the list to be active.`);
|
||||
|
||||
// this down arrow would move active item past the end of the list
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected active item to remain at the end of the list.`);
|
||||
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected first item of the list to be active.`);
|
||||
|
||||
// this up arrow would move active item past the beginning of the list
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected active item to remain at the beginning of the list.`);
|
||||
});
|
||||
|
||||
it('should not move active item to end when the last item is disabled', () => {
|
||||
itemList.items[2].disabled = true;
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected second item of the list to be active.`);
|
||||
|
||||
// this down arrow would set active item to the last item, which is disabled
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected the second item to remain active.`);
|
||||
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('should set the active item to the first item when HOME is pressed', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected last item of the list to be active.`);
|
||||
|
||||
keyManager.onKeydown(HOME_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected the HOME key to set the active item to the first item.`);
|
||||
});
|
||||
|
||||
it('should set the active item to the last item when END is pressed', () => {
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected first item of the list to be active.`);
|
||||
|
||||
keyManager.onKeydown(END_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected the END key to set the active item to the last item.`);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('should prevent the default keyboard action', () => {
|
||||
expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(false);
|
||||
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('should not prevent the default keyboard action when pressing tab', () => {
|
||||
expect(TAB_EVENT.defaultPrevented).toBe(false);
|
||||
|
||||
keyManager.onKeydown(TAB_EVENT);
|
||||
|
||||
expect(TAB_EVENT.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
expect(itemList.items[0].focus).toHaveBeenCalledTimes(1);
|
||||
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should skip disabled items using arrow keys', () => {
|
||||
itemList.items[1].disabled = true;
|
||||
describe('programmatic focus', () => {
|
||||
|
||||
// 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);
|
||||
it('should setActiveItem()', () => {
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected first item of the list to be active.`);
|
||||
|
||||
keyManager.setActiveItem(1);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected activeItemIndex to be updated when setActiveItem() was called.`);
|
||||
});
|
||||
|
||||
it('should expose the active item correctly', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
expect(keyManager.activeItemIndex).toBe(1, 'Expected active item to be the second option.');
|
||||
expect(keyManager.activeItem)
|
||||
.toBe(itemList.items[1], 'Expected the active item to match the second option.');
|
||||
|
||||
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex).toBe(2, 'Expected active item to be the third option.');
|
||||
expect(keyManager.activeItem)
|
||||
.toBe(itemList.items[2], 'Expected the active item ID to match the third option.');
|
||||
});
|
||||
|
||||
it('should setFirstItemActive()', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected last item of the list to be active.`);
|
||||
|
||||
keyManager.setFirstItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected setFirstItemActive() to set the active item to the first item.`);
|
||||
});
|
||||
|
||||
it('should set the active item to the second item if the first one is disabled', () => {
|
||||
itemList.items[0].disabled = true;
|
||||
|
||||
keyManager.setFirstItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected the second item to be active if the first was disabled.`);
|
||||
});
|
||||
|
||||
it('should setLastItemActive()', () => {
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected first item of the list to be active.`);
|
||||
|
||||
keyManager.setLastItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected setLastItemActive() to set the active item to the last item.`);
|
||||
});
|
||||
|
||||
it('should set the active item to the second to last item if the last is disabled', () => {
|
||||
itemList.items[2].disabled = true;
|
||||
|
||||
keyManager.setLastItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected the second to last item to be active if the last was disabled.`);
|
||||
});
|
||||
|
||||
it('should setNextItemActive()', () => {
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected first item of the list to be active.`);
|
||||
|
||||
keyManager.setNextItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected setNextItemActive() to set the active item to the next item.`);
|
||||
});
|
||||
|
||||
it('should set the active item to the next enabled item if next is disabled', () => {
|
||||
itemList.items[1].disabled = true;
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected first item of the list to be active.`);
|
||||
|
||||
keyManager.setNextItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected setNextItemActive() to only set enabled items as active.`);
|
||||
});
|
||||
|
||||
it('should setPreviousItemActive()', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected second item of the list to be active.`);
|
||||
|
||||
keyManager.setPreviousItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected setPreviousItemActive() to set the active item to the previous.`);
|
||||
});
|
||||
|
||||
it('should skip disabled items when setPreviousItemActive() is called', () => {
|
||||
itemList.items[1].disabled = true;
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(2, `Expected third item of the list to be active.`);
|
||||
|
||||
keyManager.setPreviousItemActive();
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected setPreviousItemActive() to skip the disabled item.`);
|
||||
});
|
||||
|
||||
// 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;
|
||||
describe('wrap mode', () => {
|
||||
|
||||
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();
|
||||
it('should return itself to allow chaining', () => {
|
||||
expect(keyManager.withWrap())
|
||||
.toEqual(keyManager, `Expected withWrap() to return an instance of ListKeyManager.`);
|
||||
});
|
||||
|
||||
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 wrap focus when arrow keying past items while in wrap mode', () => {
|
||||
keyManager.withWrap();
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
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.`);
|
||||
expect(keyManager.activeItemIndex).toBe(2, 'Expected last item to be active.');
|
||||
|
||||
// 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);
|
||||
// this down arrow moves down past the end of the list
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex).toBe(0, 'Expected active item to wrap to beginning.');
|
||||
|
||||
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.`);
|
||||
// this up arrow moves up past the beginning of the list
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(keyManager.activeItemIndex).toBe(2, 'Expected active item to wrap to end.');
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
it('should prevent the default keyboard action', () => {
|
||||
expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(false);
|
||||
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('should not prevent the default keyboard action when pressing tab', () => {
|
||||
expect(TAB_EVENT.defaultPrevented).toBe(false);
|
||||
|
||||
keyManager.onKeydown(TAB_EVENT);
|
||||
|
||||
expect(TAB_EVENT.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('programmatic focus', () => {
|
||||
describe('FocusKeyManager', () => {
|
||||
let keyManager: FocusKeyManager;
|
||||
|
||||
it('should setFocus()', () => {
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(0, `Expected focus to be on the first item of the list.`);
|
||||
beforeEach(() => {
|
||||
itemList.items = [
|
||||
new FakeFocusable(),
|
||||
new FakeFocusable(),
|
||||
new FakeFocusable()
|
||||
];
|
||||
|
||||
keyManager.setFocus(1);
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(1, `Expected focusedItemIndex to be updated when setFocus() was called.`);
|
||||
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
|
||||
keyManager = new FocusKeyManager(itemList);
|
||||
|
||||
// first item is already focused
|
||||
keyManager.setFirstItemActive();
|
||||
|
||||
spyOn(itemList.items[0], 'focus');
|
||||
spyOn(itemList.items[1], 'focus');
|
||||
spyOn(itemList.items[2], 'focus');
|
||||
});
|
||||
|
||||
it('should allow setting the focused item without calling focus', () => {
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(0, `Expected focus to be on the first item of the list.`);
|
||||
it('should focus subsequent items when down arrow is pressed', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
keyManager.updateFocusedItemIndex(1);
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(1, `Expected focusedItemIndex to be updated after calling updateFocusedItemIndex().`);
|
||||
expect(itemList.items[1].focus).not.toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(itemList.items[0].focus).not.toHaveBeenCalled();
|
||||
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
|
||||
expect(itemList.items[2].focus).not.toHaveBeenCalled();
|
||||
|
||||
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.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);
|
||||
});
|
||||
|
||||
keyManager.focusFirstItem();
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(0, `Expected focusFirstItem() to move the focus back to the first item.`);
|
||||
});
|
||||
it('should focus previous items when up arrow is pressed', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
it('should focus the second item if the first one is disabled', () => {
|
||||
itemList.items[0].disabled = true;
|
||||
expect(itemList.items[0].focus).not.toHaveBeenCalled();
|
||||
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
|
||||
|
||||
keyManager.focusFirstItem();
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(1, `Expected the second item to be focused if the first was disabled.`);
|
||||
});
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
|
||||
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.`);
|
||||
expect(itemList.items[0].focus).toHaveBeenCalledTimes(1);
|
||||
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
keyManager.focusLastItem();
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(2, `Expected focusLastItem() to move the focus to the last item in the list.`);
|
||||
});
|
||||
it('should allow setting the focused item without calling focus', () => {
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(0, `Expected first item of the list to be active.`);
|
||||
|
||||
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.`);
|
||||
});
|
||||
keyManager.updateActiveItemIndex(1);
|
||||
expect(keyManager.activeItemIndex)
|
||||
.toBe(1, `Expected activeItemIndex to update after calling updateActiveItemIndex().`);
|
||||
expect(itemList.items[1].focus).not.toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('wrap mode', () => {
|
||||
describe('ActiveDescendantKeyManager', () => {
|
||||
let keyManager: ActiveDescendantKeyManager;
|
||||
|
||||
it('should return itself to allow chaining', () => {
|
||||
expect(keyManager.withFocusWrap())
|
||||
.toEqual(keyManager, `Expected withFocusWrap() to return an instance of ListKeyManager`);
|
||||
beforeEach(() => {
|
||||
itemList.items = [
|
||||
new FakeHighlightable(),
|
||||
new FakeHighlightable(),
|
||||
new FakeHighlightable()
|
||||
];
|
||||
|
||||
keyManager = new ActiveDescendantKeyManager(itemList);
|
||||
|
||||
// first item is already focused
|
||||
keyManager.setFirstItemActive();
|
||||
|
||||
spyOn(itemList.items[0], 'setActiveStyles');
|
||||
spyOn(itemList.items[1], 'setActiveStyles');
|
||||
spyOn(itemList.items[2], 'setActiveStyles');
|
||||
|
||||
spyOn(itemList.items[0], 'setInactiveStyles');
|
||||
spyOn(itemList.items[1], 'setInactiveStyles');
|
||||
spyOn(itemList.items[2], 'setInactiveStyles');
|
||||
});
|
||||
|
||||
it('should wrap focus when arrow keying past items while in wrap mode', () => {
|
||||
keyManager.withFocusWrap();
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
it('should set subsequent items as active with the DOWN arrow', () => {
|
||||
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);
|
||||
expect(itemList.items[1].setActiveStyles).toHaveBeenCalled();
|
||||
expect(itemList.items[2].setActiveStyles).not.toHaveBeenCalled();
|
||||
|
||||
// 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);
|
||||
expect(itemList.items[2].setActiveStyles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set previous items as active with the UP arrow', () => {
|
||||
keyManager.setLastItemActive();
|
||||
|
||||
// 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(itemList.items[1].setActiveStyles).toHaveBeenCalled();
|
||||
expect(itemList.items[0].setActiveStyles).not.toHaveBeenCalled();
|
||||
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(itemList.items[0].setActiveStyles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set inactive styles on previously active items', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
expect(itemList.items[0].setInactiveStyles).toHaveBeenCalled();
|
||||
|
||||
keyManager.onKeydown(UP_ARROW_EVENT);
|
||||
expect(itemList.items[1].setInactiveStyles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -4,63 +4,64 @@ import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
/**
|
||||
* This is the interface for focusable items (used by the ListKeyManager).
|
||||
* Each item must know how to focus itself and whether or not it is currently disabled.
|
||||
* This interface is for items that can be disabled. The type passed into
|
||||
* ListKeyManager must extend this interface.
|
||||
*/
|
||||
export interface Focusable {
|
||||
focus(): void;
|
||||
export interface CanDisable {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class manages keyboard events for selectable lists. If you pass it a query list
|
||||
* of focusable items, it will focus the correct item when arrow events occur.
|
||||
* of items, it will set the active item correctly when arrow events occur.
|
||||
*/
|
||||
export class ListKeyManager {
|
||||
private _focusedItemIndex: number;
|
||||
export class ListKeyManager<T extends CanDisable> {
|
||||
private _activeItemIndex: number;
|
||||
private _activeItem: T;
|
||||
private _tabOut: Subject<any> = new Subject();
|
||||
private _wrap: boolean = false;
|
||||
|
||||
constructor(private _items: QueryList<Focusable>) {}
|
||||
constructor(private _items: QueryList<T>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on focus wrapping mode, which ensures that the focus will wrap to
|
||||
* Turns on wrapping mode, which ensures that the active item will wrap to
|
||||
* the other end of list when there are no more items in the given direction.
|
||||
*
|
||||
* @returns The ListKeyManager that the method was called on.
|
||||
*/
|
||||
withFocusWrap(): this {
|
||||
withWrap(): this {
|
||||
this._wrap = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focus of the list to the item at the index specified.
|
||||
* Sets the active item to the item at the index specified.
|
||||
*
|
||||
* @param index The index of the item to be focused.
|
||||
* @param index The index of the item to be set as active.
|
||||
*/
|
||||
setFocus(index: number): void {
|
||||
this._focusedItemIndex = index;
|
||||
this._items.toArray()[index].focus();
|
||||
setActiveItem(index: number): void {
|
||||
this._activeItemIndex = index;
|
||||
this._activeItem = this._items.toArray()[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focus depending on the key event passed in.
|
||||
* @param event Keyboard event to be used for determining which element to focus.
|
||||
* Sets the active item depending on the key event passed in.
|
||||
* @param event Keyboard event to be used for determining which element should be active.
|
||||
*/
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
switch (event.keyCode) {
|
||||
case DOWN_ARROW:
|
||||
this.focusNextItem();
|
||||
this.setNextItemActive();
|
||||
break;
|
||||
case UP_ARROW:
|
||||
this.focusPreviousItem();
|
||||
this.setPreviousItemActive();
|
||||
break;
|
||||
case HOME:
|
||||
this.focusFirstItem();
|
||||
this.setFirstItemActive();
|
||||
break;
|
||||
case END:
|
||||
this.focusLastItem();
|
||||
this.setLastItemActive();
|
||||
break;
|
||||
case TAB:
|
||||
// Note that we shouldn't prevent the default action on tab.
|
||||
@@ -73,37 +74,42 @@ export class ListKeyManager {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/** Focuses the first enabled item in the list. */
|
||||
focusFirstItem(): void {
|
||||
this._setFocusByIndex(0, 1);
|
||||
/** Returns the index of the currently active item. */
|
||||
get activeItemIndex(): number {
|
||||
return this._activeItemIndex;
|
||||
}
|
||||
|
||||
/** Focuses the last enabled item in the list. */
|
||||
focusLastItem(): void {
|
||||
this._setFocusByIndex(this._items.length - 1, -1);
|
||||
/** Returns the currently active item. */
|
||||
get activeItem(): T {
|
||||
return this._activeItem;
|
||||
}
|
||||
|
||||
/** Focuses the next enabled item in the list. */
|
||||
focusNextItem(): void {
|
||||
this._setFocusByDelta(1);
|
||||
/** Sets the active item to the first enabled item in the list. */
|
||||
setFirstItemActive(): void {
|
||||
this._setActiveItemByIndex(0, 1);
|
||||
}
|
||||
|
||||
/** Focuses a previous enabled item in the list. */
|
||||
focusPreviousItem(): void {
|
||||
this._setFocusByDelta(-1);
|
||||
/** Sets the active item to the last enabled item in the list. */
|
||||
setLastItemActive(): void {
|
||||
this._setActiveItemByIndex(this._items.length - 1, -1);
|
||||
}
|
||||
|
||||
/** Returns the index of the currently focused item. */
|
||||
get focusedItemIndex(): number {
|
||||
return this._focusedItemIndex;
|
||||
/** Sets the active item to the next enabled item in the list. */
|
||||
setNextItemActive(): void {
|
||||
this._setActiveItemByDelta(1);
|
||||
}
|
||||
|
||||
/** Sets the active item to a previous enabled item in the list. */
|
||||
setPreviousItemActive(): void {
|
||||
this._setActiveItemByDelta(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows setting of the focusedItemIndex without focusing the item.
|
||||
* @param index The new focusedItemIndex.
|
||||
* Allows setting of the activeItemIndex without any other effects.
|
||||
* @param index The new activeItemIndex.
|
||||
*/
|
||||
updateFocusedItemIndex(index: number) {
|
||||
this._focusedItemIndex = index;
|
||||
updateActiveItemIndex(index: number) {
|
||||
this._activeItemIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,56 +121,56 @@ export class ListKeyManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sets focus to the correct item, given a list of items and the delta
|
||||
* between the currently focused item and the new item to be focused. It will calculate
|
||||
* the proper focus differently depending on whether wrap mode is turned on.
|
||||
* This method sets the active item, given a list of items and the delta between the
|
||||
* currently active item and the new active item. It will calculate differently
|
||||
* depending on whether wrap mode is turned on.
|
||||
*/
|
||||
private _setFocusByDelta(delta: number, items = this._items.toArray()): void {
|
||||
this._wrap ? this._setWrapModeFocus(delta, items)
|
||||
: this._setDefaultModeFocus(delta, items);
|
||||
private _setActiveItemByDelta(delta: number, items = this._items.toArray()): void {
|
||||
this._wrap ? this._setActiveInWrapMode(delta, items)
|
||||
: this._setActiveInDefaultMode(delta, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focus properly given "wrap" mode. In other words, it will continue to move
|
||||
* Sets the active item properly given "wrap" mode. In other words, 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 list.
|
||||
*/
|
||||
private _setWrapModeFocus(delta: number, items: Focusable[]): void {
|
||||
// when focus would leave menu, wrap to beginning or end
|
||||
this._focusedItemIndex =
|
||||
(this._focusedItemIndex + delta + items.length) % items.length;
|
||||
private _setActiveInWrapMode(delta: number, items: T[]): void {
|
||||
// when active item would leave menu, wrap to beginning or end
|
||||
this._activeItemIndex =
|
||||
(this._activeItemIndex + delta + items.length) % items.length;
|
||||
|
||||
// skip all disabled menu items recursively until an active one is reached
|
||||
if (items[this._focusedItemIndex].disabled) {
|
||||
this._setWrapModeFocus(delta, items);
|
||||
// skip all disabled menu items recursively until an enabled one is reached
|
||||
if (items[this._activeItemIndex].disabled) {
|
||||
this._setActiveInWrapMode(delta, items);
|
||||
} else {
|
||||
items[this._focusedItemIndex].focus();
|
||||
this.setActiveItem(this._activeItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focus properly given the default mode. In other words, it will
|
||||
* Sets the active item properly given the default mode. In other words, it will
|
||||
* continue to move down the list until it finds an item that is not disabled. If
|
||||
* it encounters either end of the list, it will stop and not wrap.
|
||||
*/
|
||||
private _setDefaultModeFocus(delta: number, items: Focusable[]): void {
|
||||
this._setFocusByIndex(this._focusedItemIndex + delta, delta, items);
|
||||
private _setActiveInDefaultMode(delta: number, items: T[]): void {
|
||||
this._setActiveItemByIndex(this._activeItemIndex + delta, delta, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focus to the first enabled item starting at the index specified. If the
|
||||
* Sets the active item to the first enabled item starting at the index specified. If the
|
||||
* item is disabled, it will move in the fallbackDelta direction until it either
|
||||
* finds an enabled item or encounters the end of the list.
|
||||
*/
|
||||
private _setFocusByIndex(index: number, fallbackDelta: number,
|
||||
items = this._items.toArray()): void {
|
||||
private _setActiveItemByIndex(index: number, fallbackDelta: number,
|
||||
items = this._items.toArray()): void {
|
||||
if (!items[index]) { return; }
|
||||
while (items[index].disabled) {
|
||||
index += fallbackDelta;
|
||||
if (!items[index]) { return; }
|
||||
}
|
||||
|
||||
this.setFocus(index);
|
||||
this.setActiveItem(index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import {MenuPositionX, MenuPositionY} from './menu-positions';
|
||||
import {MdMenuInvalidPositionX, MdMenuInvalidPositionY} from './menu-errors';
|
||||
import {MdMenuItem} from './menu-item';
|
||||
import {ListKeyManager} from '../core/a11y/list-key-manager';
|
||||
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
|
||||
import {MdMenuPanel} from './menu-panel';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {transformMenu, fadeInItems} from './menu-animations';
|
||||
@@ -36,7 +36,7 @@ import {transformMenu, fadeInItems} from './menu-animations';
|
||||
exportAs: 'mdMenu'
|
||||
})
|
||||
export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
|
||||
private _keyManager: ListKeyManager;
|
||||
private _keyManager: FocusKeyManager;
|
||||
|
||||
/** Subscription to tab events on the menu panel */
|
||||
private _tabSubscription: Subscription;
|
||||
@@ -61,7 +61,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._keyManager = new ListKeyManager(this.items).withFocusWrap();
|
||||
this._keyManager = new FocusKeyManager(this.items).withWrap();
|
||||
this._tabSubscription = this._keyManager.tabOut.subscribe(() => {
|
||||
this._emitCloseEvent();
|
||||
});
|
||||
@@ -94,7 +94,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
|
||||
* to focus the first item when the menu is opened by the ENTER key.
|
||||
*/
|
||||
focusFirstItem() {
|
||||
this._keyManager.focusFirstItem();
|
||||
this._keyManager.setFirstItemActive();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Component, ElementRef, Input, HostBinding, Renderer} from '@angular/core';
|
||||
import {Focusable} from '../core/a11y/list-key-manager';
|
||||
import {Focusable} from '../core/a11y/focus-key-manager';
|
||||
|
||||
/**
|
||||
* This directive is intended to be used inside an md-menu tag.
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('MdSelect', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(fixture.componentInstance.select._keyManager.focusedItemIndex).toEqual(0);
|
||||
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(0);
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -232,7 +232,7 @@ describe('MdSelect', () => {
|
||||
// must wait for animation to finish
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.select._keyManager.focusedItemIndex).toEqual(1);
|
||||
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(1);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import {MdOption} from '../core/option/option';
|
||||
import {ENTER, SPACE} from '../core/keyboard/keycodes';
|
||||
import {ListKeyManager} from '../core/a11y/list-key-manager';
|
||||
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
|
||||
import {Dir} from '../core/rtl/dir';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {transformPlaceholder, transformPanel, fadeInContent} from './select-animations';
|
||||
@@ -138,7 +138,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
_selectedValueWidth: number;
|
||||
|
||||
/** Manages keyboard events for options in the panel. */
|
||||
_keyManager: ListKeyManager;
|
||||
_keyManager: FocusKeyManager;
|
||||
|
||||
/** View -> model callback called when value changes */
|
||||
_onChange = (value: any) => {};
|
||||
@@ -439,7 +439,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
|
||||
/** Sets up a key manager to listen to keyboard events on the overlay panel. */
|
||||
private _initKeyManager() {
|
||||
this._keyManager = new ListKeyManager(this.options);
|
||||
this._keyManager = new FocusKeyManager(this.options);
|
||||
this._tabSubscription = this._keyManager.tabOut.subscribe(() => {
|
||||
this.close();
|
||||
});
|
||||
@@ -516,9 +516,9 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
*/
|
||||
private _focusCorrectOption(): void {
|
||||
if (this.selected) {
|
||||
this._keyManager.setFocus(this._getOptionIndex(this.selected));
|
||||
this._keyManager.setActiveItem(this._getOptionIndex(this.selected));
|
||||
} else {
|
||||
this._keyManager.focusFirstItem();
|
||||
this._keyManager.setFirstItemActive();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user