Skip to content

Commit

Permalink
fix(select): support typing to select items on when closed (#7885)
Browse files Browse the repository at this point in the history
Adds support for selecting items on a closed select by typing. This is similar to how the native select works.
  • Loading branch information
crisbeto authored and andrewseguin committed Nov 2, 2017
1 parent 943395e commit 8edb416
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 32 deletions.
12 changes: 12 additions & 0 deletions src/cdk/a11y/list-key-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
7 changes: 6 additions & 1 deletion src/cdk/a11y/list-key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/**
Expand Down
40 changes: 40 additions & 0 deletions src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;

Expand Down
46 changes: 15 additions & 31 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 8edb416

Please sign in to comment.