From 86229f4a99dd4f6a772f75ab97a928acfb91a674 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 14 May 2017 11:57:20 +0200 Subject: [PATCH] fix(list-key-manager): remove handling for home and end keys * Removes the handling for the home and end keys from the ListKeyManager since their usage isn't as generic as the arrow keys. * Adds the home and end key handling to the select specifically. * Reworks the Autocomplete, ListKeyManager and ChipList tests to use the `createKeyboardEvent`, instead of making their own fake keyboard events. * Adds a `target` parameter to the `createKeyboardEvent` function, allowing us to define the event target. Fixes #3496. --- src/lib/autocomplete/autocomplete.spec.ts | 54 +----- src/lib/chips/chip-list.spec.ts | 17 +- src/lib/core/a11y/list-key-manager.spec.ts | 191 +++++++++------------ src/lib/core/a11y/list-key-manager.ts | 9 +- src/lib/core/testing/event-objects.ts | 7 +- src/lib/select/select.html | 2 +- src/lib/select/select.spec.ts | 31 +++- src/lib/select/select.ts | 23 ++- 8 files changed, 150 insertions(+), 184 deletions(-) diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 93b7b13aa40b..015f80eb166a 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -16,13 +16,14 @@ import {MdInputModule} from '../input/index'; import {Dir, LayoutDirection} from '../core/rtl/dir'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {Subscription} from 'rxjs/Subscription'; -import {ENTER, DOWN_ARROW, SPACE, UP_ARROW, HOME, END} from '../core/keyboard/keycodes'; +import {ENTER, DOWN_ARROW, SPACE, UP_ARROW} from '../core/keyboard/keycodes'; import {MdOption} from '../core/option/option'; import {MdAutocomplete} from './autocomplete'; import {MdInputContainer} from '../input/input-container'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import {dispatchFakeEvent} from '../core/testing/dispatch-events'; +import {createKeyboardEvent} from '../core/testing/event-objects'; import {typeInElement} from '../core/testing/type-in-element'; import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; @@ -535,8 +536,8 @@ describe('MdAutocomplete', () => { fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent; - ENTER_EVENT = new MockKeyboardEvent(ENTER) as KeyboardEvent; + DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); + ENTER_EVENT = createKeyboardEvent('keydown', ENTER); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); @@ -594,7 +595,7 @@ describe('MdAutocomplete', () => { const optionEls = overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent; + const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW); fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); tick(); fixture.detectChanges(); @@ -668,7 +669,7 @@ describe('MdAutocomplete', () => { typeInElement('New', input); fixture.detectChanges(); - const SPACE_EVENT = new MockKeyboardEvent(SPACE) as KeyboardEvent; + const SPACE_EVENT = createKeyboardEvent('keydown', SPACE); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.whenStable().then(() => { @@ -748,7 +749,7 @@ describe('MdAutocomplete', () => { const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel'); - const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent; + const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW); fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); tick(); fixture.detectChanges(); @@ -757,36 +758,6 @@ describe('MdAutocomplete', () => { expect(scrollContainer.scrollTop).toEqual(272, `Expected panel to reveal last option.`); })); - it('should scroll the active option into view when pressing END', fakeAsync(() => { - tick(); - const scrollContainer = - document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel'); - - const END_EVENT = new MockKeyboardEvent(END) as KeyboardEvent; - fixture.componentInstance.trigger._handleKeydown(END_EVENT); - tick(); - fixture.detectChanges(); - - // Expect option bottom minus the panel height (528 - 256 = 272) - expect(scrollContainer.scrollTop).toEqual(272, 'Expected panel to reveal the last option.'); - })); - - it('should scroll the active option into view when pressing HOME', fakeAsync(() => { - tick(); - const scrollContainer = - document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel'); - - scrollContainer.scrollTop = 100; - fixture.detectChanges(); - - const HOME_EVENT = new MockKeyboardEvent(HOME) as KeyboardEvent; - fixture.componentInstance.trigger._handleKeydown(HOME_EVENT); - tick(); - fixture.detectChanges(); - - expect(scrollContainer.scrollTop).toEqual(0, 'Expected panel to reveal the first option.'); - })); - }); describe('aria', () => { @@ -832,7 +803,7 @@ describe('MdAutocomplete', () => { expect(input.hasAttribute('aria-activedescendant')) .toBe(false, 'Expected aria-activedescendant to be absent if no active item.'); - const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent; + const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.whenStable().then(() => { @@ -1140,7 +1111,7 @@ describe('MdAutocomplete', () => { tick(); fixture.detectChanges(); - const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent; + const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); tick(); fixture.detectChanges(); @@ -1418,10 +1389,3 @@ class AutocompleteWithNativeInput { }); } } - - -/** This is a mock keyboard event to test keyboard events in the autocomplete. */ -class MockKeyboardEvent { - constructor(public keyCode: number) {} - preventDefault() {} -} diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 0d2407c495ba..84318e26f059 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -3,16 +3,9 @@ import {Component, DebugElement, QueryList} from '@angular/core'; import {By} from '@angular/platform-browser'; import {MdChip, MdChipList, MdChipsModule} from './index'; 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'; +import {createKeyboardEvent} from '../core/testing/event-objects'; -class FakeKeyboardEvent extends FakeEvent { - constructor(keyCode: number, protected target: HTMLElement) { - super(keyCode); - - this.target = target; - } -} describe('MdChipList', () => { let fixture: ComponentFixture; @@ -117,7 +110,7 @@ describe('MdChipList', () => { let nativeChips = chipListNativeElement.querySelectorAll('md-chip'); let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement; - let LEFT_EVENT = new FakeKeyboardEvent(LEFT_ARROW, lastNativeChip) as any; + let LEFT_EVENT = createKeyboardEvent('keydown', LEFT_ARROW, lastNativeChip); let array = chips.toArray(); let lastIndex = array.length - 1; let lastItem = array[lastIndex]; @@ -138,7 +131,7 @@ describe('MdChipList', () => { let nativeChips = chipListNativeElement.querySelectorAll('md-chip'); let firstNativeChip = nativeChips[0] as HTMLElement; - let RIGHT_EVENT: KeyboardEvent = new FakeKeyboardEvent(RIGHT_ARROW, firstNativeChip) as any; + let RIGHT_EVENT = createKeyboardEvent('keydown', RIGHT_ARROW, firstNativeChip); let array = chips.toArray(); let firstItem = array[0]; @@ -164,7 +157,7 @@ describe('MdChipList', () => { let nativeChips = chipListNativeElement.querySelectorAll('md-chip'); let firstNativeChip = nativeChips[0] as HTMLElement; - let SPACE_EVENT: KeyboardEvent = new FakeKeyboardEvent(SPACE, firstNativeChip) as any; + let SPACE_EVENT = createKeyboardEvent('keydown', SPACE, firstNativeChip); let firstChip: MdChip = chips.toArray()[0]; spyOn(testComponent, 'chipSelect'); @@ -198,7 +191,7 @@ describe('MdChipList', () => { }); it('SPACE ignores selection', () => { - let SPACE_EVENT: KeyboardEvent = new FakeEvent(SPACE) as KeyboardEvent; + let SPACE_EVENT = createKeyboardEvent('keydown', SPACE); let firstChip: MdChip = chips.toArray()[0]; spyOn(testComponent, 'chipSelect'); diff --git a/src/lib/core/a11y/list-key-manager.spec.ts b/src/lib/core/a11y/list-key-manager.spec.ts index e552e2642ddd..81cce8fc54e9 100644 --- a/src/lib/core/a11y/list-key-manager.spec.ts +++ b/src/lib/core/a11y/list-key-manager.spec.ts @@ -1,9 +1,11 @@ import {QueryList} from '@angular/core'; import {fakeAsync, tick} from '@angular/core/testing'; import {FocusKeyManager} from './focus-key-manager'; -import {DOWN_ARROW, UP_ARROW, TAB, HOME, END} from '../keyboard/keycodes'; +import {DOWN_ARROW, UP_ARROW, TAB} from '../keyboard/keycodes'; import {ListKeyManager} from './list-key-manager'; import {ActiveDescendantKeyManager} from './activedescendant-key-manager'; +import {createKeyboardEvent} from '../testing/event-objects'; + class FakeFocusable { disabled = false; @@ -24,30 +26,24 @@ class FakeQueryList extends QueryList { } } -export class FakeEvent { - defaultPrevented: boolean = false; - constructor(public keyCode: number) {} - preventDefault() { - this.defaultPrevented = true; - } -} describe('Key managers', () => { let itemList: FakeQueryList; - let DOWN_ARROW_EVENT: KeyboardEvent; - let UP_ARROW_EVENT: KeyboardEvent; - let TAB_EVENT: KeyboardEvent; - let HOME_EVENT: KeyboardEvent; - let END_EVENT: KeyboardEvent; + let fakeKeyEvents: { + downArrow: KeyboardEvent, + upArrow: KeyboardEvent, + tab: KeyboardEvent, + unsupported: KeyboardEvent + }; beforeEach(() => { itemList = new FakeQueryList(); - - DOWN_ARROW_EVENT = new FakeEvent(DOWN_ARROW) as KeyboardEvent; - UP_ARROW_EVENT = new FakeEvent(UP_ARROW) as KeyboardEvent; - TAB_EVENT = new FakeEvent(TAB) as KeyboardEvent; - HOME_EVENT = new FakeEvent(HOME) as KeyboardEvent; - END_EVENT = new FakeEvent(END) as KeyboardEvent; + fakeKeyEvents = { + downArrow: createKeyboardEvent('keydown', DOWN_ARROW), + upArrow: createKeyboardEvent('keydown', UP_ARROW), + tab: createKeyboardEvent('keydown', TAB), + unsupported: createKeyboardEvent('keydown', 65) // corresponds to the letter "a" + }; }); @@ -55,12 +51,7 @@ describe('Key managers', () => { let keyManager: ListKeyManager; beforeEach(() => { - itemList.items = [ - new FakeFocusable(), - new FakeFocusable(), - new FakeFocusable() - ]; - + itemList.items = [new FakeFocusable(), new FakeFocusable(), new FakeFocusable()]; keyManager = new ListKeyManager(itemList); // first item is already focused @@ -72,7 +63,7 @@ describe('Key managers', () => { describe('Key events', () => { it('should set subsequent items as active when down arrow is pressed', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(1, 'Expected active item to be 1 after 1 down arrow event.'); @@ -80,7 +71,7 @@ describe('Key managers', () => { expect(keyManager.setActiveItem).toHaveBeenCalledWith(1); expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2); - keyManager.onKeydown(DOWN_ARROW_EVENT); + 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); @@ -89,7 +80,7 @@ describe('Key managers', () => { it('should set first item active when down arrow pressed if no active item', () => { keyManager.setActiveItem(null); - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(0, 'Expected active item to be 0 after down key if active item was null.'); @@ -99,14 +90,14 @@ describe('Key managers', () => { }); it('should set previous items as active when up arrow is pressed', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + 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(UP_ARROW_EVENT); + 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); @@ -114,7 +105,7 @@ describe('Key managers', () => { it('should do nothing when up arrow is pressed if no active item and not wrap', () => { keyManager.setActiveItem(null); - keyManager.onKeydown(UP_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.upArrow); expect(keyManager.activeItemIndex) .toBe(null, 'Expected nothing to happen if up arrow occurs and no active item.'); @@ -127,7 +118,7 @@ describe('Key managers', () => { itemList.items[1].disabled = true; // down arrow should skip past disabled item from 0 to 2 - keyManager.onKeydown(DOWN_ARROW_EVENT); + 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); @@ -135,7 +126,7 @@ describe('Key managers', () => { expect(keyManager.setActiveItem).toHaveBeenCalledWith(2); // up arrow should skip past disabled item from 2 to 0 - keyManager.onKeydown(UP_ARROW_EVENT); + 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); @@ -147,14 +138,14 @@ describe('Key managers', () => { itemList.items[1].disabled = undefined; itemList.items[2].disabled = undefined; - keyManager.onKeydown(DOWN_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + 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); @@ -162,82 +153,76 @@ describe('Key managers', () => { }); it('should not move active item past either end of the list', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); 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); + 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(UP_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); 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 spy = jasmine.createSpy('tabOut spy'); keyManager.tabOut.first().subscribe(spy); - keyManager.onKeydown(TAB_EVENT); + keyManager.onKeydown(fakeKeyEvents.tab); expect(spy).toHaveBeenCalled(); }); - it('should prevent the default keyboard action', () => { - expect(DOWN_ARROW_EVENT.defaultPrevented).toBe(false); + 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); - keyManager.onKeydown(DOWN_ARROW_EVENT); - - expect(DOWN_ARROW_EVENT.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(TAB_EVENT.defaultPrevented).toBe(false); + expect(fakeKeyEvents.tab.defaultPrevented).toBe(false); - keyManager.onKeydown(TAB_EVENT); + keyManager.onKeydown(fakeKeyEvents.tab); - expect(TAB_EVENT.defaultPrevented).toBe(false); + 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', () => { @@ -245,7 +230,7 @@ describe('Key managers', () => { expect(keyManager.activeItemIndex).toBeNull('Expected active index to default to null.'); - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex).toBe(0, 'Expected first item to become active.'); }); @@ -264,22 +249,22 @@ describe('Key managers', () => { }); it('should expose the active item correctly', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); + keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(2, `Expected last item of the list to be active.`); @@ -333,7 +318,7 @@ describe('Key managers', () => { }); it('should setPreviousItemActive()', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(1, `Expected second item of the list to be active.`); @@ -344,8 +329,8 @@ describe('Key managers', () => { it('should skip disabled items when setPreviousItemActive() is called', () => { itemList.items[1].disabled = true; - keyManager.onKeydown(DOWN_ARROW_EVENT); - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); + keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.activeItemIndex) .toBe(2, `Expected third item of the list to be active.`); @@ -365,31 +350,31 @@ describe('Key managers', () => { 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); + 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(DOWN_ARROW_EVENT); + 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(UP_ARROW_EVENT); + 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(null); - keyManager.onKeydown(UP_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + 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); @@ -403,12 +388,7 @@ describe('Key managers', () => { let keyManager: FocusKeyManager; beforeEach(() => { - itemList.items = [ - new FakeFocusable(), - new FakeFocusable(), - new FakeFocusable() - ]; - + itemList.items = [new FakeFocusable(), new FakeFocusable(), new FakeFocusable()]; keyManager = new FocusKeyManager(itemList); // first item is already focused @@ -420,25 +400,25 @@ describe('Key managers', () => { }); it('should focus subsequent items when down arrow is pressed', () => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + 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(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); expect(itemList.items[0].focus).not.toHaveBeenCalled(); expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); - keyManager.onKeydown(UP_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.upArrow); expect(itemList.items[0].focus).toHaveBeenCalledTimes(1); expect(itemList.items[1].focus).toHaveBeenCalledTimes(1); @@ -460,12 +440,7 @@ describe('Key managers', () => { let keyManager: ActiveDescendantKeyManager; beforeEach(fakeAsync(() => { - itemList.items = [ - new FakeHighlightable(), - new FakeHighlightable(), - new FakeHighlightable() - ]; - + itemList.items = [new FakeHighlightable(), new FakeHighlightable(), new FakeHighlightable()]; keyManager = new ActiveDescendantKeyManager(itemList); // first item is already focused @@ -482,13 +457,13 @@ describe('Key managers', () => { })); it('should set subsequent items as active with the DOWN arrow', fakeAsync(() => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); tick(); expect(itemList.items[1].setActiveStyles).toHaveBeenCalled(); expect(itemList.items[2].setActiveStyles).not.toHaveBeenCalled(); - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); tick(); expect(itemList.items[2].setActiveStyles).toHaveBeenCalled(); @@ -498,22 +473,22 @@ describe('Key managers', () => { keyManager.setLastItemActive(); tick(); - keyManager.onKeydown(UP_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.upArrow); tick(); expect(itemList.items[1].setActiveStyles).toHaveBeenCalled(); expect(itemList.items[0].setActiveStyles).not.toHaveBeenCalled(); - keyManager.onKeydown(UP_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.upArrow); tick(); expect(itemList.items[0].setActiveStyles).toHaveBeenCalled(); })); it('should set inactive styles on previously active items', fakeAsync(() => { - keyManager.onKeydown(DOWN_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.downArrow); tick(); expect(itemList.items[0].setInactiveStyles).toHaveBeenCalled(); - keyManager.onKeydown(UP_ARROW_EVENT); + keyManager.onKeydown(fakeKeyEvents.upArrow); tick(); expect(itemList.items[1].setInactiveStyles).toHaveBeenCalled(); })); diff --git a/src/lib/core/a11y/list-key-manager.ts b/src/lib/core/a11y/list-key-manager.ts index b9e8cbb7b6b2..e9e11197b203 100644 --- a/src/lib/core/a11y/list-key-manager.ts +++ b/src/lib/core/a11y/list-key-manager.ts @@ -1,5 +1,5 @@ import {QueryList} from '@angular/core'; -import {UP_ARROW, DOWN_ARROW, TAB, HOME, END} from '../core'; +import {UP_ARROW, DOWN_ARROW, TAB} from '../core'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; @@ -57,12 +57,6 @@ export class ListKeyManager { case UP_ARROW: this.setPreviousItemActive(); break; - case HOME: - this.setFirstItemActive(); - break; - case END: - this.setLastItemActive(); - break; case TAB: // Note that we shouldn't prevent the default action on tab. this._tabOut.next(null); @@ -174,4 +168,3 @@ export class ListKeyManager { } } - diff --git a/src/lib/core/testing/event-objects.ts b/src/lib/core/testing/event-objects.ts index 819d943e7940..5e29bcfcea3d 100644 --- a/src/lib/core/testing/event-objects.ts +++ b/src/lib/core/testing/event-objects.ts @@ -22,7 +22,7 @@ export function createMouseEvent(type: string, x = 0, y = 0) { } /** Dispatches a keydown event from an element. */ -export function createKeyboardEvent(type: string, keyCode: number) { +export function createKeyboardEvent(type: string, keyCode: number, target?: Element) { let event = document.createEvent('KeyboardEvent') as any; // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`. let initEventFn = (event.initKeyEvent || event.initKeyboardEvent).bind(event); @@ -32,7 +32,10 @@ export function createKeyboardEvent(type: string, keyCode: number) { // Webkit Browsers don't set the keyCode when calling the init function. // See related bug https://bugs.webkit.org/show_bug.cgi?id=16735 - Object.defineProperty(event, 'keyCode', { get: () => keyCode }); + Object.defineProperties(event, { + keyCode: { get: () => keyCode }, + target: { get: () => target } + }); // IE won't set `defaultPrevented` on synthetic events so we need to do it manually. event.preventDefault = function() { diff --git a/src/lib/select/select.html b/src/lib/select/select.html index 510da0dc78a4..dc3e640e3a8f 100644 --- a/src/lib/select/select.html +++ b/src/lib/select/select.html @@ -17,7 +17,7 @@ backdropClass="cdk-overlay-transparent-backdrop" [positions]="_positions" [minWidth]="_triggerWidth" [offsetY]="_offsetY" (attach)="_onAttached()" (detach)="close()">
diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 43a4af6673cf..e67335d2e56a 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -16,7 +16,7 @@ import {MdSelect, MdSelectFloatPlaceholderType} from './select'; import {getMdSelectDynamicMultipleError, getMdSelectNonArrayValueError} from './select-errors'; import {MdOption} from '../core/option/option'; import {Dir} from '../core/rtl/dir'; -import {DOWN_ARROW, UP_ARROW, ENTER, SPACE} from '../core/keyboard/keycodes'; +import {DOWN_ARROW, UP_ARROW, ENTER, SPACE, HOME, END, TAB} from '../core/keyboard/keycodes'; import { ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; @@ -24,7 +24,6 @@ import {Subject} from 'rxjs/Subject'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {dispatchFakeEvent, dispatchKeyboardEvent} from '../core/testing/dispatch-events'; import {wrappedErrorMessage} from '../core/testing/wrapped-error-message'; -import {TAB} from '../core/keyboard/keycodes'; import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; @@ -232,6 +231,34 @@ describe('MdSelect', () => { }); })); + it('should focus the first option when pressing HOME', () => { + fixture.componentInstance.control.setValue('pizza-1'); + fixture.detectChanges(); + + trigger.click(); + fixture.detectChanges(); + + const panel = overlayContainerElement.querySelector('.mat-select-panel'); + const event = dispatchKeyboardEvent(panel, 'keydown', HOME); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0); + expect(event.defaultPrevented).toBe(true); + }); + + it('should focus the last option when pressing END', () => { + fixture.componentInstance.control.setValue('pizza-1'); + fixture.detectChanges(); + + trigger.click(); + fixture.detectChanges(); + + const panel = overlayContainerElement.querySelector('.mat-select-panel'); + const event = dispatchKeyboardEvent(panel, 'keydown', END); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7); + expect(event.defaultPrevented).toBe(true); + }); + }); describe('selection logic', () => { diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 5099709a42f6..273008146ea2 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -18,7 +18,7 @@ import { OnInit, } from '@angular/core'; import {MdOption, MdOptionSelectionChange} from '../core/option/option'; -import {ENTER, SPACE, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes'; +import {ENTER, SPACE, UP_ARROW, DOWN_ARROW, HOME, END} from '../core/keyboard/keycodes'; import {FocusKeyManager} from '../core/a11y/focus-key-manager'; import {Dir} from '../core/rtl/dir'; import {Observable} from 'rxjs/Observable'; @@ -112,7 +112,7 @@ export type MdSelectFloatPlaceholderType = 'always' | 'never' | 'auto'; '[attr.aria-owns]': '_optionIds', '[class.mat-select-disabled]': 'disabled', '[class.mat-select]': 'true', - '(keydown)': '_handleKeydown($event)', + '(keydown)': '_handleClosedKeydown($event)', '(blur)': '_onBlur()', }, animations: [ @@ -168,15 +168,15 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal */ _triggerWidth: number; + /** Manages keyboard events for options in the panel. */ + _keyManager: FocusKeyManager; + /** * The width of the selected option's value. Must be set programmatically * to ensure its overflow is clipped, as it's absolutely positioned. */ _selectedValueWidth: number; - /** Manages keyboard events for options in the panel. */ - _keyManager: FocusKeyManager; - /** View -> model callback called when value changes */ _onChange = (value: any) => {}; @@ -470,7 +470,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal } /** Handles the keyboard interactions of a closed select. */ - _handleKeydown(event: KeyboardEvent): void { + _handleClosedKeydown(event: KeyboardEvent): void { if (!this.disabled) { if (event.keyCode === ENTER || event.keyCode === SPACE) { event.preventDefault(); // prevents the page from scrolling down when pressing space @@ -481,6 +481,17 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal } } + /** Handles keypresses inside the panel. */ + _handlePanelKeydown(event: KeyboardEvent): void { + if (event.keyCode === HOME || event.keyCode === END) { + event.preventDefault(); + event.keyCode === HOME ? this._keyManager.setFirstItemActive() : + this._keyManager.setLastItemActive(); + } else { + this._keyManager.onKeydown(event); + } + } + /** * When the panel element is finished transforming in (though not fading in), it * emits an event and focuses an option if the panel is open.