[AAE-5392] - Make search text input more configurable & add an event … (#7188)

* [AAE-5392] - Make search text input more configurable & add an event emitter to indicate the states of it

* Remove fdescribe

* Emit empty search term when the search gets cleared

* Emit the empty search term when the search gets collapsed by the Search icon

* Same onBlur, emit the empty search term

* Add unit tests for emitters resetting the search term

* Fix comments, use reset event emitter instead of emitting an empty search term

* Update documentation

* Revert reset to boolean

* Fix flaky unit test
This commit is contained in:
arditdomi
2021-07-30 11:30:20 +03:00
committed by GitHub
parent cab016046a
commit 94d908e51a
5 changed files with 184 additions and 16 deletions

View File

@@ -37,13 +37,16 @@ Displays a input text that supports autocompletion
| inputType | `string` | "text" | Type of the input field to render, e.g. "search" or "text" (default). | | inputType | `string` | "text" | Type of the input field to render, e.g. "search" or "text" (default). |
| liveSearchEnabled | `boolean` | true | Toggles "find-as-you-type" suggestions for possible matches. | | liveSearchEnabled | `boolean` | true | Toggles "find-as-you-type" suggestions for possible matches. |
| searchAutocomplete | `any` | false | Trigger autocomplete results on input change. | | searchAutocomplete | `any` | false | Trigger autocomplete results on input change. |
| searchTerm | `string` | "" | Search term preselected | | searchTerm | `string` | "" | Search term preselected. |
| collapseOnBlur | `boolean` | "true" | Toggles whether to collapse the search on blur. |
| showClearButton | `boolean` | "false" | Toggles whether to show a clear button that closes the search. |
### Events ### Events
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when the result list is reset | | reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when the search input is reset |
| searchChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the search term is changed. The search term is provided in the 'value' property of the returned object. If the term is less than three characters in length then it is truncated to an empty string. | | searchChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the search term is changed. The search term is provided in the 'value' property of the returned object. If the term is less than three characters in length then it is truncated to an empty string. |
| selectResult | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when the result list is selected | | selectResult | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when the result list is selected |
| submit | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when the search is submitted by pressing the ENTER key. The search term is provided as the value of the event. | | submit | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when the search is submitted by pressing the ENTER key. The search term is provided as the value of the event. |
| searchVisibility | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when the search visibility changes. True when the search is active, false when it is inactive. |

View File

@@ -321,17 +321,17 @@ describe('SearchControlComponent', () => {
}); });
}); });
it('should NOT display a autocomplete list control when configured not to', (done) => { it('should NOT display a autocomplete list control when configured not to', async () => {
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
component.liveSearchEnabled = false; component.liveSearchEnabled = false;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
typeWordIntoSearchInput('TEST'); typeWordIntoSearchInput('TEST');
fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#autocomplete-search-result-list')).toBeNull(); expect(element.querySelector('#autocomplete-search-result-list')).toBeNull();
done();
});
}); });
}); });

View File

@@ -25,6 +25,13 @@
(ngModelChange)="inputChange($event)" (ngModelChange)="inputChange($event)"
[searchAutocomplete]="searchAutocomplete ? searchAutocomplete : null" [searchAutocomplete]="searchAutocomplete ? searchAutocomplete : null"
(keyup.enter)="searchSubmit($event)"> (keyup.enter)="searchSubmit($event)">
<button mat-icon-button matSuffix
data-automation-id="adf-clear-search-button"
class="adf-clear-search-button"
*ngIf="canShowClearSearch()"
(mousedown)="resetSearch()">
<mat-icon>clear</mat-icon>
</button>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

View File

