mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-31 17:38:48 +00:00
New implementation of focus/blur event management using Obserable
Refs #371
This commit is contained in:
@@ -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'
|
||||
}
|
||||
};
|
@@ -1,7 +1,7 @@
|
||||
<table data-automation-id="autocomplete_results" *ngIf="results && results.length && searchTerm"
|
||||
class="mdl-data-table mdl-js-data-table mdl-shadow--2dp full-width">
|
||||
<tbody (focusout)="onFocusOut()">
|
||||
<tr id="result_row_{{idx}}" *ngFor="let result of results; let idx = index" tabindex="0" (blur)="onRowBlur(result)" (focus)="onRowFocus(result)" (click)="onItemClick(result, $event)" (keyup.enter)="onRowEnter(result, $event)"
|
||||
<tbody>
|
||||
<tr id="result_row_{{idx}}" *ngFor="let result of results; let idx = index" tabindex="0" (blur)="onRowBlur($event)" (focus)="onRowFocus($event)" (click)="onItemClick(result, $event)" (keyup.enter)="onRowEnter(result, $event)"
|
||||
attr.data-automation-id="autocomplete_result_for_{{result.entry.name}}">
|
||||
<td class="img-td"><img src="{{getMimeTypeIcon(result)}}" alt="{{getMimeTypeKey(result)|translate}}"/></td>
|
||||
<td>
|
||||
|
@@ -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<AlfrescoSearchAutocompleteComponent>, 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();
|
||||
(<any> 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();
|
||||
(<any> 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();
|
||||
(<any> 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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -44,7 +44,7 @@ export class AlfrescoSearchAutocompleteComponent implements OnInit, OnChanges {
|
||||
preview: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
blurEmitter: EventEmitter<any> = new EventEmitter();
|
||||
focusEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
|
||||
|
||||
@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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -24,4 +24,5 @@
|
||||
</form>
|
||||
<alfresco-search-autocomplete *ngIf="autocompleteEnabled"
|
||||
[searchTerm]="autocompleteSearchTerm" [ngClass]="{active: searchActive, valid: searchValid}"
|
||||
(preview)="onFileClicked($event)" (blurEmitter)="onAutoCompleteBlur($event)"></alfresco-search-autocomplete>
|
||||
(preview)="onFileClicked($event)"
|
||||
(focusEmitter)="onAutoCompleteFocus($event)"></alfresco-search-autocomplete>
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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<FocusEvent>();
|
||||
|
||||
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<FocusEvent> = 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user