Skip to content

Commit 56ac940

Browse files
committed
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.
1 parent 9d719c5 commit 56ac940

File tree

4 files changed

+47
-34
lines changed

4 files changed

+47
-34
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import {
2-
Directive,
3-
ElementRef,
4-
forwardRef,
5-
Host,
6-
Input,
7-
NgZone,
8-
Optional,
9-
OnDestroy,
10-
ViewContainerRef,
2+
Directive,
3+
ElementRef,
4+
forwardRef,
5+
Host,
6+
Input,
7+
NgZone,
8+
Optional,
9+
OnDestroy,
10+
ViewContainerRef,
11+
Renderer,
12+
Inject,
13+
ChangeDetectorRef,
1114
} from '@angular/core';
1215
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
16+
import {DOCUMENT} from '@angular/platform-browser';
1317
import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core';
1418
import {MdAutocomplete} from './autocomplete';
1519
import {PositionStrategy} from '../core/overlay/position/position-strategy';
@@ -18,12 +22,13 @@ import {Observable} from 'rxjs/Observable';
1822
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
1923
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
2024
import {Dir} from '../core/rtl/dir';
25+
import {MdInputContainer} from '../input/input-container';
2126
import {Subscription} from 'rxjs/Subscription';
22-
import {Subject} from 'rxjs/Subject';
2327
import 'rxjs/add/observable/merge';
28+
import 'rxjs/add/observable/fromEvent';
29+
import 'rxjs/add/operator/filter';
2430
import 'rxjs/add/operator/startWith';
2531
import 'rxjs/add/operator/switchMap';
26-
import {MdInputContainer} from '../input/input-container';
2732

2833
/**
2934
* The following style constants are necessary to save here in order
@@ -58,8 +63,8 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
5863
'[attr.aria-expanded]': 'panelOpen.toString()',
5964
'[attr.aria-owns]': 'autocomplete?.id',
6065
'(focus)': 'openPanel()',
61-
'(blur)': '_handleBlur($event.relatedTarget?.tagName)',
6266
'(input)': '_handleInput($event)',
67+
'(blur)': '_onTouched()',
6368
'(keydown)': '_handleKeydown($event)',
6469
},
6570
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
@@ -74,9 +79,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
7479

7580
private _positionStrategy: ConnectedPositionStrategy;
7681

77-
/** Stream of blur events that should close the panel. */
78-
private _blurStream = new Subject<any>();
79-
8082
/** Whether or not the placeholder state is being overridden. */
8183
private _manuallyFloatingPlaceholder = false;
8284

@@ -100,9 +102,11 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
100102
}
101103

102104
constructor(private _element: ElementRef, private _overlay: Overlay,
103-
private _viewContainerRef: ViewContainerRef,
105+
private _renderer: Renderer, private _viewContainerRef: ViewContainerRef,
106+
private _changeDetectorRef: ChangeDetectorRef,
104107
@Optional() private _dir: Dir, private _zone: NgZone,
105-
@Optional() @Host() private _inputContainer: MdInputContainer) {}
108+
@Optional() @Host() private _inputContainer: MdInputContainer,
109+
@Optional() @Inject(DOCUMENT) private _document: any) {}
106110

107111
ngOnDestroy() {
108112
if (this._panelPositionSubscription) {
@@ -144,6 +148,12 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
144148

145149
this._panelOpen = false;
146150
this._resetPlaceholder();
151+
152+
// We need to trigger change detection manually, because
153+
// `fromEvent` doesn't seem to do it at the proper time.
154+
// This ensures that the placeholder is reset when the
155+
// user clicks outside.
156+
this._changeDetectorRef.detectChanges();
147157
}
148158

149159
/**
@@ -152,9 +162,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
152162
*/
153163
get panelClosingActions(): Observable<MdOptionSelectionChange> {
154164
return Observable.merge(
155-
this.optionSelections,
156-
this._blurStream.asObservable(),
157-
this.autocomplete._keyManager.tabOut
165+
this.optionSelections,
166+
this.autocomplete._keyManager.tabOut,
167+
this._outsideClickStream
158168
);
159169
}
160170

@@ -170,6 +180,18 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
170180
}
171181
}
172182

183+
/** Stream of click outside of the autocomplete panel. */
184+
private get _outsideClickStream(): Observable<any> {
185+
if (this._document) {
186+
return Observable.fromEvent(this._document, 'click').filter((event: MouseEvent) => {
187+
let clickTarget = event.target as HTMLElement;
188+
return this._panelOpen &&
189+
!this._inputContainer._elementRef.nativeElement.contains(clickTarget) &&
190+
!this._overlayRef.overlayElement.contains(clickTarget);
191+
});
192+
}
193+
}
194+
173195
/**
174196
* Sets the autocomplete's value. Part of the ControlValueAccessor interface
175197
* required to integrate with Angular's core forms API.
@@ -225,15 +247,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
225247
}
226248
}
227249

228-
_handleBlur(newlyFocusedTag: string): void {
229-
this._onTouched();
230-
231-
// Only emit blur event if the new focus is *not* on an option.
232-
if (newlyFocusedTag !== 'MD-OPTION') {
233-
this._blurStream.next(null);
234-
}
235-
}
236-
237250
/**
238251
* In "auto" mode, the placeholder will animate down as soon as focus is lost.
239252
* This causes the value to jump when selecting an option with the mouse.
@@ -307,7 +320,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
307320
* stemmed from the user.
308321
*/
309322
private _setValueAndClose(event: MdOptionSelectionChange | null): void {
310-
if (event) {
323+
if (event && event.source) {
311324
this._clearPreviousSelectedOption(event.source);
312325
this._setTriggerValue(event.source.value);
313326
this._onChange(event.source.value);

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,12 @@ describe('MdAutocomplete', () => {
121121
});
122122
}));
123123

124-
it('should close the panel when blurred', async(() => {
124+
it('should close the panel when input loses focus', async(() => {
125125
dispatchFakeEvent(input, 'focus');
126126
fixture.detectChanges();
127127

128128
fixture.whenStable().then(() => {
129-
dispatchFakeEvent(input, 'blur');
130-
fixture.detectChanges();
129+
dispatchFakeEvent(document, 'click');
131130

132131
expect(fixture.componentInstance.trigger.panelOpen)
133132
.toBe(false, `Expected clicking outside the panel to set its state to closed.`);

src/lib/core/testing/event-objects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function createKeyboardEvent(type: string, keyCode: number) {
4040

4141
/** Creates a fake event object with any desired event type. */
4242
export function createFakeEvent(type: string) {
43-
let event = document.createEvent('Event');
43+
let event = document.createEvent('Event');
4444
event.initEvent(type, true, true);
4545
return event;
4646
}

src/lib/input/input-container.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit {
339339
@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;
340340

341341
constructor(
342+
public _elementRef: ElementRef,
342343
private _changeDetectorRef: ChangeDetectorRef,
343344
@Optional() private _parentForm: NgForm,
344345
@Optional() private _parentFormGroup: FormGroupDirective) { }

0 commit comments

Comments
 (0)