fix(list-key-manager): remove handling for home and end keys (#4544)

* 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.
This commit is contained in:
Kristiyan Kostadinov
2017-05-22 23:48:16 +02:00
committed by tinayuangao
parent c53bbbaa11
commit 2d16345c07
8 changed files with 149 additions and 185 deletions
+9 -45
View File
@@ -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<HTMLElement>;
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() {}
}
+4 -13
View File
@@ -3,19 +3,10 @@ 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, TAB} 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<any>;
let chipListDebugElement: DebugElement;
@@ -117,7 +108,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 +129,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 +155,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');
@@ -209,7 +200,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');
+83 -108
View File
@@ -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<T> extends QueryList<T> {
}
}
export class FakeEvent {
defaultPrevented: boolean = false;
constructor(public keyCode: number) {}
preventDefault() {
this.defaultPrevented = true;
}
}
describe('Key managers', () => {
let itemList: FakeQueryList<any>;
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<any>();
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<FakeFocusable>;
beforeEach(() => {
itemList.items = [
new FakeFocusable(),
new FakeFocusable(),
new FakeFocusable()
];
itemList.items = [new FakeFocusable(), new FakeFocusable(), new FakeFocusable()];
keyManager = new ListKeyManager<FakeFocusable>(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();
}));
+1 -8
View File
@@ -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<T extends CanDisable> {
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<T extends CanDisable> {
}
}
+5 -2
View File
@@ -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() {
+1 -1
View File
@@ -17,7 +17,7 @@
backdropClass="cdk-overlay-transparent-backdrop" [positions]="_positions" [minWidth]="_triggerWidth"
[offsetY]="_offsetY" (attach)="_onAttached()" (detach)="close()">
<div class="mat-select-panel" [@transformPanel]="'showing'" (@transformPanel.done)="_onPanelDone()"
(keydown)="_keyManager.onKeydown($event)" [style.transformOrigin]="_transformOrigin"
(keydown)="_handlePanelKeydown($event)" [style.transformOrigin]="_transformOrigin"
[class.mat-select-panel-done-animating]="_panelDoneAnimating" [ngClass]="'mat-' + color">
<div class="mat-select-content" [@fadeInContent]="'showing'" (@fadeInContent.done)="_onFadeInDone()">
<ng-content></ng-content>
+29 -2
View File
@@ -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', () => {
+17 -6
View File
@@ -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.