mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-08-14 17:58:21 +00:00
[ACS-9374] Prevent search popup flicker on Enter key press
This commit is contained in:
@@ -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",
|
||||||
|
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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"
|
||||||
|
@@ -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) {
|
||||||
|
Reference in New Issue
Block a user