@@ -260,4 +260,127 @@ describe('SearchTextInputComponent', () => {
expect(element.querySelector('#adf-control-input').getAttribute('autocomplete')).toBe('on'); expect(element.querySelector('#adf-control-input').getAttribute('autocomplete')).toBe('on');
}); });
}); });
describe('Search visibility', () => {
beforeEach(() => {
userPreferencesService.setWithoutStore('textOrientation', 'ltr');
fixture.detectChanges();
});
it('should emit an event when the search becomes active', fakeAsync(() => {
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.toggleSearchBar();
tick(200);
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(true);
}));
it('should emit an event when the search becomes inactive', fakeAsync(() => {
component.toggleSearchBar();
tick(200);
expect(component.subscriptAnimationState.value).toEqual('active');
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.toggleSearchBar();
tick(200);
expect(component.subscriptAnimationState.value).toEqual('inactive');
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(false);
}));
it('should reset emit when the search becomes inactive', fakeAsync(() => {
const resetSpy = spyOn(component.reset, 'emit');
component.toggleSearchBar();
tick(200);
expect(component.subscriptAnimationState.value).toEqual('active');
component.searchTerm = 'fake-search-term';
component.toggleSearchBar();
tick(200);
expect(resetSpy).toHaveBeenCalled();
expect(component.searchTerm).toEqual('');
}));
describe('Clear button', () => {
beforeEach(fakeAsync(() => {
fixture.detectChanges();
component.subscriptAnimationState.value = 'active';
fixture.detectChanges();
tick(200);
}));
it('should clear button be visible when showClearButton is set to true', async () => {
component.showClearButton = true;
fixture.detectChanges();
await fixture.whenStable();
const clearButton = fixture.debugElement.query(By.css('[data-automation-id="adf-clear-search-button"]'));
expect(clearButton).not.toBeNull();
});
it('should clear button not be visible when showClearButton is set to false', () => {
component.showClearButton = false;
fixture.detectChanges();
const clearButton = fixture.debugElement.query(By.css('[data-automation-id="adf-clear-search-button"]'));
expect(clearButton).toBeNull();
});
it('should reset the search when clicking the clear button', async () => {
const resetEmitSpy = spyOn(component.reset, 'emit');
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.searchTerm = 'fake-search-term';
component.showClearButton = true;
fixture.detectChanges();
await fixture.whenStable();
const clearButton = fixture.debugElement.query(By.css('[data-automation-id="adf-clear-search-button"]'));
clearButton.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
fixture.detectChanges();
await fixture.whenStable();
expect(resetEmitSpy).toHaveBeenCalled();
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(false);
expect(component.subscriptAnimationState.value).toEqual('inactive');
expect(component.searchTerm).toEqual('');
});
});
describe('Collapse on blur', () => {
beforeEach(fakeAsync(() => {
fixture.detectChanges();
component.toggleSearchBar();
tick(200);
}));
it('should collapse search on blur when the collapseOnBlur is set to true', fakeAsync (() => {
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
const resetEmitSpy = spyOn(component.reset, 'emit');
component.collapseOnBlur = true;
component.searchTerm = 'fake-search-term';
component.onBlur({ relatedTarget: null });
tick(200);
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(false);
expect(component.subscriptAnimationState.value).toEqual('inactive');
expect(component.searchTerm).toEqual('');
expect(resetEmitSpy).toHaveBeenCalled();
}));
it('should not collapse search on blur when the collapseOnBlur is set to false', () => {
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.searchTerm = 'fake-search-term';
component.collapseOnBlur = false;
component.onBlur({ relatedTarget: null });
expect(searchVisibilityChangeSpy).not.toHaveBeenCalled();
expect(component.subscriptAnimationState.value).toEqual('active');
expect(component.searchTerm).toEqual('fake-search-term');
});
});
});
}); });

View File

