Skip to content

Commit

Permalink
fix(autocomplete): unable to click to select items in IE
Browse files Browse the repository at this point in the history
* Fixes being unable to select autocomplete items by clicking in IE. This was due to IE not setting the event.relatedTarget for blur events.
* Fixes potential issue if the user uses mat-option, instead of md-option.

Fixes #3351.
  • Loading branch information
crisbeto committed Apr 12, 2017
1 parent 9d719c5 commit b6ac2a9
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 33 deletions.
70 changes: 41 additions & 29 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import {
Directive,
ElementRef,
forwardRef,
Host,
Input,
NgZone,
Optional,
OnDestroy,
ViewContainerRef,
Directive,
ElementRef,
forwardRef,
Host,
Input,
NgZone,
Optional,
OnDestroy,
ViewContainerRef,
Inject,
ChangeDetectorRef,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {DOCUMENT} from '@angular/platform-browser';
import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core';
import {MdAutocomplete} from './autocomplete';
import {PositionStrategy} from '../core/overlay/position/position-strategy';
Expand All @@ -18,12 +21,13 @@ import {Observable} from 'rxjs/Observable';
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
import {Dir} from '../core/rtl/dir';
import {MdInputContainer} from '../input/input-container';
import {Subscription} from 'rxjs/Subscription';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/switchMap';
import {MdInputContainer} from '../input/input-container';

/**
* The following style constants are necessary to save here in order
Expand Down Expand Up @@ -58,8 +62,8 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
'[attr.aria-expanded]': 'panelOpen.toString()',
'[attr.aria-owns]': 'autocomplete?.id',
'(focus)': 'openPanel()',
'(blur)': '_handleBlur($event.relatedTarget?.tagName)',
'(input)': '_handleInput($event)',
'(blur)': '_onTouched()',
'(keydown)': '_handleKeydown($event)',
},
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
Expand All @@ -74,9 +78,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

private _positionStrategy: ConnectedPositionStrategy;

/** Stream of blur events that should close the panel. */
private _blurStream = new Subject<any>();

/** Whether or not the placeholder state is being overridden. */
private _manuallyFloatingPlaceholder = false;

Expand All @@ -101,8 +102,10 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

constructor(private _element: ElementRef, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir: Dir, private _zone: NgZone,
@Optional() @Host() private _inputContainer: MdInputContainer) {}
@Optional() @Host() private _inputContainer: MdInputContainer,
@Optional() @Inject(DOCUMENT) private _document: any) {}

ngOnDestroy() {
if (this._panelPositionSubscription) {
Expand Down Expand Up @@ -144,6 +147,12 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

this._panelOpen = false;
this._resetPlaceholder();

// We need to trigger change detection manually, because
// `fromEvent` doesn't seem to do it at the proper time.
// This ensures that the placeholder is reset when the
// user clicks outside.
this._changeDetectorRef.detectChanges();
}

/**
Expand All @@ -152,9 +161,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
*/
get panelClosingActions(): Observable<MdOptionSelectionChange> {
return Observable.merge(
this.optionSelections,
this._blurStream.asObservable(),
this.autocomplete._keyManager.tabOut
this.optionSelections,
this.autocomplete._keyManager.tabOut,
this._outsideClickStream
);
}

Expand All @@ -170,6 +179,18 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
}
}

/** Stream of click outside of the autocomplete panel. */
private get _outsideClickStream(): Observable<any> {
if (this._document) {
return Observable.fromEvent(this._document, 'click').filter((event: MouseEvent) => {
let clickTarget = event.target as HTMLElement;
return this._panelOpen &&
!this._inputContainer._elementRef.nativeElement.contains(clickTarget) &&
!this._overlayRef.overlayElement.contains(clickTarget);
});
}
}

/**
* Sets the autocomplete's value. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
Expand Down Expand Up @@ -225,15 +246,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
}
}

_handleBlur(newlyFocusedTag: string): void {
this._onTouched();

// Only emit blur event if the new focus is *not* on an option.
if (newlyFocusedTag !== 'MD-OPTION') {
this._blurStream.next(null);
}
}

/**
* In "auto" mode, the placeholder will animate down as soon as focus is lost.
* This causes the value to jump when selecting an option with the mouse.
Expand Down Expand Up @@ -307,7 +319,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
* stemmed from the user.
*/
private _setValueAndClose(event: MdOptionSelectionChange | null): void {
if (event) {
if (event && event.source) {
this._clearPreviousSelectedOption(event.source);
this._setTriggerValue(event.source.value);
this._onChange(event.source.value);
Expand Down
5 changes: 2 additions & 3 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,12 @@ describe('MdAutocomplete', () => {
});
}));

it('should close the panel when blurred', async(() => {
it('should close the panel when input loses focus', async(() => {
dispatchFakeEvent(input, 'focus');
fixture.detectChanges();

fixture.whenStable().then(() => {
dispatchFakeEvent(input, 'blur');
fixture.detectChanges();
dispatchFakeEvent(document, 'click');

expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected clicking outside the panel to set its state to closed.`);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/testing/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function createKeyboardEvent(type: string, keyCode: number) {

/** Creates a fake event object with any desired event type. */
export function createFakeEvent(type: string) {
let event = document.createEvent('Event');
let event = document.createEvent('Event');
event.initEvent(type, true, true);
return event;
}
1 change: 1 addition & 0 deletions src/lib/input/input-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit {
@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;

constructor(
public _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _parentForm: NgForm,
@Optional() private _parentFormGroup: FormGroupDirective) { }
Expand Down

0 comments on commit b6ac2a9

Please sign in to comment.