diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index ca504db2cac1..792943346d87 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -525,9 +525,12 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { return this._getConnectedElement().nativeElement.getBoundingClientRect().width; } - /** Reset active item to -1 so arrow events will activate the correct options. */ + /** + * Resets the active item to -1 so arrow events will activate the + * correct options, or to 0 if the consumer opted into it. + */ private _resetActiveItem(): void { - this.autocomplete._keyManager.setActiveItem(-1); + this.autocomplete._keyManager.setActiveItem(this.autocomplete.autoActiveFirstOption ? 0 : -1); } /** Determines whether the panel can be opened. */ diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 1c16c6ecea21..02ddd3d65515 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -1,4 +1,4 @@ -import {Direction, Directionality} from '@angular/cdk/bidi'; +import {Directionality} from '@angular/cdk/bidi'; import {DOWN_ARROW, ENTER, ESCAPE, SPACE, UP_ARROW, TAB} from '@angular/cdk/keycodes'; import {OverlayContainer, Overlay} from '@angular/cdk/overlay'; import {map} from 'rxjs/operators/map'; @@ -20,6 +20,7 @@ import { ViewChild, ViewChildren, NgZone, + Provider, } from '@angular/core'; import { async, @@ -46,6 +47,7 @@ import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger, MAT_AUTOCOMPLETE_SCROLL_STRATEGY, + MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, } from './index'; @@ -56,7 +58,7 @@ describe('MatAutocomplete', () => { let zone: MockNgZone; // Creates a test component fixture. - function createComponent(component: any, dir: Direction = 'ltr'): ComponentFixture { + function createComponent(component: any, providers: Provider[] = []): ComponentFixture { TestBed.configureTestingModule({ imports: [ MatAutocompleteModule, @@ -68,14 +70,14 @@ describe('MatAutocomplete', () => { ], declarations: [component], providers: [ - {provide: Directionality, useFactory: () => ({value: dir})}, {provide: ScrollDispatcher, useFactory: () => ({ scrolled: () => scrolledSubject.asObservable() })}, {provide: NgZone, useFactory: () => { zone = new MockNgZone(); return zone; - }} + }}, + ...providers ] }); @@ -410,9 +412,11 @@ describe('MatAutocomplete', () => { }); it('should have the correct text direction in RTL', () => { - const rtlFixture = createComponent(SimpleAutocomplete, 'rtl'); - rtlFixture.detectChanges(); + const rtlFixture = createComponent(SimpleAutocomplete, [ + {provide: Directionality, useFactory: () => ({value: 'rtl'})}, + ]); + rtlFixture.detectChanges(); rtlFixture.componentInstance.trigger.openPanel(); rtlFixture.detectChanges(); @@ -1291,12 +1295,12 @@ describe('MatAutocomplete', () => { beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); + }); + it('should deselect any other selected option', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - }); - it('should deselect any other selected option', fakeAsync(() => { let options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[0].click(); @@ -1320,6 +1324,9 @@ describe('MatAutocomplete', () => { })); it('should call deselect only on the previous selected option', fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + let options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[0].click(); @@ -1342,15 +1349,33 @@ describe('MatAutocomplete', () => { componentOptions.slice(1).forEach(option => expect(option.deselect).not.toHaveBeenCalled()); })); - it('should emit an event when an option is selected', fakeAsync(() => { - const spy = jasmine.createSpy('option selection spy'); - const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy); - const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; - option.click(); + it('should be able to preselect the first option', fakeAsync(() => { + fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true; + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); fixture.detectChanges(); - expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange)); - subscription.unsubscribe(); + expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList) + .toContain('mat-active', 'Expected first option to be highlighted.'); + })); + + it('should be able to configure preselecting the first option globally', fakeAsync(() => { + overlayContainer.ngOnDestroy(); + fixture.destroy(); + TestBed.resetTestingModule(); + fixture = createComponent(SimpleAutocomplete, [ + {provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: {autoActiveFirstOption: true}} + ]); + + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList) + .toContain('mat-active', 'Expected first option to be highlighted.'); })); it('should handle `optionSelections` being accessed too early', fakeAsync(() => { @@ -1743,8 +1768,8 @@ describe('MatAutocomplete', () => { - + {{ state.code }}: {{ state.name }} diff --git a/src/lib/autocomplete/autocomplete.ts b/src/lib/autocomplete/autocomplete.ts index 593e230c3fb0..20a8a4e8a950 100644 --- a/src/lib/autocomplete/autocomplete.ts +++ b/src/lib/autocomplete/autocomplete.ts @@ -19,6 +19,9 @@ import { ChangeDetectionStrategy, EventEmitter, Output, + InjectionToken, + Inject, + Optional, } from '@angular/core'; import { MatOption, @@ -28,6 +31,7 @@ import { CanDisableRipple, } from '@angular/material/core'; import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; /** @@ -50,6 +54,16 @@ export class MatAutocompleteSelectedEvent { export class MatAutocompleteBase {} export const _MatAutocompleteMixinBase = mixinDisableRipple(MatAutocompleteBase); +/** Default `mat-autocomplete` options that can be overridden. */ +export interface MatAutocompleteDefaultOptions { + /** Whether the first option should be highlighted when an autocomplete panel is opened. */ + autoActiveFirstOption?: boolean; +} + +/** Injection token to be used to override the default options for `mat-autocomplete`. */ +export const MAT_AUTOCOMPLETE_DEFAULT_OPTIONS = + new InjectionToken('mat-autocomplete-default-options'); + @Component({ moduleId: module.id, @@ -98,6 +112,18 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC /** Function that maps an option's control value to its display value in the trigger. */ @Input() displayWith: ((value: any) => string) | null = null; + /** + * Whether the first option should be highlighted when the autocomplete panel is opened. + * Can be configured globally through the `MAT_AUTOCOMPLETE_DEFAULT_OPTIONS` token. + */ + @Input() + get autoActiveFirstOption(): boolean { return this._autoActiveFirstOption; } + set autoActiveFirstOption(value: boolean) { + this._autoActiveFirstOption = coerceBooleanProperty(value); + } + private _autoActiveFirstOption: boolean; + + /** Event that is emitted whenever an option from the list is selected. */ @Output() readonly optionSelected: EventEmitter = new EventEmitter(); @@ -118,8 +144,19 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC /** Unique ID to be used by autocomplete trigger's "aria-owns" property. */ id: string = `mat-autocomplete-${_uniqueAutocompleteIdCounter++}`; - constructor(private _changeDetectorRef: ChangeDetectorRef, private _elementRef: ElementRef) { + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + + // @deletion-target Turn into required param in 6.0.0 + @Optional() @Inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS) + defaults?: MatAutocompleteDefaultOptions) { super(); + + this._autoActiveFirstOption = defaults && + typeof defaults.autoActiveFirstOption !== 'undefined' ? + defaults.autoActiveFirstOption : + false; } ngAfterContentInit() {