From 9f6393a7d3aa9108e73abd35bbc1e9b1103528ef Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Wed, 1 Feb 2017 18:40:26 -0800 Subject: [PATCH] fix(autocomplete): double-clicking input shouldnt close the panel --- src/lib/autocomplete/autocomplete-trigger.ts | 23 ++-- src/lib/autocomplete/autocomplete.spec.ts | 105 ++++++++++--------- 2 files changed, 70 insertions(+), 58 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 78d5dd9d26f4..8869723786a2 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -19,10 +19,11 @@ import {Observable} from 'rxjs/Observable'; import {MdOptionSelectEvent, MdOption} from '../core/option/option'; import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager'; import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes'; +import {Dir} from '../core/rtl/dir'; import {Subscription} from 'rxjs/Subscription'; +import {Subject} from 'rxjs/Subject'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/merge'; -import {Dir} from '../core/rtl/dir'; import 'rxjs/add/operator/startWith'; import 'rxjs/add/operator/switchMap'; @@ -59,7 +60,7 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = { '[attr.aria-expanded]': 'panelOpen.toString()', '[attr.aria-owns]': 'autocomplete?.id', '(focus)': 'openPanel()', - '(blur)': '_onTouched()', + '(blur)': '_handleBlur($event.relatedTarget?.tagName)', '(input)': '_handleInput($event.target.value)', '(keydown)': '_handleKeydown($event)', }, @@ -77,6 +78,9 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce private _keyManager: ActiveDescendantKeyManager; private _positionStrategy: ConnectedPositionStrategy; + /** Stream of blur events that should close the panel. */ + private _blurStream = new Subject(); + /** View -> model callback called when value changes */ _onChange: (value: any) => {}; @@ -132,12 +136,12 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce /** * A stream of actions that should close the autocomplete panel, including - * when an option is selected and when the backdrop is clicked. + * when an option is selected, on blur, and when TAB is pressed. */ get panelClosingActions(): Observable { return Observable.merge( ...this.optionSelections, - this._overlayRef.backdropClick(), + this._blurStream.asObservable(), this._keyManager.tabOut ); } @@ -201,6 +205,15 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce this.openPanel(); } + _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); + } + } + /** * Given that we are not actually focusing active options, we must manually adjust scroll * 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 const overlayState = new OverlayState(); overlayState.positionStrategy = this._getOverlayPosition(); overlayState.width = this._getHostWidth(); - overlayState.hasBackdrop = true; - overlayState.backdropClass = 'md-overlay-transparent-backdrop'; overlayState.direction = this._dir ? this._dir.value : 'ltr'; return overlayState; } diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 0c0ddd5e2d86..e18973986ced 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -60,6 +60,7 @@ describe('MdAutocomplete', () => { it('should open the panel when the input is focused', () => { expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel state to start out closed.`); + dispatchEvent('focus', input); fixture.detectChanges(); @@ -74,6 +75,7 @@ describe('MdAutocomplete', () => { it('should open the panel programmatically', () => { expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel state to start out closed.`); + fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); @@ -85,22 +87,18 @@ describe('MdAutocomplete', () => { .toContain('California', `Expected panel to display when opened programmatically.`); }); - it('should close the panel when a click occurs outside it', async(() => { + it('should close the panel when blurred', async(() => { dispatchEvent('focus', input); fixture.detectChanges(); fixture.whenStable().then(() => { - const backdrop = - overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; - backdrop.click(); + dispatchEvent('blur', input); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected clicking outside the panel to set its state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected clicking outside the panel to close the panel.`); - }); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected clicking outside the panel to set its state to closed.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected clicking outside the panel to close the panel.`); }); })); @@ -113,12 +111,10 @@ describe('MdAutocomplete', () => { option.click(); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected clicking an option to set the panel state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected clicking an option to close the panel.`); - }); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected clicking an option to set the panel state to closed.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected clicking an option to close the panel.`); }); })); @@ -148,31 +144,26 @@ describe('MdAutocomplete', () => { options[1].click(); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected clicking a new option to set the panel state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected clicking a new option to close the panel.`); - }); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected clicking a new option to set the panel state to closed.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected clicking a new option to close the panel.`); }); }); })); - it('should close the panel programmatically', async(() => { + it('should close the panel programmatically', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); fixture.componentInstance.trigger.closePanel(); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected closing programmatically to set the panel state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected closing programmatically to close the panel.`); - }); - - })); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected closing programmatically to set the panel state to closed.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected closing programmatically to close the panel.`); + }); it('should close the panel when the options list is empty', async(() => { dispatchEvent('focus', input); @@ -183,15 +174,13 @@ describe('MdAutocomplete', () => { input.value = 'af'; dispatchEvent('input', input); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected panel to close when options list is empty.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected panel to close when options list is empty.`); - }); + + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected panel to close when options list is empty.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected panel to close when options list is empty.`); }); })); - }); it('should have the correct text direction in RTL', () => { @@ -428,7 +417,7 @@ describe('MdAutocomplete', () => { fixture.detectChanges(); }); - it('should should not focus the option when DOWN key is pressed', async(() => { + it('should not focus the option when DOWN key is pressed', async(() => { fixture.whenStable().then(() => { spyOn(fixture.componentInstance.options.first, 'focus'); @@ -437,12 +426,26 @@ describe('MdAutocomplete', () => { }); })); + it('should not close the panel when DOWN key is pressed', async(() => { + fixture.whenStable().then(() => { + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(true, `Expected panel state to stay open when DOWN key is pressed.`); + expect(overlayContainerElement.textContent) + .toContain('Alabama', `Expected panel to keep displaying when DOWN key is pressed.`); + expect(overlayContainerElement.textContent) + .toContain('California', `Expected panel to keep displaying when DOWN key is pressed.`); + }); + })); + it('should set the active item to the first option when DOWN key is pressed', async(() => { fixture.whenStable().then(() => { const optionEls = overlayContainerElement.querySelectorAll('md-option') as NodeListOf; fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.whenStable().then(() => { fixture.detectChanges(); expect(fixture.componentInstance.trigger.activeOption) @@ -567,22 +570,20 @@ describe('MdAutocomplete', () => { fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected panel state to read closed after ENTER key.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected panel to close after ENTER key.`); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected panel state to read closed after ENTER key.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected panel to close after ENTER key.`); - input.value = 'Alabam'; - dispatchEvent('input', input); - fixture.detectChanges(); + input.value = 'Alabam'; + dispatchEvent('input', input); + fixture.detectChanges(); - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(true, `Expected panel state to read open when typing in input.`); - expect(overlayContainerElement.textContent) - .toContain('Alabama', `Expected panel to display when typing in input.`); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(true, `Expected panel state to read open when typing in input.`); + expect(overlayContainerElement.textContent) + .toContain('Alabama', `Expected panel to display when typing in input.`); }); - }); })); it('should scroll to active options below the fold', async(() => {