mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
tinayuangao
parent
c53bbbaa11
commit
2d16345c07
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,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> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user