From 8bfe58c2a79120cbdcf49b64897672057f857680 Mon Sep 17 00:00:00 2001 From: Will Abson Date: Tue, 25 Oct 2016 18:08:57 +0100 Subject: [PATCH] New implementation of focus/blur event management using Obserable Refs #371 --- .../assets/alfresco-search.component.mock.ts | 76 +++++++++++ ...lfresco-search-autocomplete.component.html | 4 +- ...esco-search-autocomplete.component.spec.ts | 124 +++++++++--------- .../alfresco-search-autocomplete.component.ts | 21 +-- .../alfresco-search-control.component.html | 3 +- .../alfresco-search-control.component.spec.ts | 75 +++++++---- .../alfresco-search-control.component.ts | 52 ++++++-- 7 files changed, 241 insertions(+), 114 deletions(-) create mode 100644 ng2-components/ng2-alfresco-search/src/assets/alfresco-search.component.mock.ts diff --git a/ng2-components/ng2-alfresco-search/src/assets/alfresco-search.component.mock.ts b/ng2-components/ng2-alfresco-search/src/assets/alfresco-search.component.mock.ts new file mode 100644 index 0000000000..32975ae655 --- /dev/null +++ b/ng2-components/ng2-alfresco-search/src/assets/alfresco-search.component.mock.ts @@ -0,0 +1,76 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export var result = { + list: { + entries: [ + { + entry: { + id: '123', + name: 'MyDoc', + isFile : true, + content: { + mimeType: 'text/plain' + }, + createdByUser: { + displayName: 'John Doe' + }, + modifiedByUser: { + displayName: 'John Doe' + } + } + } + ] + } +}; + +export var folderResult = { + list: { + entries: [ + { + entry: { + id: '123', + name: 'MyFolder', + isFile : false, + isFolder : true, + createdByUser: { + displayName: 'John Doe' + }, + modifiedByUser: { + displayName: 'John Doe' + } + } + } + ] + } +}; + +export var noResult = { + list: { + entries: [] + } +}; + +export var errorJson = { + error: { + errorKey: 'Search failed', + statusCode: 400, + briefSummary: '08220082 search failed', + stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.', + descriptionURL: 'https://api-explorer.alfresco.com' + } +}; diff --git a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.html b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.html index bbb1b5ea82..52a4ddaeb3 100644 --- a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.html +++ b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.html @@ -1,7 +1,7 @@ - - +
{{getMimeTypeKey(result)|translate}} diff --git a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.spec.ts b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.spec.ts index d88a184b92..5c0324e498 100644 --- a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.spec.ts +++ b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.spec.ts @@ -19,6 +19,7 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { AlfrescoSearchAutocompleteComponent } from './alfresco-search-autocomplete.component'; import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service'; import { TranslationMock } from './../assets/translation.service.mock'; +import { result, folderResult, noResult, errorJson } from './../assets/alfresco-search.component.mock'; import { AlfrescoSearchService } from '../services/alfresco-search.service'; import { AlfrescoApiService, @@ -34,66 +35,6 @@ describe('AlfrescoSearchAutocompleteComponent', () => { let fixture: ComponentFixture, element: HTMLElement; let component: AlfrescoSearchAutocompleteComponent; - let result = { - list: { - entries: [ - { - entry: { - id: '123', - name: 'MyDoc', - isFile : true, - content: { - mimeType: 'text/plain' - }, - createdByUser: { - displayName: 'John Doe' - }, - modifiedByUser: { - displayName: 'John Doe' - } - } - } - ] - } - }; - - let folderResult = { - list: { - entries: [ - { - entry: { - id: '123', - name: 'MyFolder', - isFile : false, - isFolder : true, - createdByUser: { - displayName: 'John Doe' - }, - modifiedByUser: { - displayName: 'John Doe' - } - } - } - ] - } - }; - - let noResult = { - list: { - entries: [] - } - }; - - let errorJson = { - error: { - errorKey: 'Search failed', - statusCode: 400, - briefSummary: '08220082 search failed', - stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.', - descriptionURL: 'https://api-explorer.alfresco.com' - } - }; - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -286,4 +227,67 @@ describe('AlfrescoSearchAutocompleteComponent', () => { component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); }); + it('should emit preview when enter key pressed when a file item is in focus', (done) => { + + let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + component.resultsEmitter.subscribe(x => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).dispatchEvent(new KeyboardEvent('keyup', { + key: 'Enter' + })); + }); + + component.searchTerm = 'searchTerm'; + component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); + + component.preview.subscribe(e => { + done(); + }); + }); + + it('should emit a focus event when a result comes into focus', (done) => { + + let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + component.resultsEmitter.subscribe(x => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('focus')); + }); + + component.searchTerm = 'searchTerm'; + component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); + + component.focusEmitter.subscribe((e: FocusEvent) => { + expect(e).not.toBeNull(); + expect(e.type).toBe('focus'); + done(); + }); + }); + + it('should emit a focus event when a result loses focus', (done) => { + + let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + component.resultsEmitter.subscribe(x => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('blur')); + }); + + component.searchTerm = 'searchTerm'; + component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); + + component.focusEmitter.subscribe((e: FocusEvent) => { + expect(e).not.toBeNull(); + expect(e.type).toBe('blur'); + done(); + }); + }); + }); diff --git a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.ts b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.ts index a5bccf9a55..71eb6411a1 100644 --- a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.ts +++ b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-autocomplete.component.ts @@ -44,7 +44,7 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges { preview: EventEmitter = new EventEmitter(); @Output() - blurEmitter: EventEmitter = new EventEmitter(); + focusEmitter: EventEmitter = new EventEmitter(); @Output() resultsEmitter = new EventEmitter(); @@ -132,19 +132,12 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges { } } - onRowBlur(node): void { - window.setTimeout(() => { - let focusedEl = document.activeElement; - if (focusedEl && focusedEl.id && focusedEl.id.indexOf('result_row_') === 0) { - return; - } - this.blurEmitter.emit(node); - }, 100); - console.log('row blur', node); + onRowFocus($event: FocusEvent): void { + this.focusEmitter.emit($event); } - onRowFocus(node): void { - console.log('row focus', node); + onRowBlur($event: FocusEvent): void { + this.focusEmitter.emit($event); } onRowEnter(node): void { @@ -157,8 +150,4 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges { } } - onFocusOut(): void { - console.log('onfocusout'); - } - } diff --git a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.html b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.html index b175a5926c..36236f9dfd 100644 --- a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.html +++ b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.html @@ -24,4 +24,5 @@ + (preview)="onFileClicked($event)" + (focusEmitter)="onAutoCompleteFocus($event)"> diff --git a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.spec.ts b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.spec.ts index b8321de775..76eee29fe3 100644 --- a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.spec.ts +++ b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.spec.ts @@ -20,6 +20,7 @@ import { AlfrescoSearchControlComponent } from './alfresco-search-control.compon import { AlfrescoSearchAutocompleteComponent } from './alfresco-search-autocomplete.component'; import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service'; import { TranslationMock } from './../assets/translation.service.mock'; +import { result } from './../assets/alfresco-search.component.mock'; import { AlfrescoSettingsService, AlfrescoApiService, @@ -150,24 +151,45 @@ describe('AlfrescoSearchControlComponent', () => { expect(autocomplete.classList.contains('active')).toBe(false); }); - it('should make find-as-you-type control visible when search box has focus', () => { + it('should make find-as-you-type control visible when search box has focus', (done) => { fixture.detectChanges(); - inputEl.dispatchEvent(new Event('focus')); - fixture.detectChanges(); - let autocomplete: Element = element.querySelector('alfresco-search-autocomplete'); - expect(autocomplete.classList.contains('active')).toBe(true); + inputEl.dispatchEvent(new FocusEvent('focus')); + window.setTimeout(() => { // wait for debounce() to complete + fixture.detectChanges(); + let autocomplete: Element = element.querySelector('alfresco-search-autocomplete'); + expect(autocomplete.classList.contains('active')).toBe(true); + done(); + }, 100); }); it('should hide find-as-you-type results when the search box loses focus', (done) => { fixture.detectChanges(); - inputEl.dispatchEvent(new Event('focus')); - inputEl.dispatchEvent(new Event('blur')); + inputEl.dispatchEvent(new FocusEvent('focus')); + inputEl.dispatchEvent(new FocusEvent('blur')); window.setTimeout(() => { fixture.detectChanges(); let autocomplete: Element = element.querySelector('alfresco-search-autocomplete'); expect(autocomplete.classList.contains('active')).toBe(false); done(); - }, 250); + }, 100); + }); + + it('should keep find-as-you-type control visible when user tabs into results', (done) => { + let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + fixture.detectChanges(); + inputEl.dispatchEvent(new FocusEvent('focus')); + fixture.detectChanges(); + inputEl.dispatchEvent(new FocusEvent('blur')); + component.onAutoCompleteFocus(new FocusEvent('focus')); + window.setTimeout(() => { // wait for debounce() to complete + fixture.detectChanges(); + let autocomplete: Element = element.querySelector('alfresco-search-autocomplete'); + expect(autocomplete.classList.contains('active')).toBe(true); + done(); + }, 100); }); it('should hide find-as-you-type results when escape key pressed', () => { @@ -239,32 +261,41 @@ describe('AlfrescoSearchControlComponent', () => { describe('component focus', () => { - it('should fire an event when the search box receives focus', () => { + it('should fire an event when the search box receives focus', (done) => { spyOn(component.expand, 'emit'); let inputEl: HTMLElement = element.querySelector('input'); - inputEl.dispatchEvent(new Event('focus')); - expect(component.expand.emit).toHaveBeenCalledWith({ - expanded: true - }); + inputEl.dispatchEvent(new FocusEvent('focus')); + window.setTimeout(() => { + expect(component.expand.emit).toHaveBeenCalledWith({ + expanded: true + }); + done(); + }, 100); }); - it('should fire an event when the search box loses focus', () => { + it('should fire an event when the search box loses focus', (done) => { spyOn(component.expand, 'emit'); let inputEl: HTMLElement = element.querySelector('input'); - inputEl.dispatchEvent(new Event('blur')); - expect(component.expand.emit).toHaveBeenCalledWith({ - expanded: false - }); + inputEl.dispatchEvent(new FocusEvent('blur')); + window.setTimeout(() => { + expect(component.expand.emit).toHaveBeenCalledWith({ + expanded: false + }); + done(); + }, 100); }); it('should NOT fire an event when the search box receives/loses focus but the component is not expandable', - () => { + (done) => { spyOn(component.expand, 'emit'); component.expandable = false; let inputEl: HTMLElement = element.querySelector('input'); - inputEl.dispatchEvent(new Event('focus')); - inputEl.dispatchEvent(new Event('blur')); - expect(component.expand.emit).not.toHaveBeenCalled(); + inputEl.dispatchEvent(new FocusEvent('focus')); + inputEl.dispatchEvent(new FocusEvent('blur')); + window.setTimeout(() => { + expect(component.expand.emit).not.toHaveBeenCalled(); + done(); + }, 100); }); }); diff --git a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.ts b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.ts index d51e198219..ad6284c79f 100644 --- a/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.ts +++ b/ng2-components/ng2-alfresco-search/src/components/alfresco-search-control.component.ts @@ -16,9 +16,10 @@ */ import { FormControl, Validators } from '@angular/forms'; -import { Component, Input, Output, OnInit, ElementRef, EventEmitter, ViewChild } from '@angular/core'; +import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ViewChild } from '@angular/core'; import { AlfrescoTranslationService } from 'ng2-alfresco-core'; import { SearchTermValidator } from './../forms/search-term-validator'; +import { Observable, Subject } from 'rxjs/Rx'; @Component({ moduleId: module.id, @@ -26,7 +27,7 @@ import { SearchTermValidator } from './../forms/search-term-validator'; templateUrl: './alfresco-search-control.component.html', styleUrls: ['./alfresco-search-control.component.css'] }) -export class AlfrescoSearchControlComponent implements OnInit { +export class AlfrescoSearchControlComponent implements OnInit, OnDestroy { @Input() searchTerm = ''; @@ -66,6 +67,8 @@ export class AlfrescoSearchControlComponent implements OnInit { searchValid = false; + private focusSubject = new Subject(); + constructor(private translate: AlfrescoTranslationService) { this.searchControl = new FormControl( @@ -80,9 +83,16 @@ export class AlfrescoSearchControlComponent implements OnInit { this.onSearchTermChange(value); } ); + + this.setupFocusEventHandlers(); + this.translate.addTranslationFolder('node_modules/ng2-alfresco-search/dist/src'); } + ngOnDestroy(): void { + this.focusSubject.unsubscribe(); + } + private onSearchTermChange(value: string): void { this.searchActive = true; this.autocompleteSearchTerm = value; @@ -94,6 +104,20 @@ export class AlfrescoSearchControlComponent implements OnInit { }); } + private setupFocusEventHandlers() { + let focusEvents: Observable = this.focusSubject.asObservable().debounceTime(50); + focusEvents.filter(($event: FocusEvent) => { + return $event.type === 'focusin' || $event.type === 'focus'; + }).subscribe(($event) => { + this.onSearchFocus($event); + }); + focusEvents.filter(($event: any) => { + return $event.type === 'focusout' || $event.type === 'blur'; + }).subscribe(($event) => { + this.onSearchBlur($event); + }); + } + getTextFieldClassName(): string { return 'mdl-textfield mdl-js-textfield' + (this.expandable ? ' mdl-textfield--expandable' : ''); } @@ -127,28 +151,30 @@ export class AlfrescoSearchControlComponent implements OnInit { }); } - onFocus(): void { + onSearchFocus($event): void { this.searchActive = true; + } + + onSearchBlur($event): void { + this.searchActive = false; + } + + onFocus($event): void { if (this.expandable) { this.expand.emit({ expanded: true }); } + this.focusSubject.next($event); } - onBlur(): void { - window.setTimeout(() => { - let focusedEl = document.activeElement; - if (focusedEl && focusedEl.id && focusedEl.id.indexOf('result_row_') === 0) { - return; - } - this.searchActive = false; - }, 200); + onBlur($event): void { if (this.expandable && (this.searchControl.value === '' || this.searchControl.value === undefined)) { this.expand.emit({ expanded: false }); } + this.focusSubject.next($event); } onEscape(): void { @@ -159,8 +185,8 @@ export class AlfrescoSearchControlComponent implements OnInit { this.searchActive = true; } - onAutoCompleteBlur(): void { - this.searchActive = false; + onAutoCompleteFocus($event): void { + this.focusSubject.next($event); } }