diff --git a/projects/aca-content/assets/i18n/en.json b/projects/aca-content/assets/i18n/en.json index 8f1445966..12f368ab4 100644 --- a/projects/aca-content/assets/i18n/en.json +++ b/projects/aca-content/assets/i18n/en.json @@ -574,7 +574,8 @@ "FILES": "Files", "FOLDERS": "Folders", "LIBRARIES": "Libraries", - "HINT": "Search input must have at least 2 alphanumeric characters." + "HINT": "Search input must have at least 2 alphanumeric characters.", + "REQUIRED": "Search term is required." }, "SORT": { "SORTING_OPTION": "Sort by", diff --git a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html index 609f50d88..acde13ebd 100644 --- a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html +++ b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html @@ -5,7 +5,8 @@ mat-icon-button matPrefix class="app-search-button" - (click)="searchSubmit()" + (click)="openDropdown()" + (keydown)="onSearchButtonKeyDown($event)" [title]="'SEARCH.BUTTON.TOOLTIP' | translate" > search @@ -18,7 +19,9 @@ [type]="inputType" id="app-control-input" [formControl]="searchFieldFormControl" - (keyup.enter)="searchSubmit()" + (keydown)="onInputKeyDown($event)" + (focus)="onInputFocus()" + (blur)="onBlur()" [placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate" autocomplete="off" /> diff --git a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts index ea035e75b..055f50a48 100644 --- a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts +++ b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts @@ -42,14 +42,24 @@ describe('SearchInputControlComponent', () => { fixture.detectChanges(); }); - it('should emit submit event on searchSubmit', () => { - component.searchTerm = 'mock-search-term'; + it('should emit submit event if form is valid', () => { + component.searchTerm = 'valid'; - let submittedSearchTerm = ''; - component.submit.subscribe((searchTerm) => (submittedSearchTerm = searchTerm)); + let submittedTerm = ''; + component.submit.subscribe((term) => (submittedTerm = term)); component.searchSubmit(); - expect(submittedSearchTerm).toBe('mock-search-term'); + expect(submittedTerm).toBe('valid'); + }); + + it('should not emit submit event if form is invalid', () => { + component.searchTerm = ''; + + let submitted = false; + component.submit.subscribe(() => (submitted = true)); + + component.searchSubmit(); + expect(submitted).toBeFalse(); }); it('should emit searchChange event on inputChange', () => { @@ -87,4 +97,57 @@ describe('SearchInputControlComponent', () => { fixture.detectChanges(); expect(component.isTermTooShort()).toBe(false); }); + + it('should activate search bar on openDropdown()', () => { + component.openDropdown(); + expect(component.isSearchBarActive).toBeTrue(); + }); + + it('should set isSearchBarActive to true on input focus', () => { + component.onInputFocus(); + expect(component.isSearchBarActive).toBeTrue(); + }); + + it('should reset isSearchBarActive and mark field as untouched on blur', () => { + component.onInputFocus(); + component.searchFieldFormControl.markAsTouched(); + + component.onBlur(); + + expect(component.isSearchBarActive).toBeFalse(); + expect(component.searchFieldFormControl.touched).toBeFalse(); + }); + + it('should handle Enter key on input and submit if valid', () => { + const event = new KeyboardEvent('keydown', { key: 'Enter' }); + spyOn(event, 'preventDefault'); + spyOn(event, 'stopPropagation'); + component.searchTerm = 'abc'; + + let emitted = ''; + component.submit.subscribe((val) => (emitted = val)); + + component.onInputKeyDown(event); + expect(event.preventDefault).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(emitted).toBe('abc'); + }); + + it('should prevent default and open dropdown on Enter key from search icon', () => { + const event = new KeyboardEvent('keydown', { key: 'Enter' }); + spyOn(event, 'preventDefault'); + + component.onSearchButtonKeyDown(event); + expect(event.preventDefault).toHaveBeenCalled(); + expect(component.isSearchBarActive).toBeTrue(); + }); + + it('should not submit on non-Enter key press', () => { + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + let emitted = false; + component.submit.subscribe(() => (emitted = true)); + + component.onInputKeyDown(event); + expect(emitted).toBeFalse(); + }); }); diff --git a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts index 6b73d6052..c06700d51 100644 --- a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts +++ b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts @@ -29,7 +29,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ @@ -65,7 +65,9 @@ export class SearchInputControlComponent implements OnInit { @ViewChild('searchInput', { static: true }) searchInput: ElementRef; - searchFieldFormControl = new FormControl(''); + searchFieldFormControl = new FormControl('', [Validators.required]); + + isSearchBarActive = false; get searchTerm(): string { return this.searchFieldFormControl.value.replace('text:', 'TEXT:'); @@ -82,8 +84,36 @@ export class SearchInputControlComponent implements OnInit { }); } + onSearchButtonKeyDown(event: KeyboardEvent): void { + if (event.key === 'Enter') { + event.preventDefault(); + this.openDropdown(); + } + } + + onInputKeyDown(event: KeyboardEvent): void { + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + this.searchSubmit(); + } + } + + openDropdown() { + this.isSearchBarActive = true; + setTimeout(() => { + this.searchInput.nativeElement.focus(); + }, 0); + } + + onInputFocus() { + this.isSearchBarActive = true; + } + searchSubmit() { - if (!this.searchFieldFormControl.errors) { + this.searchFieldFormControl.markAsTouched(); + + if (this.searchFieldFormControl.valid) { this.submit.emit(this.searchTerm); } } @@ -93,6 +123,11 @@ export class SearchInputControlComponent implements OnInit { this.searchChange.emit(''); } + onBlur() { + this.isSearchBarActive = false; + this.searchFieldFormControl.markAsUntouched(); + } + isTermTooShort() { return !!(this.searchTerm && this.searchTerm.length < 2); } diff --git a/projects/aca-content/src/lib/components/search/search-input/search-input.component.html b/projects/aca-content/src/lib/components/search/search-input/search-input.component.html index e5a2ef805..5f647124f 100644 --- a/projects/aca-content/src/lib/components/search/search-input/search-input.component.html +++ b/projects/aca-content/src/lib/components/search/search-input/search-input.component.html @@ -36,7 +36,14 @@ (submit)="onSearchSubmit($event)" (searchChange)="onSearchChange($event)" /> - {{ 'SEARCH.INPUT.HINT' | translate }} +
+ + {{ 'SEARCH.INPUT.HINT' | translate }} + + + {{ 'SEARCH.INPUT.REQUIRED' | translate }} + +