Skip to content

Commit 19351d3

Browse files
authored
fix(autocomplete): double-clicking input shouldnt close the panel (#2835)
1 parent 8449ef4 commit 19351d3

File tree

2 files changed

+70
-58
lines changed

2 files changed

+70
-58
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import {Observable} from 'rxjs/Observable';
1919
import {MdOptionSelectEvent, MdOption} from '../core/option/option';
2020
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';
2121
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
22+
import {Dir} from '../core/rtl/dir';
2223
import {Subscription} from 'rxjs/Subscription';
24+
import {Subject} from 'rxjs/Subject';
2325
import 'rxjs/add/observable/of';
2426
import 'rxjs/add/observable/merge';
25-
import {Dir} from '../core/rtl/dir';
2627
import 'rxjs/add/operator/startWith';
2728
import 'rxjs/add/operator/switchMap';
2829

@@ -59,7 +60,7 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
5960
'[attr.aria-expanded]': 'panelOpen.toString()',
6061
'[attr.aria-owns]': 'autocomplete?.id',
6162
'(focus)': 'openPanel()',
62-
'(blur)': '_onTouched()',
63+
'(blur)': '_handleBlur($event.relatedTarget?.tagName)',
6364
'(input)': '_handleInput($event.target.value)',
6465
'(keydown)': '_handleKeydown($event)',
6566
},
@@ -77,6 +78,9 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
7778
private _keyManager: ActiveDescendantKeyManager;
7879
private _positionStrategy: ConnectedPositionStrategy;
7980

81+
/** Stream of blur events that should close the panel. */
82+
private _blurStream = new Subject<any>();
83+
8084
/** View -> model callback called when value changes */
8185
_onChange: (value: any) => {};
8286

@@ -132,12 +136,12 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
132136

133137
/**
134138
* A stream of actions that should close the autocomplete panel, including
135-
* when an option is selected and when the backdrop is clicked.
139+
* when an option is selected, on blur, and when TAB is pressed.
136140
*/
137141
get panelClosingActions(): Observable<MdOptionSelectEvent> {
138142
return Observable.merge(
139143
...this.optionSelections,
140-
this._overlayRef.backdropClick(),
144+
this._blurStream.asObservable(),
141145
this._keyManager.tabOut
142146
);
143147
}
@@ -201,6 +205,15 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
201205
this.openPanel();
202206
}
203207

