Skip to content

Commit 8edb416

Browse files
crisbetoandrewseguin
authored andcommitted
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.
1 parent 943395e commit 8edb416

File tree

4 files changed

+73
-32
lines changed

4 files changed

+73
-32
lines changed

src/cdk/a11y/list-key-manager.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ describe('Key managers', () => {
255255
subscription.unsubscribe();
256256
});
257257

258+
it('should not emit an event if the item did not change', () => {
259+
const spy = jasmine.createSpy('change spy');
260+
const subscription = keyManager.change.subscribe(spy);
261+
262+
keyManager.setActiveItem(2);
263+
keyManager.setActiveItem(2);
264+
265+
expect(spy).toHaveBeenCalledTimes(1);
266+
267+
subscription.unsubscribe();
268+
});
269+
258270
});
259271

260272
describe('programmatic focus', () => {

src/cdk/a11y/list-key-manager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,14 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
100100
* @param index The index of the item to be set as active.
101101
*/
102102
setActiveItem(index: number): void {
103+
const previousIndex = this._activeItemIndex;
104+
103105
this._activeItemIndex = index;
104106
this._activeItem = this._items.toArray()[index];
105-
this.change.next(index);
107+
108+
if (this._activeItemIndex !== previousIndex) {
109+
this.change.next(index);
110+
}
106111
}
107112

108113
/**

src/lib/select/select.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,27 @@ describe('MatSelect', () => {
20662066
'Expected value from second option to have been set on the model.');
20672067
}));
20682068

2069+
it('should be able to select options by typing on a closed select', fakeAsync(() => {
2070+
const formControl = fixture.componentInstance.control;
2071+
const options = fixture.componentInstance.options.toArray();
2072+
2073+
expect(formControl.value).toBeFalsy('Expected no initial value.');
2074+
2075+
dispatchEvent(select, createKeyboardEvent('keydown', 80, undefined, 'p'));
2076+
tick(200);
2077+
2078+
expect(options[1].selected).toBe(true, 'Expected second option to be selected.');
2079+
expect(formControl.value).toBe(options[1].value,
2080+
'Expected value from second option to have been set on the model.');
2081+
2082+
dispatchEvent(select, createKeyboardEvent('keydown', 69, undefined, 'e'));
2083+
tick(200);
2084+
2085+
expect(options[5].selected).toBe(true, 'Expected sixth option to be selected.');
2086+
expect(formControl.value).toBe(options[5].value,
2087+
'Expected value from sixth option to have been set on the model.');
2088+
}));
2089+
20692090
it('should open the panel when pressing the arrow keys on a closed multiple select', () => {
20702091
fixture.destroy();
20712092

@@ -2086,6 +2107,25 @@ describe('MatSelect', () => {
20862107
expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.');
20872108
});
20882109

2110+
it('should do nothing when typing on a closed multi-select', () => {
2111+
fixture.destroy();
2112+
2113+
const multiFixture = TestBed.createComponent(MultiSelect);
2114+
const instance = multiFixture.componentInstance;
2115+
2116+
multiFixture.detectChanges();
2117+
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
2118+
2119+
const initialValue = instance.control.value;
2120+
2121+
expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.');
2122+
2123+
dispatchEvent(select, createKeyboardEvent('keydown', 80, undefined, 'p'));
2124+
2125+
expect(instance.select.panelOpen).toBe(false, 'Expected panel to stay closed.');
2126+
expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.');
2127+
});
2128+
20892129
it('should do nothing if the key manager did not change the active item', fakeAsync(() => {
20902130
const formControl = fixture.componentInstance.control;
20912131

src/lib/select/select.ts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,9 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
524524
// `parseInt` ignores the trailing 'px' and converts this to a number.
525525
this._triggerFontSize = parseInt(getComputedStyle(this.trigger.nativeElement)['font-size']);
526526

527+
this._panelOpen = true;
527528
this._calculateOverlayPosition();
528529
this._highlightCorrectOption();
529-
this._panelOpen = true;
530530
this._changeDetectorRef.markForCheck();
531531

532532
// Set the font size on the panel element once it exists.
@@ -637,11 +637,15 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
637637

638638
/** Handles keyboard events while the select is closed. */
639639
private _handleClosedKeydown(event: KeyboardEvent): void {
640-
if (event.keyCode === ENTER || event.keyCode === SPACE) {
640+
const keyCode = event.keyCode;
641+
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
642+
const isOpenKey = keyCode === ENTER || keyCode === SPACE;
643+
644+
if (isOpenKey || (this.multiple && isArrowKey)) {
641645
event.preventDefault(); // prevents the page from scrolling down when pressing space
642646
this.open();
643-
} else if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
644-
this._handleClosedArrowKey(event);
647+
} else if (!this.multiple) {
648+
this._keyManager.onKeydown(event);
645649
}
646650
}
647651

@@ -813,10 +817,13 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
813817
this._keyManager = new ActiveDescendantKeyManager<MatOption>(this.options).withTypeAhead();
814818
this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => this.close());
815819

816-
this._keyManager.change.pipe(
817-
takeUntil(this._destroy),
818-
filter(() => this._panelOpen && !!this.panel)
819-
).subscribe(() => this._scrollActiveOptionIntoView());
820+
this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
821+
if (this._panelOpen && this.panel) {
822+
this._scrollActiveOptionIntoView();
823+
} else if (!this._panelOpen && !this.multiple && this._keyManager.activeItem) {
824+
this._keyManager.activeItem._selectViaInteraction();
825+
}
826+
});
820827
}
821828

822829
/** Drops current option subscriptions and IDs and resets from scratch. */
@@ -1171,29 +1178,6 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
11711178
return `50% ${originY}px 0px`;
11721179
}
11731180

1174-
/** Handles the user pressing the arrow keys on a closed select. */
1175-
private _handleClosedArrowKey(event: KeyboardEvent): void {
1176-
if (this._multiple) {
1177-
event.preventDefault();
1178-
this.open();
1179-
} else {
1180-
const prevActiveItem = this._keyManager.activeItem;
1181-
1182-
// Cycle though the select options even when the select is closed,
1183-
// matching the behavior of the native select element.
1184-
// TODO(crisbeto): native selects also cycle through the options with left/right arrows,
1185-
// however the key manager only supports up/down at the moment.
1186-
this._keyManager.onKeydown(event);
1187-
1188-
const currentActiveItem = this._keyManager.activeItem;
1189-
1190-
if (currentActiveItem && currentActiveItem !== prevActiveItem) {
1191-
this._clearSelection();
1192-
this._setSelectionByValue(currentActiveItem.value, true);
1193-
}
1194-
}
1195-
}
1196-
11971181
/** Calculates the amount of items in the select. This includes options and group labels. */
11981182
private _getItemCount(): number {
11991183
return this.options.length + this.optionGroups.length;

0 commit comments

Comments
 (0)