UNPKG

ipsos-components

Version:

Material Design components for Angular

685 lines (515 loc) 27.2 kB
import {DOWN_ARROW, TAB, UP_ARROW} from '@angular/cdk/keycodes'; import {take} from 'rxjs/operators/take'; import {QueryList} from '@angular/core'; import {fakeAsync, tick} from '@angular/core/testing'; import {createKeyboardEvent} from '../testing/event-objects'; import {ActiveDescendantKeyManager} from './activedescendant-key-manager'; import {FocusKeyManager} from './focus-key-manager'; import {ListKeyManager} from './list-key-manager'; class FakeFocusable { constructor(private _label = '') { } disabled = false; focus() {} getLabel() { return this._label; } } class FakeHighlightable { disabled = false; setActiveStyles() {} setInactiveStyles() {} } class FakeQueryList<T> extends QueryList<T> { items: T[]; get length() { return this.items.length; } get first() { return this.items[0]; } toArray() { return this.items; } some() { return this.items.some.apply(this.items, arguments); } } describe('Key managers', () => { let itemList: FakeQueryList<any>; let fakeKeyEvents: { downArrow: KeyboardEvent, upArrow: KeyboardEvent, tab: KeyboardEvent, unsupported: KeyboardEvent }; beforeEach(() => { itemList = new FakeQueryList<any>(); fakeKeyEvents = { downArrow: createKeyboardEvent('keydown', DOWN_ARROW), upArrow: createKeyboardEvent('keydown', UP_ARROW), tab: createKeyboardEvent('keydown', TAB), unsupported: createKeyboardEvent('keydown', 192) // corresponds to the tilde character (~) }; }); describe('ListKeyManager', () => { let keyManager: ListKeyManager<FakeFocusable>; beforeEach(() => { itemList.items = [ new FakeFocusable('one'), new FakeFocusable('two'), new FakeFocusable('three') ]; keyManager = new ListKeyManager<FakeFocusable>(itemList); // first item is already focused keyManager.setFirstItemActive(); spyOn(keyManager, 'setActiveItem').and.callThrough(); }); describe('Key events', () => { it('should set subsequent items as active when down arrow is pressed', () => { keyManager.onKeydown(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); 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 first item active when down arrow pressed if no active item', () => { keyManager.setActiveItem(-1); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(0, 'Expected active item to be 0 after down key if active item was null.'); expect(keyManager.setActiveItem).toHaveBeenCalledWith(0); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(1); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2); }); it('should set previous items as active when up arrow is pressed', () => { keyManager.onKeydown(fakeKeyEvents.downArrow); 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(fakeKeyEvents.upArrow); 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 do nothing when up arrow is pressed if no active item and not wrap', () => { keyManager.setActiveItem(-1); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(keyManager.activeItemIndex) .toBe(-1, 'Expected nothing to happen if up arrow occurs and no active item.'); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(1); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2); }); 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(fakeKeyEvents.downArrow); 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(fakeKeyEvents.upArrow); 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(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); keyManager.onKeydown(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(2, `Expected active item to remain at the end of the list.`); keyManager.onKeydown(fakeKeyEvents.upArrow); keyManager.onKeydown(fakeKeyEvents.upArrow); 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(fakeKeyEvents.upArrow); 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(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(1, `Expected the second item to remain active.`); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2); }); it('should emit tabOut when the tab key is pressed', () => { let spy = jasmine.createSpy('tabOut spy'); keyManager.tabOut.pipe(take(1)).subscribe(spy); keyManager.onKeydown(fakeKeyEvents.tab); expect(spy).toHaveBeenCalled(); }); it('should prevent the default keyboard action when pressing the arrow keys', () => { expect(fakeKeyEvents.downArrow.defaultPrevented).toBe(false); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(fakeKeyEvents.downArrow.defaultPrevented).toBe(true); expect(fakeKeyEvents.upArrow.defaultPrevented).toBe(false); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(fakeKeyEvents.upArrow.defaultPrevented).toBe(true); }); it('should not prevent the default keyboard action when pressing tab', () => { expect(fakeKeyEvents.tab.defaultPrevented).toBe(false); keyManager.onKeydown(fakeKeyEvents.tab); expect(fakeKeyEvents.tab.defaultPrevented).toBe(false); }); it('should not do anything for unsupported key presses', () => { keyManager.setActiveItem(1); expect(keyManager.activeItemIndex).toBe(1); expect(fakeKeyEvents.unsupported.defaultPrevented).toBe(false); keyManager.onKeydown(fakeKeyEvents.unsupported); expect(keyManager.activeItemIndex).toBe(1); expect(fakeKeyEvents.unsupported.defaultPrevented).toBe(false); }); it('should activate the first item when pressing down on a clean key manager', () => { keyManager = new ListKeyManager<FakeFocusable>(itemList); expect(keyManager.activeItemIndex).toBe(-1, 'Expected active index to default to -1.'); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex).toBe(0, 'Expected first item to become active.'); }); it('should emit an event whenever the active item changes', () => { const spy = jasmine.createSpy('change spy'); const subscription = keyManager.change.subscribe(spy); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(spy).toHaveBeenCalledTimes(1); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(spy).toHaveBeenCalledTimes(2); subscription.unsubscribe(); }); it('should not emit an event if the item did not change', () => { const spy = jasmine.createSpy('change spy'); const subscription = keyManager.change.subscribe(spy); keyManager.setActiveItem(2); keyManager.setActiveItem(2); expect(spy).toHaveBeenCalledTimes(1); subscription.unsubscribe(); }); }); describe('programmatic focus', () => { 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(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); keyManager.onKeydown(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); keyManager.onKeydown(fakeKeyEvents.downArrow); 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.`); }); }); describe('wrap mode', () => { it('should return itself to allow chaining', () => { expect(keyManager.withWrap()) .toEqual(keyManager, `Expected withWrap() to return an instance of ListKeyManager.`); }); it('should wrap focus when arrow keying past items while in wrap mode', () => { keyManager.withWrap(); keyManager.onKeydown(fakeKeyEvents.downArrow); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex).toBe(2, 'Expected last item to be active.'); // this down arrow moves down past the end of the list keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex).toBe(0, 'Expected active item to wrap to beginning.'); // this up arrow moves up past the beginning of the list keyManager.onKeydown(fakeKeyEvents.upArrow); expect(keyManager.activeItemIndex).toBe(2, 'Expected active item to wrap to end.'); }); it('should set last item active when up arrow is pressed if no active item', () => { keyManager.withWrap(); keyManager.setActiveItem(-1); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(keyManager.activeItemIndex) .toBe(2, 'Expected last item to be active on up arrow if no active item.'); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); expect(keyManager.setActiveItem).toHaveBeenCalledWith(2); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(0, 'Expected active item to be 0 after wrapping back to beginning.'); expect(keyManager.setActiveItem).toHaveBeenCalledWith(0); }); }); describe('typeahead mode', () => { const debounceInterval = 300; beforeEach(() => { keyManager.withTypeAhead(debounceInterval); keyManager.setActiveItem(-1); }); it('should throw if the items do not implement the getLabel method', () => { const invalidQueryList = new FakeQueryList(); invalidQueryList.items = [{ disabled: false }]; const invalidManager = new ListKeyManager(invalidQueryList); expect(() => invalidManager.withTypeAhead()).toThrowError(/must implement/); }); it('should debounce the input key presses', fakeAsync(() => { keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o')); // types "o" keyManager.onKeydown(createKeyboardEvent('keydown', 78, undefined, 'n')); // types "n" keyManager.onKeydown(createKeyboardEvent('keydown', 69, undefined, 'e')); // types "e" expect(keyManager.activeItem).not.toBe(itemList.items[0]); tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[0]); })); it('should focus the first item that starts with a letter', fakeAsync(() => { keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t" tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[1]); })); it('should focus the first item that starts with sequence of letters', fakeAsync(() => { keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t" keyManager.onKeydown(createKeyboardEvent('keydown', 72, undefined, 'h')); // types "h" tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[2]); })); it('should cancel any pending timers if a navigation key is pressed', fakeAsync(() => { keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t" keyManager.onKeydown(createKeyboardEvent('keydown', 72, undefined, 'h')); // types "h" keyManager.onKeydown(fakeKeyEvents.downArrow); tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[0]); })); it('should handle non-English input', fakeAsync(() => { itemList.items = [ new FakeFocusable('едно'), new FakeFocusable('две'), new FakeFocusable('три') ]; const keyboardEvent = createKeyboardEvent('keydown', 68, undefined, 'д'); keyManager.onKeydown(keyboardEvent); // types "д" tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[1]); })); it('should handle non-letter characters', fakeAsync(() => { itemList.items = [ new FakeFocusable('[]'), new FakeFocusable('321'), new FakeFocusable('`!?') ]; keyManager.onKeydown(createKeyboardEvent('keydown', 192, undefined, '`')); // types "`" tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[2]); keyManager.onKeydown(createKeyboardEvent('keydown', 51, undefined, '3')); // types "3" tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[1]); keyManager.onKeydown(createKeyboardEvent('keydown', 219, undefined, '[')); // types "[" tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[0]); })); it('should not focus disabled items', fakeAsync(() => { expect(keyManager.activeItem).toBeFalsy(); itemList.items[0].disabled = true; keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o')); // types "o" tick(debounceInterval); expect(keyManager.activeItem).toBeFalsy(); })); it('should start looking for matches after the active item', fakeAsync(() => { itemList.items = [ new FakeFocusable('Bilbo'), new FakeFocusable('Frodo'), new FakeFocusable('Pippin'), new FakeFocusable('Boromir'), new FakeFocusable('Aragorn') ]; keyManager.setActiveItem(1); keyManager.onKeydown(createKeyboardEvent('keydown', 66, undefined, 'b')); tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[3]); })); it('should wrap back around if there were no matches after the active item', fakeAsync(() => { itemList.items = [ new FakeFocusable('Bilbo'), new FakeFocusable('Frodo'), new FakeFocusable('Pippin'), new FakeFocusable('Boromir'), new FakeFocusable('Aragorn') ]; keyManager.setActiveItem(3); keyManager.onKeydown(createKeyboardEvent('keydown', 66, undefined, 'b')); tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[0]); })); it('should wrap back around if the last item is active', fakeAsync(() => { keyManager.setActiveItem(2); keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o')); tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[0]); })); it('should be able to select the first item', fakeAsync(() => { keyManager.setActiveItem(-1); keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o')); tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[0]); })); it('should not do anything if there is no match', fakeAsync(() => { keyManager.setActiveItem(1); keyManager.onKeydown(createKeyboardEvent('keydown', 87, undefined, 'w')); tick(debounceInterval); expect(keyManager.activeItem).toBe(itemList.items[1]); })); }); }); describe('FocusKeyManager', () => { let keyManager: FocusKeyManager<FakeFocusable>; beforeEach(() => { itemList.items = [new FakeFocusable(), new FakeFocusable(), new FakeFocusable()]; keyManager = new FocusKeyManager<FakeFocusable>(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 focus subsequent items when down arrow is pressed', () => { keyManager.onKeydown(fakeKeyEvents.downArrow); expect(itemList.items[0].focus).not.toHaveBeenCalled(); expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); expect(itemList.items[2].focus).not.toHaveBeenCalled(); keyManager.onKeydown(fakeKeyEvents.downArrow); 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(fakeKeyEvents.downArrow); expect(itemList.items[0].focus).not.toHaveBeenCalled(); expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); }); it('should allow setting the focused item without calling focus', () => { expect(keyManager.activeItemIndex) .toBe(0, `Expected first item of the list to be active.`); keyManager.updateActiveItemIndex(1); expect(keyManager.activeItemIndex) .toBe(1, `Expected activeItemIndex to update after calling updateActiveItemIndex().`); expect(itemList.items[1].focus).not.toHaveBeenCalledTimes(1); }); }); describe('ActiveDescendantKeyManager', () => { let keyManager: ActiveDescendantKeyManager<FakeHighlightable>; beforeEach(() => { itemList.items = [new FakeHighlightable(), new FakeHighlightable(), new FakeHighlightable()]; keyManager = new ActiveDescendantKeyManager<FakeHighlightable>(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 set subsequent items as active with the DOWN arrow', () => { keyManager.onKeydown(fakeKeyEvents.downArrow); expect(itemList.items[1].setActiveStyles).toHaveBeenCalled(); expect(itemList.items[2].setActiveStyles).not.toHaveBeenCalled(); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(itemList.items[2].setActiveStyles).toHaveBeenCalled(); }); it('should set previous items as active with the UP arrow', () => { keyManager.setLastItemActive(); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(itemList.items[1].setActiveStyles).toHaveBeenCalled(); expect(itemList.items[0].setActiveStyles).not.toHaveBeenCalled(); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(itemList.items[0].setActiveStyles).toHaveBeenCalled(); }); it('should set inactive styles on previously active items', () => { keyManager.onKeydown(fakeKeyEvents.downArrow); expect(itemList.items[0].setInactiveStyles).toHaveBeenCalled(); keyManager.onKeydown(fakeKeyEvents.upArrow); expect(itemList.items[1].setInactiveStyles).toHaveBeenCalled(); }); }); });