diff --git a/docs/core/components/search-text-input.component.md b/docs/core/components/search-text-input.component.md index 8dd7540f4e..9cbedb1011 100644 --- a/docs/core/components/search-text-input.component.md +++ b/docs/core/components/search-text-input.component.md @@ -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). | | liveSearchEnabled | `boolean` | true | Toggles "find-as-you-type" suggestions for possible matches. | | 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 | Name | Type | Description | | ---- | ---- | ----------- | -| reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the result list is reset | +| reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the search input is reset | | searchChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | 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)`` | Emitted when the result list is selected | | submit | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | 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)`` | Emitted when the search visibility changes. True when the search is active, false when it is inactive. | diff --git a/lib/content-services/src/lib/search/components/search-control.component.spec.ts b/lib/content-services/src/lib/search/components/search-control.component.spec.ts index 84531a89f0..7189d3f4ce 100644 --- a/lib/content-services/src/lib/search/components/search-control.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-control.component.spec.ts @@ -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)))); component.liveSearchEnabled = false; fixture.detectChanges(); + await fixture.whenStable(); typeWordIntoSearchInput('TEST'); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(element.querySelector('#autocomplete-search-result-list')).toBeNull(); - done(); - }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(element.querySelector('#autocomplete-search-result-list')).toBeNull(); }); }); diff --git a/lib/core/search-text/search-text-input.component.html b/lib/core/search-text/search-text-input.component.html index fcb89d7e24..3e6897b92b 100644 --- a/lib/core/search-text/search-text-input.component.html +++ b/lib/core/search-text/search-text-input.component.html @@ -25,6 +25,13 @@ (ngModelChange)="inputChange($event)" [searchAutocomplete]="searchAutocomplete ? searchAutocomplete : null" (keyup.enter)="searchSubmit($event)"> + - \ No newline at end of file + diff --git a/lib/core/search-text/search-text-input.component.spec.ts b/lib/core/search-text/search-text-input.component.spec.ts index 4a43d314f4..9d1bcc8c40 100644 --- a/lib/core/search-text/search-text-input.component.spec.ts +++ b/lib/core/search-text/search-text-input.component.spec.ts @@ -260,4 +260,127 @@ describe('SearchTextInputComponent', () => { 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'); + }); + }); + }); }); diff --git a/lib/core/search-text/search-text-input.component.ts b/lib/core/search-text/search-text-input.component.ts index d64c3ff42f..a98db869b8 100644 --- a/lib/core/search-text/search-text-input.component.ts +++ b/lib/core/search-text/search-text-input.component.ts @@ -76,6 +76,14 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { @Input() 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 * 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 @@ -98,6 +106,10 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { @Output() reset: EventEmitter = new EventEmitter(); + /** Emitted when the search visibility changes. True when the search is active, false when it is inactive */ + @Output() + searchVisibility: EventEmitter = new EventEmitter(); + @ViewChild('searchInput', { static: true }) searchInput: ElementRef; @@ -134,10 +146,11 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { if (this.subscriptAnimationState.value === 'inactive') { this.searchTerm = ''; 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.emitVisibilitySearch(); } }); } @@ -157,7 +170,7 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { } applySearchFocus(animationDoneEvent) { - if (animationDoneEvent.toState === 'active' && this.defaultState !== SearchTextStateEnum.expanded) { + if (animationDoneEvent.toState === 'active' && this.isDefaultStateCollapsed()) { this.searchInput.nativeElement.focus(); } } @@ -186,7 +199,7 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { } private getAnimationState(dir: string): SearchAnimationState { - if ( this.expandable && this.defaultState === SearchTextStateEnum.expanded ) { + if (this.expandable && this.isDefaultStateExpanded()) { return this.animationStates[dir].active; } else if ( this.expandable ) { return this.animationStates[dir].inactive; @@ -230,9 +243,8 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { } onBlur($event) { - if (!$event.relatedTarget && this.defaultState === SearchTextStateEnum.collapsed) { - this.searchTerm = ''; - this.subscriptAnimationState = this.animationStates[this.dir].inactive; + if (this.collapseOnBlur && !$event.relatedTarget) { + this.resetSearch(); } } @@ -261,7 +273,7 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { } isSearchBarActive(): boolean { - return this.subscriptAnimationState.value === 'active' && this.liveSearchEnabled; + return this.subscriptAnimationState.value === 'active'; } ngOnDestroy() { @@ -279,4 +291,27 @@ export class SearchTextInputComponent implements OnInit, OnDestroy { this.onDestroy$.next(true); 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); + } + }