Skip to content

Commit

Permalink
feat(autocomplete): add event when option is activated (#18387)
Browse files Browse the repository at this point in the history
Exposes an event that emits whenever an autocomplete option is activated using the keyboard.

Fixes #17587.
  • Loading branch information
crisbeto authored Feb 20, 2020
1 parent 35421c8 commit 4629e23
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 3 deletions.
49 changes: 49 additions & 0 deletions src/material/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -2978,3 +3006,24 @@ class AutocompleteWithNativeAutocompleteAttribute {
})
class InputWithoutAutocompleteAndDisabled {
}


@Component({
template: `
<mat-form-field>
<input matInput [matAutocomplete]="auto">
</mat-form-field>
<mat-autocomplete #auto="matAutocomplete" (optionActivated)="optionActivated($event)">
<mat-option *ngFor="let state of states" [value]="state">{{ state }}</mat-option>
</mat-autocomplete>
`
})
class AutocompleteWithActivatedEvent {
states = ['California', 'West Virginia', 'Florida'];
optionActivated = jasmine.createSpy('optionActivated callback');

@ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
@ViewChild(MatAutocomplete) autocomplete: MatAutocomplete;
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
25 changes: 24 additions & 1 deletion src/material/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
TemplateRef,
ViewChild,
ViewEncapsulation,
OnDestroy,
} from '@angular/core';
import {
CanDisableRipple,
Expand All @@ -33,6 +34,7 @@ import {
MatOption,
mixinDisableRipple,
} from '@angular/material/core';
import {Subscription} from 'rxjs';


/**
Expand All @@ -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 */
Expand Down Expand Up @@ -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<MatOption>;
Expand Down Expand Up @@ -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<void> = new EventEmitter<void>();

/** Emits whenever an option is activated using the keyboard. */
@Output() readonly optionActivated: EventEmitter<MatAutocompleteActivatedEvent> =
new EventEmitter<MatAutocompleteActivatedEvent>();

/**
* Takes classes set on the host mat-autocomplete element and applies them to the panel
* inside the overlay container to allow for easy styling.
Expand Down Expand Up @@ -183,10 +198,18 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC

ngAfterContentInit() {
this._keyManager = new ActiveDescendantKeyManager<MatOption>(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.
Expand Down
11 changes: 9 additions & 2 deletions tools/public_api_guard/material/autocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -34,6 +34,7 @@ export declare class MatAutocomplete extends _MatAutocompleteMixinBase implement
id: string;
get isOpen(): boolean;
readonly opened: EventEmitter<void>;
readonly optionActivated: EventEmitter<MatAutocompleteActivatedEvent>;
optionGroups: QueryList<MatOptgroup>;
readonly optionSelected: EventEmitter<MatAutocompleteSelectedEvent>;
options: QueryList<MatOption>;
Expand All @@ -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<MatAutocomplete, "mat-autocomplete", ["matAutocomplete"], { "disableRipple": "disableRipple"; "displayWith": "displayWith"; "autoActiveFirstOption": "autoActiveFirstOption"; "panelWidth": "panelWidth"; "classList": "class"; }, { "optionSelected": "optionSelected"; "opened": "opened"; "closed": "closed"; }, ["options", "optionGroups"]>;
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatAutocomplete, "mat-autocomplete", ["matAutocomplete"], { "disableRipple": "disableRipple"; "displayWith": "displayWith"; "autoActiveFirstOption": "autoActiveFirstOption"; "panelWidth": "panelWidth"; "classList": "class"; }, { "optionSelected": "optionSelected"; "opened": "opened"; "closed": "closed"; "optionActivated": "optionActivated"; }, ["options", "optionGroups"]>;
static ɵfac: i0.ɵɵFactoryDef<MatAutocomplete>;
}

export interface MatAutocompleteActivatedEvent {
option: MatOption | null;
source: MatAutocomplete;
}

export interface MatAutocompleteDefaultOptions {
autoActiveFirstOption?: boolean;
}
Expand Down

0 comments on commit 4629e23

Please sign in to comment.