From a8bea1800b161e659f14d369596d987f31cf8484 Mon Sep 17 00:00:00 2001 From: Will Abson Date: Wed, 26 Oct 2016 12:56:42 +0100 Subject: [PATCH] Allow use of arrow keys to navigate FAYT results and update tests Refs #371 --- .../assets/alfresco-search.component.mock.ts | 44 +- ...lfresco-search-autocomplete.component.html | 12 +- ...esco-search-autocomplete.component.spec.ts | 485 +++++++++++------- .../alfresco-search-autocomplete.component.ts | 48 +- .../alfresco-search-control.component.html | 6 +- .../alfresco-search-control.component.spec.ts | 36 ++ .../alfresco-search-control.component.ts | 39 +- 7 files changed, 453 insertions(+), 217 deletions(-) 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 index 32975ae655..edd563beb7 100644 --- 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 @@ -15,25 +15,37 @@ * limitations under the License. */ +const entryItem = { + entry: { + id: '123', + name: 'MyDoc', + isFile : true, + content: { + mimeType: 'text/plain' + }, + createdByUser: { + displayName: 'John Doe' + }, + modifiedByUser: { + displayName: 'John Doe' + } + } +}; + export var result = { list: { entries: [ - { - entry: { - id: '123', - name: 'MyDoc', - isFile : true, - content: { - mimeType: 'text/plain' - }, - createdByUser: { - displayName: 'John Doe' - }, - modifiedByUser: { - displayName: 'John Doe' - } - } - } + entryItem + ] + } +}; + +export var results = { + list: { + entries: [ + entryItem, + entryItem, + entryItem ] } }; 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 112efa8329..af3d623fc0 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,8 +1,14 @@ - - + +
{{getMimeTypeKey(result)|translate}}
{{result.entry.name}}
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 5c0324e498..684e4008ad 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,7 +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 { result, results, folderResult, noResult, errorJson } from './../assets/alfresco-search.component.mock'; import { AlfrescoSearchService } from '../services/alfresco-search.service'; import { AlfrescoApiService, @@ -35,6 +35,12 @@ describe('AlfrescoSearchAutocompleteComponent', () => { let fixture: ComponentFixture, element: HTMLElement; let component: AlfrescoSearchAutocompleteComponent; + let updateSearchTerm = (newSearchTerm: string): void => { + let oldSearchTerm = component.searchTerm; + component.searchTerm = newSearchTerm; + component.ngOnChanges({searchTerm: { currentValue: newSearchTerm, previousValue: oldSearchTerm}}); + }; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -64,230 +70,337 @@ describe('AlfrescoSearchAutocompleteComponent', () => { expect(translationService.addTranslationFolder).toHaveBeenCalledWith('node_modules/ng2-alfresco-search/dist/src'); }); - it('should display search results when a search term is provided', () => { - let searchTerm = { currentValue: 'customSearchTerm', previousValue: ''}; - spyOn(component, 'displaySearchResults').and.stub(); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({ - searchTerm: searchTerm + describe('search results', () => { + + let searchService; + + beforeEach(() => { + searchService = fixture.debugElement.injector.get(AlfrescoSearchService); }); - fixture.detectChanges(); - expect(component.displaySearchResults).toHaveBeenCalledWith(searchTerm.currentValue); + + it('should clear results straight away when a new search term is entered', async(() => { + + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + component.searchTerm = 'searchTerm'; + component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} }); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + component.searchTerm = 'searchTerm2'; + component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} }); + fixture.detectChanges(); + expect(element.querySelectorAll('table[data-automation-id="autocomplete_results"] tbody tr').length).toBe(0); + }); + })); + + it('should display the returned search results', (done) => { + + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + expect( element.querySelector('#result_user_0').innerHTML).toBe('John Doe'); + expect( element.querySelector('#result_name_0').innerHTML).toBe('MyDoc'); + done(); + }); + + updateSearchTerm('searchTerm'); + }); + + it('should display the correct thumbnail for result items', (done) => { + + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + component.baseComponentPath = 'http://localhost'; + + let thumbnailService = fixture.debugElement.injector.get(AlfrescoThumbnailService); + spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('fake-type-icon.svg'); + spyOn(thumbnailService, 'getMimeTypeKey').and.returnValue('FAKE_TYPE'); + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + let imgEl = element.querySelector('#result_row_0 img'); + expect(imgEl).not.toBeNull(); + expect(imgEl.src).toBe('http://localhost/img/fake-type-icon.svg'); + expect(imgEl.alt).toBe('SEARCH.ICONS.FAKE_TYPE'); + done(); + }); + + updateSearchTerm('searchTerm'); + }); + + it('should display no result if no result are returned', (done) => { + + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(noResult)); + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + expect(element.querySelector('#search_no_result')).not.toBeNull(); + done(); + }); + + updateSearchTerm('searchTerm'); + }); + }); - it('should clear results straight away when a new search term is entered', async(() => { + describe('errors', () => { - let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); - spyOn(searchService, 'getSearchNodesPromise') - .and.returnValue(Promise.resolve(result)); + let searchService; - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} }); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - component.searchTerm = 'searchTerm2'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} }); - fixture.detectChanges(); - expect(element.querySelectorAll('table[data-automation-id="autocomplete_results"] tbody tr').length).toBe(0); - }); - })); - - it('should display the returned search results', (done) => { - - let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); - spyOn(searchService, 'getSearchNodesPromise') - .and.returnValue(Promise.resolve(result)); - - component.resultsEmitter.subscribe(x => { - fixture.detectChanges(); - expect( element.querySelector('#result_user_0').innerHTML).toBe('John Doe'); - expect( element.querySelector('#result_name_0').innerHTML).toBe('MyDoc'); - done(); + beforeEach(() => { + searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.reject(errorJson)); }); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} }); + it('should display an error if an error is encountered running the search', (done) => { + + component.errorEmitter.subscribe(() => { + fixture.detectChanges(); + let resultsEl = element.querySelector('[data-automation-id="autocomplete_results"]'); + let errorEl = element.querySelector('[data-automation-id="autocomplete_error_message"]'); + expect(resultsEl).toBeNull(); + expect(errorEl).not.toBeNull(); + expect(errorEl.innerText.trim()).toBe('SEARCH.RESULTS.ERROR'); + done(); + }); + + updateSearchTerm('searchTerm'); + }); + + it('should clear errors straight away when a new search is performed', async(() => { + + updateSearchTerm('searchTerm'); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + component.searchTerm = 'searchTerm2'; + component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} }); + fixture.detectChanges(); + let errorEl = element.querySelector('[data-automation-id="autocomplete_error_message"]'); + expect(errorEl).toBeNull(); + }); + })); + }); - it('should display the correct thumbnail for result items', (done) => { + describe('mouse interactions', () => { - let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); - spyOn(searchService, 'getSearchNodesPromise') - .and.returnValue(Promise.resolve(result)); + let searchService; - component.baseComponentPath = 'http://localhost'; - - let thumbnailService = fixture.debugElement.injector.get(AlfrescoThumbnailService); - spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('fake-type-icon.svg'); - spyOn(thumbnailService, 'getMimeTypeKey').and.returnValue('FAKE_TYPE'); - - component.resultsEmitter.subscribe(() => { - fixture.detectChanges(); - let imgEl = element.querySelector('#result_row_0 img'); - expect(imgEl).not.toBeNull(); - expect(imgEl.src).toBe('http://localhost/img/fake-type-icon.svg'); - expect(imgEl.alt).toBe('SEARCH.ICONS.FAKE_TYPE'); - done(); + beforeEach(() => { + searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + }); + + it('should emit preview when file item clicked', (done) => { + + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).click(); + }); + + updateSearchTerm('searchTerm'); + + component.preview.subscribe(() => { + done(); + }); + }); + + it('should not emit preview if a non-file item is clicked', (done) => { + + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(folderResult)); + + spyOn(component.preview, 'emit'); + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).click(); + expect(component.preview.emit).not.toHaveBeenCalled(); + done(); + }); + + updateSearchTerm('searchTerm'); }); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} }); }); - it('should display no result if no result are returned', (done) => { + describe('keyboard interactions', () => { - let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); - spyOn(searchService, 'getSearchNodesPromise') - .and.returnValue(Promise.resolve(noResult)); + let searchService; - component.resultsEmitter.subscribe(x => { - fixture.detectChanges(); - expect(element.querySelector('#search_no_result')).not.toBeNull(); - done(); + beforeEach(() => { + searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(results)); + }); + + it('should emit preview when enter key pressed when a file item is in focus', (done) => { + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).dispatchEvent(new KeyboardEvent('keyup', { + key: 'Enter' + })); + }); + + updateSearchTerm('searchTerm'); + + component.preview.subscribe(() => { + done(); + }); + }); + + it('should emit cancel event when escape key pressed when a result is in focus', (done) => { + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).dispatchEvent(new KeyboardEvent('keyup', { + key: 'Escape' + })); + }); + + updateSearchTerm('searchTerm'); + + component.cancelEmitter.subscribe(() => { + done(); + }); + }); + + it('should focus the next result when down arrow key pressed when a result is in focus', (done) => { + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + let firstResult: any = element.querySelector('#result_row_0'); + let secondResult: any = element.querySelector('#result_row_1'); + spyOn(secondResult, 'focus'); + firstResult.focus(); + firstResult.dispatchEvent(new KeyboardEvent('keyup', { + key: 'ArrowDown' + })); + expect(secondResult.focus).toHaveBeenCalled(); + done(); + }); + + updateSearchTerm('searchTerm'); + }); + + it('should do nothing when down arrow key pressed when the last result is in focus', (done) => { + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + let lastResult: any = element.querySelector('#result_row_2'); + lastResult.focus(); + lastResult.dispatchEvent(new KeyboardEvent('keyup', { + key: 'ArrowDown' + })); + done(); + }); + + updateSearchTerm('searchTerm'); + }); + + it('should focus the previous result when up arrow key pressed when a result is in focus', (done) => { + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + let firstResult: any = element.querySelector('#result_row_0'); + let secondResult: any = element.querySelector('#result_row_1'); + spyOn(firstResult, 'focus'); + secondResult.focus(); + secondResult.dispatchEvent(new KeyboardEvent('keyup', { + key: 'ArrowUp' + })); + expect(firstResult.focus).toHaveBeenCalled(); + done(); + }); + + updateSearchTerm('searchTerm'); + }); + + it('should emit scroll back event when up arrow key pressed and the first result is in focus', (done) => { + + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + let firstResult: any = element.querySelector('#result_row_0'); + firstResult.dispatchEvent(new KeyboardEvent('keyup', { + key: 'ArrowUp' + })); + }); + + component.scrollBackEmitter.subscribe(() => { + done(); + }); + + updateSearchTerm('searchTerm'); }); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); }); - it('should display an error if an error is encountered running the search', (done) => { + describe('changing focus', () => { - let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); - spyOn(searchService, 'getSearchNodesPromise') - .and.returnValue(Promise.reject(errorJson)); + let searchService; - component.errorEmitter.subscribe(() => { - fixture.detectChanges(); - let resultsEl = element.querySelector('[data-automation-id="autocomplete_results"]'); - let errorEl = element.querySelector('[data-automation-id="autocomplete_error_message"]'); - expect(resultsEl).toBeNull(); - expect(errorEl).not.toBeNull(); - expect(errorEl.innerText.trim()).toBe('SEARCH.RESULTS.ERROR'); - done(); + beforeEach(() => { + searchService = fixture.debugElement.injector.get(AlfrescoSearchService); + spyOn(searchService, 'getSearchNodesPromise') + .and.returnValue(Promise.resolve(result)); }); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); - }); + it('should emit a focus event when a result comes into focus', (done) => { - it('should clear errors straight away when a new search is performed', async(() => { + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('focus')); + }); - let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); - spyOn(searchService, 'getSearchNodesPromise') - .and.returnValue(Promise.reject(errorJson)); + updateSearchTerm('searchTerm'); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - component.searchTerm = 'searchTerm2'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} }); - fixture.detectChanges(); - let errorEl = element.querySelector('[data-automation-id="autocomplete_error_message"]'); - expect(errorEl).toBeNull(); - }); - })); - - it('should emit preview when file item clicked', (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')).click(); + component.focusEmitter.subscribe((e: FocusEvent) => { + expect(e).not.toBeNull(); + expect(e.type).toBe('focus'); + done(); + }); }); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); + it('should emit a focus event when a result loses focus', (done) => { - component.preview.subscribe(e => { - done(); - }); - }); + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + ( element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('blur')); + }); - it('should not emit preview if a non-file item is clicked', (done) => { + updateSearchTerm('searchTerm'); - let searchService = fixture.debugElement.injector.get(AlfrescoSearchService); - spyOn(searchService, 'getSearchNodesPromise') - .and.returnValue(Promise.resolve(folderResult)); - - spyOn(component.preview, 'emit'); - component.resultsEmitter.subscribe(x => { - fixture.detectChanges(); - ( element.querySelector('#result_row_0')).click(); - expect(component.preview.emit).not.toHaveBeenCalled(); - done(); + component.focusEmitter.subscribe((e: FocusEvent) => { + expect(e).not.toBeNull(); + expect(e.type).toBe('blur'); + done(); + }); }); - component.searchTerm = 'searchTerm'; - component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''}}); - }); + it('should give focus to the first result when focusResult() is called externally', (done) => { - it('should emit preview when enter key pressed when a file item is in focus', (done) => { + component.resultsEmitter.subscribe(() => { + fixture.detectChanges(); + let firstResult: any = element.querySelector('#result_row_0'); + spyOn(firstResult, 'focus'); + component.focusResult(); + expect(firstResult.focus).toHaveBeenCalled(); + 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' - })); + updateSearchTerm('searchTerm'); }); - 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 71eb6411a1..fa57baf510 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 @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Component, EventEmitter, Input, OnInit, OnChanges, Output } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, OnInit, OnChanges, Output, ViewChild } from '@angular/core'; import { AlfrescoSearchService } from './../services/alfresco-search.service'; import { AlfrescoThumbnailService } from './../services/alfresco-thumbnail.service'; import { AlfrescoTranslationService } from 'ng2-alfresco-core'; @@ -46,12 +46,20 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges { @Output() focusEmitter: EventEmitter = new EventEmitter(); + @Output() + cancelEmitter = new EventEmitter(); + @Output() resultsEmitter = new EventEmitter(); + @Output() + scrollBackEmitter = new EventEmitter(); + @Output() errorEmitter = new EventEmitter(); + @ViewChild('resultsTableBody', {}) resultsTableBody: ElementRef; + constructor(private alfrescoSearchService: AlfrescoSearchService, private translate: AlfrescoTranslationService, private alfrescoThumbnailService: AlfrescoThumbnailService) { @@ -119,10 +127,12 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges { } } - onItemClick(node, event?: Event): void { - if (event) { - event.preventDefault(); - } + focusResult(): void { + let firstResult = this.resultsTableBody.nativeElement.querySelector('tr'); + ( firstResult).focus(); + } + + onItemClick(node): void { if (node && node.entry) { if (node.entry.isFile) { this.preview.emit({ @@ -150,4 +160,32 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges { } } + private getNextElementSibling(node: Element): Element { + return node.nextElementSibling; + } + + private getPreviousElementSibling(node: Element): Element { + return node.previousElementSibling; + } + + onRowArrowDown($event: KeyboardEvent): void { + let nextElement = this.getNextElementSibling( $event.target); + if (nextElement) { + ( nextElement).focus(); + } + } + + onRowArrowUp($event: KeyboardEvent): void { + let previousElement = this.getPreviousElementSibling( $event.target); + if (previousElement) { + ( previousElement).focus(); + } else { + this.scrollBackEmitter.emit($event); + } + } + + onRowEscape($event: KeyboardEvent): void { + this.cancelEmitter.emit($event); + } + } 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 36236f9dfd..ab75efb3e5 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 @@ -22,7 +22,9 @@ - + (focusEmitter)="onAutoCompleteFocus($event)" + (scrollBackEmitter)="onAutoCompleteReturn($event)" + (cancelEmitter)="onAutoCompleteCancel($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 76eee29fe3..dd7b0f1157 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 @@ -217,6 +217,42 @@ describe('AlfrescoSearchControlComponent', () => { expect(autocomplete.classList.contains('active')).toBe(true); }); + it('should select the first result in find-as-you-type when down arrow is pressed and FAYT is visible', (done) => { + fixture.detectChanges(); + spyOn(component.autocompleteComponent, 'focusResult'); + fixture.detectChanges(); + inputEl.dispatchEvent(new Event('focus')); + window.setTimeout(() => { // wait for debounce() to complete + fixture.detectChanges(); + inputEl.dispatchEvent(new KeyboardEvent('keyup', { + key: 'ArrowDown' + })); + fixture.detectChanges(); + expect(component.autocompleteComponent.focusResult).toHaveBeenCalled(); + done(); + }, 100); + }); + + it('should focus input element when find-as-you-type returns control', () => { + fixture.detectChanges(); + spyOn(inputEl, 'focus'); + fixture.detectChanges(); + component.onAutoCompleteReturn(new KeyboardEvent('keyup', { + key: 'ArrowUp' + })); + expect(inputEl.focus).toHaveBeenCalled(); + }); + + it('should focus input element when find-as-you-type is cancelled', () => { + fixture.detectChanges(); + spyOn(inputEl, 'focus'); + fixture.detectChanges(); + component.onAutoCompleteCancel(new KeyboardEvent('keyup', { + key: 'ArrowUp' + })); + expect(inputEl.focus).toHaveBeenCalled(); + }); + it('should NOT display a find-as-you-type control when configured not to', () => { fixture.componentInstance.autocompleteEnabled = false; fixture.detectChanges(); 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 ad6284c79f..01f46edf71 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 @@ -18,6 +18,7 @@ import { FormControl, Validators } from '@angular/forms'; import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ViewChild } from '@angular/core'; import { AlfrescoTranslationService } from 'ng2-alfresco-core'; +import { AlfrescoSearchAutocompleteComponent } from './alfresco-search-autocomplete.component'; import { SearchTermValidator } from './../forms/search-term-validator'; import { Observable, Subject } from 'rxjs/Rx'; @@ -57,6 +58,9 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy { @ViewChild('searchInput', {}) searchInput: ElementRef; + @ViewChild('autocomplete') + autocompleteComponent: AlfrescoSearchAutocompleteComponent; + @Input() autocompleteEnabled = true; @@ -94,7 +98,7 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy { } private onSearchTermChange(value: string): void { - this.searchActive = true; + this.setAutoCompleteDisplayed(true); this.autocompleteSearchTerm = value; this.searchControl.setValue(value, true); this.searchValid = this.searchControl.valid; @@ -145,6 +149,14 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy { } } + isAutoCompleteDisplayed(): boolean { + return this.searchActive; + } + + setAutoCompleteDisplayed(display: boolean): void { + this.searchActive = display; + } + onFileClicked(event): void { this.preview.emit({ value: event.value @@ -152,11 +164,11 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy { } onSearchFocus($event): void { - this.searchActive = true; + this.setAutoCompleteDisplayed(true); } onSearchBlur($event): void { - this.searchActive = false; + this.setAutoCompleteDisplayed(false); } onFocus($event): void { @@ -178,15 +190,32 @@ export class AlfrescoSearchControlComponent implements OnInit, OnDestroy { } onEscape(): void { - this.searchActive = false; + this.setAutoCompleteDisplayed(false); } onArrowDown(): void { - this.searchActive = true; + if (this.isAutoCompleteDisplayed()) { + this.autocompleteComponent.focusResult(); + } else { + this.setAutoCompleteDisplayed(true); + } } onAutoCompleteFocus($event): void { this.focusSubject.next($event); } + onAutoCompleteReturn($event): void { + if (this.searchInput) { + ( this.searchInput.nativeElement).focus(); + } + } + + onAutoCompleteCancel($event): void { + if (this.searchInput) { + ( this.searchInput.nativeElement).focus(); + } + this.setAutoCompleteDisplayed(false); + } + }