From 8edb416d2602aaafe8139d1f47985a9e1661edf5 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 2 Nov 2017 04:38:08 +0100 Subject: [PATCH] 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. --- src/cdk/a11y/list-key-manager.spec.ts | 12 +++++++ src/cdk/a11y/list-key-manager.ts | 7 +++- src/lib/select/select.spec.ts | 40 +++++++++++++++++++++++ src/lib/select/select.ts | 46 +++++++++------------------ 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/src/cdk/a11y/list-key-manager.spec.ts b/src/cdk/a11y/list-key-manager.spec.ts index 26b5975d810..742b9399027 100644 --- a/src/cdk/a11y/list-key-manager.spec.ts +++ b/src/cdk/a11y/list-key-manager.spec.ts @@ -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', () => { diff --git a/src/cdk/a11y/list-key-manager.ts b/src/cdk/a11y/list-key-manager.ts index fca032b3dcc..ff54c0d0851 100644 --- a/src/cdk/a11y/list-key-manager.ts +++ b/src/cdk/a11y/list-key-manager.ts @@ -100,9 +100,14 @@ export class ListKeyManager { * @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); + } } /** diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index ccc0b7f3ef6..4a81af1952e 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -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; diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index e2e608b509e..0e4e3017b92 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -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(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;