From b6ac2a9def6330432d7c9f9868c904ebb4e95fe5 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 16 Mar 2017 22:18:36 +0100 Subject: [PATCH] fix(autocomplete): unable to click to select items in IE * 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. --- src/lib/autocomplete/autocomplete-trigger.ts | 70 ++++++++++++-------- src/lib/autocomplete/autocomplete.spec.ts | 5 +- src/lib/core/testing/event-objects.ts | 2 +- src/lib/input/input-container.ts | 1 + 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 22dd34265699..97446e402aaf 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -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'; @@ -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 @@ -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] @@ -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(); - /** Whether or not the placeholder state is being overridden. */ private _manuallyFloatingPlaceholder = false; @@ -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) { @@ -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(); } /** @@ -152,9 +161,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { */ get panelClosingActions(): Observable { return Observable.merge( - this.optionSelections, - this._blurStream.asObservable(), - this.autocomplete._keyManager.tabOut + this.optionSelections, + this.autocomplete._keyManager.tabOut, + this._outsideClickStream ); } @@ -170,6 +179,18 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } } + /** Stream of click outside of the autocomplete panel. */ + private get _outsideClickStream(): Observable { + 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. @@ -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. @@ -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); diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index f7ca92a28b8a..7b2108b5b749 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -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.`); diff --git a/src/lib/core/testing/event-objects.ts b/src/lib/core/testing/event-objects.ts index 6bc030b5734a..909ac27c773c 100644 --- a/src/lib/core/testing/event-objects.ts +++ b/src/lib/core/testing/event-objects.ts @@ -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; } diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 7df88b4de348..470032a12616 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -339,6 +339,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit { @ContentChildren(MdSuffix) _suffixChildren: QueryList; constructor( + public _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, @Optional() private _parentForm: NgForm, @Optional() private _parentFormGroup: FormGroupDirective) { }