[ACS-9374] Prevent search popup flicker on Enter key press

This commit is contained in:
Shivangi917
2025-07-16 07:57:36 -04:00
parent 9d3a8b6a7c
commit 2128f2021c
6 changed files with 130 additions and 15 deletions

View File

@@ -574,7 +574,8 @@
"FILES": "Files", "FILES": "Files",
"FOLDERS": "Folders", "FOLDERS": "Folders",
"LIBRARIES": "Libraries", "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": { "SORT": {
"SORTING_OPTION": "Sort by", "SORTING_OPTION": "Sort by",

View File

@@ -5,7 +5,8 @@
mat-icon-button mat-icon-button
matPrefix matPrefix
class="app-search-button" class="app-search-button"
(click)="searchSubmit()" (click)="openDropdown()"
(keydown)="onSearchButtonKeyDown($event)"
[title]="'SEARCH.BUTTON.TOOLTIP' | translate" [title]="'SEARCH.BUTTON.TOOLTIP' | translate"
> >
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon> <mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
@@ -18,7 +19,9 @@
[type]="inputType" [type]="inputType"
id="app-control-input" id="app-control-input"
[formControl]="searchFieldFormControl" [formControl]="searchFieldFormControl"
(keyup.enter)="searchSubmit()" (keydown)="onInputKeyDown($event)"
(focus)="onInputFocus()"
(blur)="onBlur()"
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate" [placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
autocomplete="off" autocomplete="off"
/> />

View File

@@ -42,14 +42,24 @@ describe('SearchInputControlComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should emit submit event on searchSubmit', () => { it('should emit submit event if form is valid', () => {
component.searchTerm = 'mock-search-term'; component.searchTerm = 'valid';
let submittedSearchTerm = ''; let submittedTerm = '';
component.submit.subscribe((searchTerm) => (submittedSearchTerm = searchTerm)); component.submit.subscribe((term) => (submittedTerm = term));
component.searchSubmit(); 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', () => { it('should emit searchChange event on inputChange', () => {
@@ -87,4 +97,57 @@ describe('SearchInputControlComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(component.isTermTooShort()).toBe(false); 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();
});
}); });

View File

@@ -29,7 +29,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; 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'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
@@ -65,7 +65,9 @@ export class SearchInputControlComponent implements OnInit {
@ViewChild('searchInput', { static: true }) @ViewChild('searchInput', { static: true })
searchInput: ElementRef; searchInput: ElementRef;
searchFieldFormControl = new FormControl(''); searchFieldFormControl = new FormControl('', [Validators.required]);
isSearchBarActive = false;
get searchTerm(): string { get searchTerm(): string {
return this.searchFieldFormControl.value.replace('text:', 'TEXT:'); 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() { searchSubmit() {
if (!this.searchFieldFormControl.errors) { this.searchFieldFormControl.markAsTouched();
if (this.searchFieldFormControl.valid) {
this.submit.emit(this.searchTerm); this.submit.emit(this.searchTerm);
} }
} }
@@ -93,6 +123,11 @@ export class SearchInputControlComponent implements OnInit {
this.searchChange.emit(''); this.searchChange.emit('');
} }
onBlur() {
this.isSearchBarActive = false;
this.searchFieldFormControl.markAsUntouched();
}
isTermTooShort() { isTermTooShort() {
return !!(this.searchTerm && this.searchTerm.length < 2); return !!(this.searchTerm && this.searchTerm.length < 2);
} }

View File

@@ -36,7 +36,14 @@
(submit)="onSearchSubmit($event)" (submit)="onSearchSubmit($event)"
(searchChange)="onSearchChange($event)" (searchChange)="onSearchChange($event)"
/> />
<mat-hint *ngIf="hasLibrariesConstraint" class="app-search-hint">{{ 'SEARCH.INPUT.HINT' | translate }}</mat-hint> <div class="app-search-feedback">
<mat-hint *ngIf="hasLibrariesConstraint" class="app-search-hint">
{{ 'SEARCH.INPUT.HINT' | translate }}
</mat-hint>
<mat-error *ngIf="searchInputControl.searchFieldFormControl.hasError('required') && searchInputControl.searchFieldFormControl.touched" class="app-search-error">
{{ 'SEARCH.INPUT.REQUIRED' | translate }}
</mat-error>
</div>
<div id="search-options" class="app-search-options"> <div id="search-options" class="app-search-options">
<mat-checkbox *ngFor="let option of searchOptions" <mat-checkbox *ngFor="let option of searchOptions"

View File

@@ -49,16 +49,22 @@ $search-border-radius: 4px;
.app-search-options { .app-search-options {
color: var(--theme-text-color); color: var(--theme-text-color);
border-top: 1px solid var(--theme-divider-color); border-top: 1px solid var(--theme-divider-color);
padding: 10px; padding: 25px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
column-gap: 24px; column-gap: 24px;
} }
.app-search-hint { .app-search-feedback {
position: absolute; position: absolute;
font-size: 12px; gap: 4px;
padding-left: 17px; padding-left: 17px;
margin-top: 4px;
}
.app-search-hint,
.app-search-error {
font-size: 12px;
} }
@media screen and (max-width: 959px) { @media screen and (max-width: 959px) {