208+
_handleBlur(newlyFocusedTag: string): void {
209+
this._onTouched();
210+
211+
// Only emit blur event if the new focus is *not* on an option.
212+
if (newlyFocusedTag !== 'MD-OPTION') {
213+
this._blurStream.next(null);
214+
}
215+
}
216+
204217
/**
205218
* Given that we are not actually focusing active options, we must manually adjust scroll
206219
* to reveal options below the fold. First, we find the offset of the option from the top
@@ -283,8 +296,6 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
283296
const overlayState = new OverlayState();
284297
overlayState.positionStrategy = this._getOverlayPosition();
285298
overlayState.width = this._getHostWidth();
286-
overlayState.hasBackdrop = true;
287-
overlayState.backdropClass = 'md-overlay-transparent-backdrop';
288299
overlayState.direction = this._dir ? this._dir.value : 'ltr';
289300
return overlayState;
290301
}

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('MdAutocomplete', () => {
6060
it('should open the panel when the input is focused', () => {
6161
expect(fixture.componentInstance.trigger.panelOpen)
6262
.toBe(false, `Expected panel state to start out closed.`);
63+
6364
dispatchEvent('focus', input);
6465
fixture.detectChanges();
6566

@@ -74,6 +75,7 @@ describe('MdAutocomplete', () => {
7475
it('should open the panel programmatically', () => {
7576
expect(fixture.componentInstance.trigger.panelOpen)
7677
.toBe(false, `Expected panel state to start out closed.`);
78+
7779
fixture.componentInstance.trigger.openPanel();
7880
fixture.detectChanges();
7981

@@ -85,22 +87,18 @@ describe('MdAutocomplete', () => {
8587
.toContain('California', `Expected panel to display when opened programmatically.`);
8688
});
8789

88-
it('should close the panel when a click occurs outside it', async(() => {
90+
it('should close the panel when blurred', async(() => {
8991
dispatchEvent('focus', input);
9092
fixture.detectChanges();
9193

9294
fixture.whenStable().then(() => {
93-
const backdrop =
94-
overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
95-
backdrop.click();
95+
dispatchEvent('blur', input);
9696
fixture.detectChanges();
9797

98-
fixture.whenStable().then(() => {
99-
expect(fixture.componentInstance.trigger.panelOpen)
100-
.toBe(false, `Expected clicking outside the panel to set its state to closed.`);
101-
expect(overlayContainerElement.textContent)
102-
.toEqual('', `Expected clicking outside the panel to close the panel.`);
103-
});
98+
expect(fixture.componentInstance.trigger.panelOpen)
99+
.toBe(false, `Expected clicking outside the panel to set its state to closed.`);
100+
expect(overlayContainerElement.textContent)
101+
.toEqual('', `Expected clicking outside the panel to close the panel.`);
104102
});
105103
}));
106104

@@ -113,12 +111,10 @@ describe('MdAutocomplete', () => {
113111
option.click();
114112
fixture.detectChanges();
115113

116-
fixture.whenStable().then(() => {
117-
expect(fixture.componentInstance.trigger.panelOpen)
118-
.toBe(false, `Expected clicking an option to set the panel state to closed.`);
119-
expect(overlayContainerElement.textContent)
120-
.toEqual('', `Expected clicking an option to close the panel.`);
121-
});
114+
expect(fixture.componentInstance.trigger.panelOpen)
115+
.toBe(false, `Expected clicking an option to set the panel state to closed.`);
116+
expect(overlayContainerElement.textContent)
117+
.toEqual('', `Expected clicking an option to close the panel.`);
122118
});
123119
}));
124120

@@ -148,31 +144,26 @@ describe('MdAutocomplete', () => {
148144
options[1].click();
149145
fixture.detectChanges();
150146

151-
fixture.whenStable().then(() => {
152-
expect(fixture.componentInstance.trigger.panelOpen)
153-
.toBe(false, `Expected clicking a new option to set the panel state to closed.`);
154-
expect(overlayContainerElement.textContent)
155-
.toEqual('', `Expected clicking a new option to close the panel.`);
156-
});
147+
expect(fixture.componentInstance.trigger.panelOpen)
148+
.toBe(false, `Expected clicking a new option to set the panel state to closed.`);
149+
expect(overlayContainerElement.textContent)
150+
.toEqual('', `Expected clicking a new option to close the panel.`);
157151
});
158152
});
159153
}));
160154

161-
it('should close the panel programmatically', async(() => {
155+
it('should close the panel programmatically', () => {
162156
fixture.componentInstance.trigger.openPanel();
163157
fixture.detectChanges();
164158

165159
fixture.componentInstance.trigger.closePanel();
166160
fixture.detectChanges();
167161

168-
fixture.whenStable().then(() => {
169-
expect(fixture.componentInstance.trigger.panelOpen)
170-
.toBe(false, `Expected closing programmatically to set the panel state to closed.`);
171-
expect(overlayContainerElement.textContent)
172-
.toEqual('', `Expected closing programmatically to close the panel.`);
173-
});
174-
175-
}));
162+
expect(fixture.componentInstance.trigger.panelOpen)
163+
.toBe(false, `Expected closing programmatically to set the panel state to closed.`);
164+
expect(overlayContainerElement.textContent)
165+
.toEqual('', `Expected closing programmatically to close the panel.`);
166+
});
176167

177168
it('should close the panel when the options list is empty', async(() => {
178169
dispatchEvent('focus', input);
@@ -183,15 +174,13 @@ describe('MdAutocomplete', () => {
183174
input.value = 'af';
184175
dispatchEvent('input', input);
185176
fixture.detectChanges();
186-
fixture.whenStable().then(() => {
187-
expect(fixture.componentInstance.trigger.panelOpen)
188-
.toBe(false, `Expected panel to close when options list is empty.`);
189-
expect(overlayContainerElement.textContent)
190-
.toEqual('', `Expected panel to close when options list is empty.`);
191-
});
177+
178+
expect(fixture.componentInstance.trigger.panelOpen)
179+
.toBe(false, `Expected panel to close when options list is empty.`);
180+
expect(overlayContainerElement.textContent)
181+
.toEqual('', `Expected panel to close when options list is empty.`);
192182
});
193183
}));
194-
195184
});
196185

197186
it('should have the correct text direction in RTL', () => {
@@ -428,7 +417,7 @@ describe('MdAutocomplete', () => {
428417
fixture.detectChanges();
429418
});
430419

431-
it('should should not focus the option when DOWN key is pressed', async(() => {
420+
it('should not focus the option when DOWN key is pressed', async(() => {
432421
fixture.whenStable().then(() => {
433422
spyOn(fixture.componentInstance.options.first, 'focus');
434423

@@ -437,12 +426,26 @@ describe('MdAutocomplete', () => {
437426
});
438427
}));
439428

429+
it('should not close the panel when DOWN key is pressed', async(() => {
430+
fixture.whenStable().then(() => {
431+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
432+
433+
expect(fixture.componentInstance.trigger.panelOpen)
434+
.toBe(true, `Expected panel state to stay open when DOWN key is pressed.`);
435+
expect(overlayContainerElement.textContent)
436+
.toContain('Alabama', `Expected panel to keep displaying when DOWN key is pressed.`);
437+
expect(overlayContainerElement.textContent)
438+
.toContain('California', `Expected panel to keep displaying when DOWN key is pressed.`);
439+
});
440+
}));
441+
440442
it('should set the active item to the first option when DOWN key is pressed', async(() => {
441443
fixture.whenStable().then(() => {
442444
const optionEls =
443445
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
444446

445447
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
448+
446449
fixture.whenStable().then(() => {
447450
fixture.detectChanges();
448451
expect(fixture.componentInstance.trigger.activeOption)
@@ -567,22 +570,20 @@ describe('MdAutocomplete', () => {
567570
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
568571
fixture.detectChanges();
569572

570-
fixture.whenStable().then(() => {
571-
expect(fixture.componentInstance.trigger.panelOpen)
572-
.toBe(false, `Expected panel state to read closed after ENTER key.`);
573-
expect(overlayContainerElement.textContent)
574-
.toEqual('', `Expected panel to close after ENTER key.`);
573+
expect(fixture.componentInstance.trigger.panelOpen)
574+
.toBe(false, `Expected panel state to read closed after ENTER key.`);
575+
expect(overlayContainerElement.textContent)
576+
.toEqual('', `Expected panel to close after ENTER key.`);
575577

576-
input.value = 'Alabam';
577-
dispatchEvent('input', input);
578-
fixture.detectChanges();
578+
input.value = 'Alabam';
579+
dispatchEvent('input', input);
580+
fixture.detectChanges();
579581

580-
expect(fixture.componentInstance.trigger.panelOpen)
581-
.toBe(true, `Expected panel state to read open when typing in input.`);
582-
expect(overlayContainerElement.textContent)
583-
.toContain('Alabama', `Expected panel to display when typing in input.`);
582+
expect(fixture.componentInstance.trigger.panelOpen)
583+
.toBe(true, `Expected panel state to read open when typing in input.`);
584+
expect(overlayContainerElement.textContent)
585+
.toContain('Alabama', `Expected panel to display when typing in input.`);
584586
});
585-
});
586587
}));
587588

588589
it('should scroll to active options below the fold', async(() => {

0 commit comments

Comments
 (0)