From ff00a5edb2f4ff61cecf396709fb22502afdd361 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Tue, 4 Feb 2020 20:31:53 +0100 Subject: [PATCH] feat(autocomplete): add event when option is activated Exposes an event that emits whenever an autocomplete option is activated using the keyboard. Fixes #17587. --- .../autocomplete/autocomplete.spec.ts | 49 +++++++++++++++++++ src/material/autocomplete/autocomplete.ts | 25 +++++++++- .../material/autocomplete.d.ts | 11 ++++- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index c5e6e7efe864..c13393f7d6e0 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -2496,6 +2496,34 @@ describe('MatAutocomplete', () => { expect(event.option.value).toBe('Puerto Rico'); })); + it('should emit an event when an option is activated', fakeAsync(() => { + const fixture = createComponent(AutocompleteWithActivatedEvent); + + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + zone.simulateZoneExit(); + fixture.detectChanges(); + + const input = fixture.nativeElement.querySelector('input'); + const spy = fixture.componentInstance.optionActivated; + const autocomplete = fixture.componentInstance.autocomplete; + const options = fixture.componentInstance.options.toArray(); + + expect(spy).not.toHaveBeenCalled(); + + dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + expect(spy.calls.mostRecent().args[0]).toEqual({source: autocomplete, option: options[0]}); + + dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + expect(spy.calls.mostRecent().args[0]).toEqual({source: autocomplete, option: options[1]}); + + dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + expect(spy.calls.mostRecent().args[0]).toEqual({source: autocomplete, option: options[2]}); + })); + it('should be able to set a custom panel connection element', () => { const fixture = createComponent(AutocompleteWithDifferentOrigin); @@ -2978,3 +3006,24 @@ class AutocompleteWithNativeAutocompleteAttribute { }) class InputWithoutAutocompleteAndDisabled { } + + +@Component({ + template: ` + + + + + + {{ state }} + + ` +}) +class AutocompleteWithActivatedEvent { + states = ['California', 'West Virginia', 'Florida']; + optionActivated = jasmine.createSpy('optionActivated callback'); + + @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger; + @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete; + @ViewChildren(MatOption) options: QueryList; +} diff --git a/src/material/autocomplete/autocomplete.ts b/src/material/autocomplete/autocomplete.ts index 4e96f737c907..87e519dc2e62 100644 --- a/src/material/autocomplete/autocomplete.ts +++ b/src/material/autocomplete/autocomplete.ts @@ -24,6 +24,7 @@ import { TemplateRef, ViewChild, ViewEncapsulation, + OnDestroy, } from '@angular/core'; import { CanDisableRipple, @@ -33,6 +34,7 @@ import { MatOption, mixinDisableRipple, } from '@angular/material/core'; +import {Subscription} from 'rxjs'; /** @@ -50,6 +52,14 @@ export class MatAutocompleteSelectedEvent { public option: MatOption) { } } +/** Event object that is emitted when an autocomplete option is activated. */ +export interface MatAutocompleteActivatedEvent { + /** Reference to the autocomplete panel that emitted the event. */ + source: MatAutocomplete; + + /** Option that was selected. */ + option: MatOption|null; +} // Boilerplate for applying mixins to MatAutocomplete. /** @docs-private */ @@ -91,7 +101,8 @@ export function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY(): MatAutocompleteDefau ] }) export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterContentInit, - CanDisableRipple { + CanDisableRipple, OnDestroy { + private _activeOptionChanges = Subscription.EMPTY; /** Manages active item in option list based on key events. */ _keyManager: ActiveDescendantKeyManager; @@ -149,6 +160,10 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC /** Event that is emitted when the autocomplete panel is closed. */ @Output() readonly closed: EventEmitter = new EventEmitter(); + /** Emits whenever an option is activated using the keyboard. */ + @Output() readonly optionActivated: EventEmitter = + new EventEmitter(); + /** * Takes classes set on the host mat-autocomplete element and applies them to the panel * inside the overlay container to allow for easy styling. @@ -183,10 +198,18 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC ngAfterContentInit() { this._keyManager = new ActiveDescendantKeyManager(this.options).withWrap(); + this._activeOptionChanges = this._keyManager.change.subscribe(index => { + this.optionActivated.emit({source: this, option: this.options.toArray()[index] || null}); + }); + // Set the initial visibility state. this._setVisibility(); } + ngOnDestroy() { + this._activeOptionChanges.unsubscribe(); + } + /** * Sets the panel scrollTop. This allows us to manually scroll to display options * above or below the fold, as they are not actually being focused when active. diff --git a/tools/public_api_guard/material/autocomplete.d.ts b/tools/public_api_guard/material/autocomplete.d.ts index 52719b4d4280..fe958ac72a5b 100644 --- a/tools/public_api_guard/material/autocomplete.d.ts +++ b/tools/public_api_guard/material/autocomplete.d.ts @@ -20,7 +20,7 @@ export declare const MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER: { export declare const MAT_AUTOCOMPLETE_VALUE_ACCESSOR: any; -export declare class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterContentInit, CanDisableRipple { +export declare class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterContentInit, CanDisableRipple, OnDestroy { _classList: { [key: string]: boolean; }; @@ -34,6 +34,7 @@ export declare class MatAutocomplete extends _MatAutocompleteMixinBase implement id: string; get isOpen(): boolean; readonly opened: EventEmitter; + readonly optionActivated: EventEmitter; optionGroups: QueryList; readonly optionSelected: EventEmitter; options: QueryList; @@ -47,12 +48,18 @@ export declare class MatAutocomplete extends _MatAutocompleteMixinBase implement _setScrollTop(scrollTop: number): void; _setVisibility(): void; ngAfterContentInit(): void; + ngOnDestroy(): void; static ngAcceptInputType_autoActiveFirstOption: BooleanInput; static ngAcceptInputType_disableRipple: BooleanInput; - static ɵcmp: i0.ɵɵComponentDefWithMeta; + static ɵcmp: i0.ɵɵComponentDefWithMeta; static ɵfac: i0.ɵɵFactoryDef; } +export interface MatAutocompleteActivatedEvent { + option: MatOption | null; + source: MatAutocomplete; +} + export interface MatAutocompleteDefaultOptions { autoActiveFirstOption?: boolean; }