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