[ADF-2002] added ARROW support for search autocomplete results (#2732)

* [ADF-2002] added arrow up and down feature

* [ADF-2002] fixed search bar animation and added arrow support

* [ADF-2002] added some test for arrow manage
This commit is contained in:
Vito
2017-11-24 23:50:26 +00:00
committed by Eugenio Romano
parent 78cd7ad84b
commit eba4399d6c
5 changed files with 145 additions and 72 deletions

View File

@@ -10,13 +10,15 @@
</a>
<mat-form-field class="adf-input-form-field-divider">
<input matInput
#inputSearch
[type]="inputType"
[autocomplete]="getAutoComplete()"
id="adf-control-input"
[(ngModel)]="searchTerm"
(focus)="activateToolbar()"
(focus)="activateToolbar($event)"
(blur)="onBlur($event)"
(keyup.escape)="toggleSearchBar()"
(keyup.arrowdown)="selectFirstResult()"
(ngModelChange)="inputChange($event)"
[searchAutocomplete]="auto"
(keyup.enter)="searchSubmit($event)">
@@ -36,6 +38,8 @@
[tabindex]="0"
(focus)="onFocus($event)"
(blur)="onBlur($event)"
(keyup.arrowdown)="onRowArrowDown($event)"
(keyup.arrowup)="onRowArrowUp($event)"
class="adf-search-autocomplete-item"
(click)="elementClicked(item)"
(keyup.enter)="elementClicked(item)">

View File

@@ -55,6 +55,7 @@
}
&-search-autocomplete-item {
&:hover {
background-color: mat-color($background, 'hover');
opacity: 1;

View File

@@ -69,6 +69,13 @@ describe('SearchControlComponent', () => {
TestBed.resetTestingModule();
}));
function typeWordIntoSearchInput(word: string): void {
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = word;
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
}
describe('when input values are inserted', () => {
beforeEach(async(() => {
@@ -83,18 +90,12 @@ describe('SearchControlComponent', () => {
expect(value).toBe('customSearchTerm');
});
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'customSearchTerm';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('customSearchTerm');
fixture.detectChanges();
}));
it('should update FAYT search when user inputs a valid term', async(() => {
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'customSearchTerm';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('customSearchTerm');
spyOn(component, 'isSearchBarActive').and.returnValue(true);
spyOn(searchService, 'search').and.returnValue(Observable.of(results));
@@ -108,10 +109,7 @@ describe('SearchControlComponent', () => {
}));
it('should NOT update FAYT term when user inputs an empty string as search term ', async(() => {
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = '';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('');
spyOn(component, 'isSearchBarActive').and.returnValue(true);
spyOn(searchService, 'search').and.returnValue(Observable.of(results));
@@ -126,10 +124,7 @@ describe('SearchControlComponent', () => {
component.searchChange.subscribe(value => {
expect(value).toBe('cu');
});
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'cu';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('cu');
fixture.detectChanges();
}));
});
@@ -188,9 +183,7 @@ describe('SearchControlComponent', () => {
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
let enterKeyEvent: any = new Event('keyup');
enterKeyEvent.keyCode = '13';
inputDebugElement.nativeElement.dispatchEvent(enterKeyEvent);
@@ -209,10 +202,7 @@ describe('SearchControlComponent', () => {
spyOn(searchService, 'search').and.returnValue(Observable.of(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -227,10 +217,7 @@ describe('SearchControlComponent', () => {
spyOn(searchService, 'search').and.returnValue(Observable.of(noResult));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'NO RES';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('NO RES');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -245,9 +232,7 @@ describe('SearchControlComponent', () => {
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'NO RES';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('NO RES');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -268,9 +253,7 @@ describe('SearchControlComponent', () => {
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -290,9 +273,7 @@ describe('SearchControlComponent', () => {
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -315,9 +296,7 @@ describe('SearchControlComponent', () => {
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -356,15 +335,75 @@ describe('SearchControlComponent', () => {
component.liveSearchEnabled = false;
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#autocomplete-search-result-list')).toBeNull();
});
}));
it('should select the first item on autocomplete list when ARROW DOWN is pressed on input', async(() => {
spyOn(searchService, 'search').and.returnValue(Observable.of(results));
fixture.detectChanges();
typeWordIntoSearchInput('TEST');
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#autocomplete-search-result-list')).not.toBeNull();
inputDebugElement.triggerEventHandler('keyup.arrowdown', {});
fixture.detectChanges();
expect(document.activeElement.id).toBe('result_option_0');
});
}));
it('should select the second item on autocomplete list when ARROW DOWN is pressed on list', async(() => {
spyOn(searchService, 'search').and.returnValue(Observable.of(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#autocomplete-search-result-list')).not.toBeNull();
inputDebugElement.triggerEventHandler('keyup.arrowdown', {});
fixture.detectChanges();
expect(document.activeElement.id).toBe('result_option_0');
let firstElement = debugElement.query(By.css('#result_option_0'));
firstElement.triggerEventHandler('keyup.arrowdown', { target : firstElement.nativeElement});
fixture.detectChanges();
expect(document.activeElement.id).toBe('result_option_1');
});
}));
it('should focus the input search when ARROW UP is pressed on the first list item', async(() => {
spyOn(searchService, 'search').and.returnValue(Observable.of(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#autocomplete-search-result-list')).not.toBeNull();
inputDebugElement.triggerEventHandler('keyup.arrowdown', {});
fixture.detectChanges();
expect(document.activeElement.id).toBe('result_option_0');
let firstElement = debugElement.query(By.css('#result_option_0'));
firstElement.triggerEventHandler('keyup.arrowup', { target : firstElement.nativeElement});
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(document.activeElement.id).toBe('adf-control-input');
});
});
}));
});
describe('search button', () => {
@@ -434,10 +473,7 @@ describe('SearchControlComponent', () => {
expect(item.entry.id).toBe('123');
});
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -456,10 +492,7 @@ describe('SearchControlComponent', () => {
});
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -476,10 +509,7 @@ describe('SearchControlComponent', () => {
expect(component.searchTerm).toBe('TEST');
});
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
inputDebugElement.nativeElement.value = 'TEST';
inputDebugElement.nativeElement.focus();
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
typeWordIntoSearchInput('TEST');
fixture.detectChanges();
fixture.whenStable().then(() => {

View File

@@ -17,11 +17,13 @@
import { AuthenticationService, ThumbnailService } from '@alfresco/adf-core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output,
QueryList, ViewEncapsulation, ViewChild, ViewChildren, ElementRef } from '@angular/core';
import { MinimalNodeEntity, QueryBody } from 'alfresco-js-api';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/distinctUntilChanged';
import { SearchComponent } from './search.component';
import { MatListItem } from '@angular/material';
@Component({
selector: 'adf-search-control',
@@ -73,6 +75,15 @@ export class SearchControlComponent implements OnInit, OnDestroy {
@Output()
optionClicked: EventEmitter<any> = new EventEmitter();
@ViewChild(SearchComponent)
searchAutocomplete: SearchComponent;
@ViewChild('inputSearch')
inputSearch: ElementRef;
@ViewChildren(MatListItem)
private listResultElement: QueryList<MatListItem>;
searchTerm: string = '';
subscriptAnimationState: string;
@@ -88,6 +99,10 @@ export class SearchControlComponent implements OnInit, OnDestroy {
if (this.subscriptAnimationState === 'inactive') {
this.searchTerm = '';
this.searchAutocomplete.resetResults();
if ( document.activeElement.id === this.inputSearch.nativeElement.id) {
this.inputSearch.nativeElement.blur();
}
}
}
});
@@ -165,15 +180,39 @@ export class SearchControlComponent implements OnInit, OnDestroy {
this.focusSubject.next($event);
}
activateToolbar($event) {
activateToolbar() {
if (!this.isSearchBarActive()) {
this.toggleSearchBar();
}
}
selectFirstResult() {
if ( this.listResultElement && this.listResultElement.length > 0) {
let firstElement: MatListItem = <MatListItem> this.listResultElement.first;
firstElement._getHostElement().focus();
}
}
onRowArrowDown($event: KeyboardEvent): void {
let nextElement: any = this.getNextElementSibling(<Element> $event.target);
if (nextElement) {
nextElement.focus();
}
}
onRowArrowUp($event: KeyboardEvent): void {
let previousElement: any = this.getPreviousElementSibling(<Element> $event.target);
if (previousElement) {
previousElement.focus();
}else {
this.inputSearch.nativeElement.focus();
this.focusSubject.next(new FocusEvent('focus'));
}
}
private setupFocusEventHandlers() {
let focusEvents: Observable<FocusEvent> = this.focusSubject.asObservable()
.distinctUntilChanged().debounceTime(50);
.debounceTime(50);
focusEvents.filter(($event: any) => {
return this.isSearchBarActive() && ($event.type === 'blur' || $event.type === 'focusout');
}).subscribe(() => {
@@ -181,4 +220,12 @@ export class SearchControlComponent implements OnInit, OnDestroy {
});
}
private getNextElementSibling(node: Element): Element {
return node.nextElementSibling;
}
private getPreviousElementSibling(node: Element): Element {
return node.previousElementSibling;
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DOWN_ARROW, ENTER, ESCAPE, UP_ARROW } from '@angular/cdk/keycodes';
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
import {
ChangeDetectorRef,
Directive,
@@ -36,10 +36,6 @@ import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import { SearchComponent } from './search.component';
export const AUTOCOMPLETE_OPTION_HEIGHT = 48;
export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
export const SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SearchTriggerDirective),
@@ -150,14 +146,8 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
} else if (keyCode === ENTER) {
this.escapeEventStream.next();
event.preventDefault();
}else {
let isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW;
if ( isArrowKey ) {
if ( !this.panelOpen ) {
this.openPanel();
}
}
}
}
handleInput(event: KeyboardEvent): void {
@@ -168,6 +158,7 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
this.searchPanel.keyPressedStream.next(inputValue);
this.openPanel();
} else {
this.searchPanel.resetResults();
this.closePanel();
}
}