mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
feat: WIP - fixing search input errors for draft PR
This commit is contained in:
+3
-4
@@ -1,12 +1,11 @@
|
|||||||
<div class="app-search-container">
|
<div class="app-search-container">
|
||||||
<mat-form-field class="app-input-form-field" appearance="outline">
|
<mat-form-field class="app-input-form-field" appearance="outline">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
matPrefix
|
matPrefix
|
||||||
class="app-search-button"
|
class="app-search-button"
|
||||||
(click)="openDropdown()"
|
(click)="openDropdown()"
|
||||||
(keydown)="onSearchButtonKeyDown($event)"
|
(keydown.enter)="openDropdown()"
|
||||||
[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>
|
||||||
@@ -19,12 +18,12 @@
|
|||||||
[type]="inputType"
|
[type]="inputType"
|
||||||
id="app-control-input"
|
id="app-control-input"
|
||||||
[formControl]="searchFieldFormControl"
|
[formControl]="searchFieldFormControl"
|
||||||
(keydown)="onInputKeyDown($event)"
|
(keydown.enter)="searchSubmit()"
|
||||||
(focus)="onInputFocus()"
|
|
||||||
(blur)="onBlur()"
|
(blur)="onBlur()"
|
||||||
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
|
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div matSuffix>
|
<div matSuffix>
|
||||||
<button mat-icon-button (click)="clear()">
|
<button mat-icon-button (click)="clear()">
|
||||||
<mat-icon *ngIf="searchFieldFormControl.value.length" class="app-suffix-icon">clear</mat-icon>
|
<mat-icon *ngIf="searchFieldFormControl.value.length" class="app-suffix-icon">clear</mat-icon>
|
||||||
|
|||||||
+7
-62
@@ -44,22 +44,20 @@ describe('SearchInputControlComponent', () => {
|
|||||||
|
|
||||||
it('should emit submit event if form is valid', () => {
|
it('should emit submit event if form is valid', () => {
|
||||||
component.searchTerm = 'valid';
|
component.searchTerm = 'valid';
|
||||||
|
spyOn(component.submit, 'emit');
|
||||||
let submittedTerm = '';
|
|
||||||
component.submit.subscribe((term) => (submittedTerm = term));
|
|
||||||
|
|
||||||
component.searchSubmit();
|
component.searchSubmit();
|
||||||
expect(submittedTerm).toBe('valid');
|
|
||||||
|
expect(component.submit.emit).toHaveBeenCalledWith('valid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not emit submit event if form is invalid', () => {
|
it('should not emit submit event if form is invalid', () => {
|
||||||
component.searchTerm = '';
|
component.searchTerm = '';
|
||||||
|
spyOn(component.submit, 'emit');
|
||||||
let submitted = false;
|
|
||||||
component.submit.subscribe(() => (submitted = true));
|
|
||||||
|
|
||||||
component.searchSubmit();
|
component.searchSubmit();
|
||||||
expect(submitted).toBeFalse();
|
|
||||||
|
expect(component.submit.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit searchChange event on inputChange', () => {
|
it('should emit searchChange event on inputChange', () => {
|
||||||
@@ -96,58 +94,5 @@ describe('SearchInputControlComponent', () => {
|
|||||||
component.searchTerm = 'dd';
|
component.searchTerm = 'dd';
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-25
@@ -67,8 +67,6 @@ export class SearchInputControlComponent implements OnInit {
|
|||||||
|
|
||||||
searchFieldFormControl = new FormControl('', [Validators.required]);
|
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:');
|
||||||
}
|
}
|
||||||
@@ -84,37 +82,18 @@ 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() {
|
openDropdown() {
|
||||||
this.isSearchBarActive = true;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.searchInput.nativeElement.focus();
|
this.searchInput.nativeElement.focus();
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputFocus() {
|
|
||||||
this.isSearchBarActive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchSubmit() {
|
searchSubmit() {
|
||||||
this.searchFieldFormControl.markAsTouched();
|
this.searchFieldFormControl.markAsTouched();
|
||||||
|
|
||||||
if (this.searchFieldFormControl.valid) {
|
const trimmedTerm = this.searchTerm?.trim();
|
||||||
this.submit.emit(this.searchTerm);
|
if (this.searchFieldFormControl.valid && trimmedTerm?.length > 0) {
|
||||||
|
this.submit.emit(trimmedTerm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +103,6 @@ export class SearchInputControlComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.isSearchBarActive = false;
|
|
||||||
this.searchFieldFormControl.markAsUntouched();
|
this.searchFieldFormControl.markAsUntouched();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-15
@@ -1,10 +1,12 @@
|
|||||||
<button class="app-search-container"
|
<button class="app-search-container" [matMenuTriggerFor]="searchOptionsMenu" (menuOpened)="onMenuOpened()" (menuClosed)="syncInputValues()">
|
||||||
[matMenuTriggerFor]="searchOptionsMenu"
|
|
||||||
(menuOpened)="onMenuOpened()"
|
|
||||||
(menuClosed)="syncInputValues()"
|
|
||||||
>
|
|
||||||
<mat-form-field class="app-input-form-field" appearance="outline">
|
<mat-form-field class="app-input-form-field" appearance="outline">
|
||||||
<button class="aca-search-input--search-button" mat-icon-button matPrefix (click)="searchByOption()" [title]="'SEARCH.BUTTON.TOOLTIP' | translate">
|
<button
|
||||||
|
class="aca-search-input--search-button"
|
||||||
|
mat-icon-button
|
||||||
|
matPrefix
|
||||||
|
(click)="searchByOption()"
|
||||||
|
[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>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -20,7 +22,12 @@
|
|||||||
<div matSuffix>
|
<div matSuffix>
|
||||||
<mat-icon class="app-suffix-icon">arrow_drop_down</mat-icon>
|
<mat-icon class="app-suffix-icon">arrow_drop_down</mat-icon>
|
||||||
|
|
||||||
<button class="aca-search-input--close-button" mat-icon-button [attr.aria-label]="'SEARCH.CLOSE_BUTTON.ARIA_LABEL' | translate" (click)="exitSearch()">
|
<button
|
||||||
|
class="aca-search-input--close-button"
|
||||||
|
mat-icon-button
|
||||||
|
[attr.aria-label]="'SEARCH.CLOSE_BUTTON.ARIA_LABEL' | translate"
|
||||||
|
(click)="exitSearch()"
|
||||||
|
>
|
||||||
<mat-icon class="app-suffix-icon">close</mat-icon>
|
<mat-icon class="app-suffix-icon">close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,14 +43,13 @@
|
|||||||
(submit)="onSearchSubmit($event)"
|
(submit)="onSearchSubmit($event)"
|
||||||
(searchChange)="onSearchChange($event)"
|
(searchChange)="onSearchChange($event)"
|
||||||
/>
|
/>
|
||||||
<div class="app-search-feedback">
|
<mat-error *ngIf="hasLibrariesConstraint" class="app-search-hint">
|
||||||
<mat-hint *ngIf="hasLibrariesConstraint" class="app-search-hint">
|
{{ 'SEARCH.INPUT.HINT' | translate }}
|
||||||
{{ 'SEARCH.INPUT.HINT' | translate }}
|
</mat-error>
|
||||||
</mat-hint>
|
<mat-error
|
||||||
<mat-error *ngIf="searchInputControl.searchFieldFormControl.hasError('required') && searchInputControl.searchFieldFormControl.touched" class="app-search-error">
|
*ngIf="searchInputControl.searchFieldFormControl.errors?.required && searchInputControl.searchFieldFormControl.touched" class="app-search-error">
|
||||||
{{ 'SEARCH.INPUT.REQUIRED' | translate }}
|
{{ 'SEARCH.INPUT.REQUIRED' | translate }}
|
||||||
</mat-error>
|
</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"
|
||||||
|
|||||||
+2
-5
@@ -55,15 +55,12 @@ $search-border-radius: 4px;
|
|||||||
column-gap: 24px;
|
column-gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-search-feedback {
|
.app-search-hint,
|
||||||
|
.app-search-error {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding-left: 17px;
|
padding-left: 17px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.app-search-hint,
|
|
||||||
.app-search-error {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { SearchInputComponent } from './search-input.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { MatError } from '@angular/material/form-field';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||||
|
import { SearchInputControlComponent } from '../search-input-control/search-input-control.component';
|
||||||
|
|
||||||
|
describe('SearchInputComponent', () => {
|
||||||
|
let fixture: ComponentFixture<SearchInputComponent>;
|
||||||
|
let component: SearchInputComponent;
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AppTestingModule, ReactiveFormsModule, SearchInputComponent, SearchInputControlComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SearchInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
// (component as any).searchInputControl = mockControl;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation Behavior', () => {
|
||||||
|
function getFirstError(): string {
|
||||||
|
const error = fixture.debugElement.query(By.directive(MatError));
|
||||||
|
return error?.nativeElement.textContent.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should show required error when field is empty and touched', () => {
|
||||||
|
component.searchInputControl.searchFieldFormControl.setValue('');
|
||||||
|
component.searchInputControl.searchFieldFormControl.markAsTouched();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const error = getFirstError();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(error).toBe('SEARCH.INPUT.REQUIRED');
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('should not show error when field has value', () => {
|
||||||
|
// mockControl.searchFieldFormControl.setValue('test');
|
||||||
|
// mockControl.searchFieldFormControl.markAsTouched();
|
||||||
|
// fixture.detectChanges();
|
||||||
|
|
||||||
|
// const error = fixture.debugElement.query(By.directive(MatError));
|
||||||
|
// expect(error).toBeNull();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should not show error when field is untouched', () => {
|
||||||
|
// mockControl.searchFieldFormControl.setValue('');
|
||||||
|
// mockControl.searchFieldFormControl.markAsUntouched();
|
||||||
|
// fixture.detectChanges();
|
||||||
|
|
||||||
|
// const error = fixture.debugElement.query(By.directive(MatError));
|
||||||
|
// expect(error).toBeNull();
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -192,6 +192,12 @@ export class SearchInputComponent implements OnInit, OnDestroy {
|
|||||||
searchByOption() {
|
searchByOption() {
|
||||||
this.syncInputValues();
|
this.syncInputValues();
|
||||||
this.has400LibraryError = false;
|
this.has400LibraryError = false;
|
||||||
|
|
||||||
|
const searchTerm = this.searchedWord?.trim();
|
||||||
|
if (!searchTerm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isLibrariesChecked()) {
|
if (this.isLibrariesChecked()) {
|
||||||
this.hasLibrariesConstraint = this.evaluateLibrariesConstraint();
|
this.hasLibrariesConstraint = this.evaluateLibrariesConstraint();
|
||||||
if (this.onLibrariesSearchResults && this.isSameSearchTerm()) {
|
if (this.onLibrariesSearchResults && this.isSameSearchTerm()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user