fix(select): support typing to select items on when closed (#7885)

Adds support for selecting items on a closed select by typing. This is similar to how the native select works.
This commit is contained in:
Kristiyan Kostadinov
2017-11-02 04:38:08 +01:00
committed by Andrew Seguin
parent 943395ee5c
commit 8edb416d26
4 changed files with 73 additions and 32 deletions
+12
View File
@@ -255,6 +255,18 @@ describe('Key managers', () => {
subscription.unsubscribe();
});
it('should not emit an event if the item did not change', () => {
const spy = jasmine.createSpy('change spy');
const subscription = keyManager.change.subscribe(spy);
keyManager.setActiveItem(2);
keyManager.setActiveItem(2);
expect(spy).toHaveBeenCalledTimes(1);
subscription.unsubscribe();
});
});
describe('programmatic focus', () => {
+6 -1
View File
@@ -100,9 +100,14 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
* @param index The index of the item to be set as active.
*/
setActiveItem(index: number): void {
const previousIndex = this._activeItemIndex;
this._activeItemIndex = index;
this._activeItem = this._items.toArray()[index];
this.change.next(index);
if (this._activeItemIndex !== previousIndex) {
this.change.next(index);
}
}
/**
+40
View File
@@ -2066,6 +2066,27 @@ describe('MatSelect', () => {
'Expected value from second option to have been set on the model.');
}));
it('should be able to select options by typing on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).toBeFalsy('Expected no initial value.');
dispatchEvent(select, createKeyboardEvent('keydown', 80, undefined, 'p'));
tick(200);
expect(options[1].selected).toBe(true, 'Expected second option to be selected.');
expect(formControl.value).toBe(options[1].value,
'Expected value from second option to have been set on the model.');
dispatchEvent(select, createKeyboardEvent('keydown', 69, undefined, 'e'));
tick(200);
expect(options[5].selected).toBe(true, 'Expected sixth option to be selected.');
expect(formControl.value).toBe(options[5].value,
'Expected value from sixth option to have been set on the model.');
}));
it('should open the panel when pressing the arrow keys on a closed multiple select', () => {
fixture.destroy();
@@ -2086,6 +2107,25 @@ describe('MatSelect', () => {
expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.');
});
it('should do nothing when typing on a closed multi-select', () => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
const initialValue = instance.control.value;
expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.');
dispatchEvent(select, createKeyboardEvent('keydown', 80, undefined, 'p'));
expect(instance.select.panelOpen).toBe(false, 'Expected panel to stay closed.');
expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.');
});
it('should do nothing if the key manager did not change the active item', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
+15 -31
View File
@@ -524,9 +524,9 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
// `parseInt` ignores the trailing 'px' and converts this to a number.
this._triggerFontSize = parseInt(getComputedStyle(this.trigger.nativeElement)['font-size']);
this._panelOpen = true;
this._calculateOverlayPosition();
this._highlightCorrectOption();
this._panelOpen = true;
this._changeDetectorRef.markForCheck();
// Set the font size on the panel element once it exists.
@@ -637,11 +637,15 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
/** Handles keyboard events while the select is closed. */
private _handleClosedKeydown(event: KeyboardEvent): void {
if (event.keyCode === ENTER || event.keyCode === SPACE) {
const keyCode = event.keyCode;
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
const isOpenKey = keyCode === ENTER || keyCode === SPACE;
if (isOpenKey || (this.multiple && isArrowKey)) {
event.preventDefault(); // prevents the page from scrolling down when pressing space
this.open();
} else if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
this._handleClosedArrowKey(event);
} else if (!this.multiple) {
this._keyManager.onKeydown(event);
}
}
@@ -813,10 +817,13 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
this._keyManager = new ActiveDescendantKeyManager<MatOption>(this.options).withTypeAhead();
this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => this.close());
this._keyManager.change.pipe(
takeUntil(this._destroy),
filter(() => this._panelOpen && !!this.panel)
).subscribe(() => this._scrollActiveOptionIntoView());
this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
if (this._panelOpen && this.panel) {
this._scrollActiveOptionIntoView();
} else if (!this._panelOpen && !this.multiple && this._keyManager.activeItem) {
this._keyManager.activeItem._selectViaInteraction();
}
});
}
/** Drops current option subscriptions and IDs and resets from scratch. */
@@ -1171,29 +1178,6 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
return `50% ${originY}px 0px`;
}
/** Handles the user pressing the arrow keys on a closed select. */
private _handleClosedArrowKey(event: KeyboardEvent): void {
if (this._multiple) {
event.preventDefault();
this.open();
} else {
const prevActiveItem = this._keyManager.activeItem;
// Cycle though the select options even when the select is closed,
// matching the behavior of the native select element.
// TODO(crisbeto): native selects also cycle through the options with left/right arrows,
// however the key manager only supports up/down at the moment.
this._keyManager.onKeydown(event);
const currentActiveItem = this._keyManager.activeItem;
if (currentActiveItem && currentActiveItem !== prevActiveItem) {
this._clearSelection();
this._setSelectionByValue(currentActiveItem.value, true);
}
}
}
/** Calculates the amount of items in the select. This includes options and group labels. */
private _getItemCount(): number {
return this.options.length + this.optionGroups.length;