@@ -76,6 +76,14 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
@Input() @Input()
defaultState: SearchTextStateEnum = SearchTextStateEnum.collapsed; defaultState: SearchTextStateEnum = SearchTextStateEnum.collapsed;
/** Toggles whether to collapse the search on blur. */
@Input()
collapseOnBlur: boolean = true;
/** Toggles whether to show a clear button that closes the search */
@Input()
showClearButton: boolean = false;
/** Emitted when the search term is changed. The search term is provided /** Emitted when the search term is changed. The search term is provided
* in the 'value' property of the returned object. If the term is less * in the 'value' property of the returned object. If the term is less
* than three characters in length then it is truncated to an empty * than three characters in length then it is truncated to an empty
@@ -98,6 +106,10 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
@Output() @Output()
reset: EventEmitter<boolean> = new EventEmitter(); reset: EventEmitter<boolean> = new EventEmitter();
/** Emitted when the search visibility changes. True when the search is active, false when it is inactive */
@Output()
searchVisibility: EventEmitter<boolean> = new EventEmitter<boolean>();
@ViewChild('searchInput', { static: true }) @ViewChild('searchInput', { static: true })
searchInput: ElementRef; searchInput: ElementRef;
@@ -134,10 +146,11 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
if (this.subscriptAnimationState.value === 'inactive') { if (this.subscriptAnimationState.value === 'inactive') {
this.searchTerm = ''; this.searchTerm = '';
this.reset.emit(true); this.reset.emit(true);
if ( document.activeElement.id === this.searchInput.nativeElement.id) { if (document.activeElement.id === this.searchInput.nativeElement.id) {
this.searchInput.nativeElement.blur(); this.searchInput.nativeElement.blur();
} }
} }
this.emitVisibilitySearch();
} }
}); });
} }
@@ -157,7 +170,7 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
} }
applySearchFocus(animationDoneEvent) { applySearchFocus(animationDoneEvent) {
if (animationDoneEvent.toState === 'active' && this.defaultState !== SearchTextStateEnum.expanded) { if (animationDoneEvent.toState === 'active' && this.isDefaultStateCollapsed()) {
this.searchInput.nativeElement.focus(); this.searchInput.nativeElement.focus();
} }
} }
@@ -186,7 +199,7 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
} }
private getAnimationState(dir: string): SearchAnimationState { private getAnimationState(dir: string): SearchAnimationState {
if ( this.expandable && this.defaultState === SearchTextStateEnum.expanded ) { if (this.expandable && this.isDefaultStateExpanded()) {
return this.animationStates[dir].active; return this.animationStates[dir].active;
} else if ( this.expandable ) { } else if ( this.expandable ) {
return this.animationStates[dir].inactive; return this.animationStates[dir].inactive;
@@ -230,9 +243,8 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
} }
onBlur($event) { onBlur($event) {
if (!$event.relatedTarget && this.defaultState === SearchTextStateEnum.collapsed) { if (this.collapseOnBlur && !$event.relatedTarget) {
this.searchTerm = ''; this.resetSearch();
this.subscriptAnimationState = this.animationStates[this.dir].inactive;
} }
} }
@@ -261,7 +273,7 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
} }
isSearchBarActive(): boolean { isSearchBarActive(): boolean {
return this.subscriptAnimationState.value === 'active' && this.liveSearchEnabled; return this.subscriptAnimationState.value === 'active';
} }
ngOnDestroy() { ngOnDestroy() {
@@ -279,4 +291,27 @@ export class SearchTextInputComponent implements OnInit, OnDestroy {
this.onDestroy$.next(true); this.onDestroy$.next(true);
this.onDestroy$.complete(); this.onDestroy$.complete();
} }
canShowClearSearch(): boolean {
return this.showClearButton && this.isSearchBarActive();
}
resetSearch() {
if (this.isSearchBarActive()) {
this.toggleSearchBar();
}
}
private isDefaultStateCollapsed(): boolean {
return this.defaultState === SearchTextStateEnum.collapsed;
}
private isDefaultStateExpanded(): boolean {
return this.defaultState === SearchTextStateEnum.expanded;
}
private emitVisibilitySearch() {
this.isSearchBarActive() ? this.searchVisibility.emit(true) : this.searchVisibility.emit(false);
}
} }