Skip to content

Commit

Permalink
[ACS-5266] Advanced Search - New component for Category facet (#8764)
Browse files Browse the repository at this point in the history
* [ACS-5266] new component for category facet

* [ACS-5266] fixed tests & docs

* [ACS-5266] some fixes

* [ACS-5266] linting

* [ACS-5266] some improvements

* [ACS-5266] reduced observable from child component

* [ACS-5266] fixed docs

* [ACS-5266] rebase & improvements

* [ACS-5266] typo
  • Loading branch information
nikita-web-ua committed Jul 21, 2023
1 parent 1ebac21 commit 2a4507d
Show file tree
Hide file tree
Showing 18 changed files with 308 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,32 @@ Represents an input with autocomplete options.

```html
<adf-search-chip-autocomplete-input
[autocompleteOptions]="allOptions"
[autocompleteOptions]="autocompleteOptions"
[onReset$]="onResetObservable$"
[allowOnlyPredefinedValues]="allowOnlyPredefinedValues"
(inputChanged)="onInputChange($event)"
(optionsChanged)="onOptionsChange($event)">
</adf-search-chip-autocomplete-input>
```

### Properties

| Name | Type | Default value | Description |
|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------|
| autocompleteOptions | `string[]` | [] | Options for autocomplete |
| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | Observable that will listen to any reset event causing component to clear the chips and input |
| allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones |
| placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. |
| compareOption | (option1: string, option2: string) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. |
| formatChipValue | (option: string) => string | | Function which is used to format custom typed options. |
| filter | (options: string[], value: string) => string[] | | Function which is used to filter out possibile options from hint. By default it checks if option includes typed value and is case insensitive. |
| Name | Type | Default value | Description |
|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------------------------------------------------------|
| autocompleteOptions | `AutocompleteOption[]` | [] | Options for autocomplete |
| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | Observable that will listen to any reset event causing component to clear the chips and input |
| allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones |
| placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. |
| compareOption | (option1: AutocompleteOption, option2: AutocompleteOption) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. |
| formatChipValue | (option: string) => string | | Function which is used to format custom typed options. |
| filter | (options: AutocompleteOption[], value: string) => AutocompleteOption[] | | Function which is used to filter out possible options from hint. By default it checks if option includes typed value and is case insensitive. |

### Events

| Name | Type | Description |
| ---- | ---- |-----------------------------------------------|
| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string[]>` | Emitted when the selected options are changed |
| inputChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the input changes |
| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<AutocompleteOption[]>` | Emitted when the selected options are changed |

## See also

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
"hideDefaultAction": true,
"allowOnlyPredefinedValues": false,
"field": "SITE",
"options": [ "Option 1", "Option 2" ]
"autocompleteOptions": [ {"value": "Option 1"}, {"value": "Option 2"} ]
}
}
}
Expand All @@ -42,7 +42,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
| Name | Type | Description |
| ---- |----------|--------------------------------------------------------------------------------------------------------------------|
| field | `string` | Field to apply the query to. Required value |
| options | `string[]` | Predefined options for autocomplete |
| autocompleteOptions | `AutocompleteOption[]` | Predefined options for autocomplete |
| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined |
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true |
| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false |
Expand Down
2 changes: 1 addition & 1 deletion docs/content-services/pipes/is-included.pipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Checks if the provided value is contained in the provided array.
<!-- {% raw %} -->

```HTML
<mat-option [disabled]="value | adfIsIncluded: arrayOfValues"</mat-option>
<mat-option [disabled]="value | adfIsIncluded: arrayOfValues : comparator"></mat-option>
```

<!-- {% endraw %} -->
Expand Down
1 change: 1 addition & 0 deletions lib/content-services/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
"SUMMARY": "{{numResults}} result found for {{searchTerm}}",
"NONE": "No results found for {{searchTerm}}",
"ERROR": "We hit a problem during the search - try again.",
"WILL_CONTAIN": "Results will contain '{{searchTerm}}'",
"COLUMNS": {
"NAME": "Display name",
"MODIFIED_BY": "Modified by",
Expand Down
3 changes: 3 additions & 0 deletions lib/content-services/src/lib/material.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import { MatOptionModule, MatRippleModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
Expand All @@ -40,6 +41,7 @@ import { MatBadgeModule } from '@angular/material/badge';
@NgModule({
imports: [
MatButtonModule,
MatAutocompleteModule,
MatChipsModule,
MatDialogModule,
MatIconModule,
Expand All @@ -63,6 +65,7 @@ import { MatBadgeModule } from '@angular/material/badge';
],
exports: [
MatButtonModule,
MatAutocompleteModule,
MatChipsModule,
MatDialogModule,
MatIconModule,
Expand Down
7 changes: 7 additions & 0 deletions lib/content-services/src/lib/pipes/is-included.pipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@ describe('IsIncludedPipe', () => {
it('should return false if the number is not contained in an array', () => {
expect(pipe.transform(50, array)).toBeFalsy();
});

it('should use provided comparator to check if value contains in the provided array', () => {
const arrayOfObjects = [{id: 'id-1', value: 'value-1'}, {id: 'id-2', value: 'value-2'}];
const filterFunction = (extension1, extension2) => extension1.value === extension2.value;
expect(pipe.transform({id: 'id-1', value: 'value-1'}, arrayOfObjects, filterFunction)).toBeTruthy();
expect(pipe.transform({id: 'id-1', value: 'value-3'}, arrayOfObjects, filterFunction)).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
class="adf-option-chips"
*ngFor="let option of selectedOptions"
(removed)="remove(option)">
<span>{{option}}</span>
<button matChipRemove class="adf-option-chips-delete-button" [attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option">
<span [matTooltip]="'SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath}"
[matTooltipDisabled]="!option.fullPath" [matTooltipShowDelay]="tooltipShowDelay">
{{ option.value }}
</span>
<button matChipRemove class="adf-option-chips-delete-button" [matTooltipDisabled]="!option.fullPath"
[matTooltip]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' \'' + option.fullPath + '\''"
[matTooltipShowDelay]="tooltipShowDelay"
[attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option.value">
<mat-icon class="adf-option-chips-delete-icon">close</mat-icon>
</button>
</mat-chip>
Expand All @@ -24,9 +30,15 @@
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete"
(optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false">
<mat-option [disabled]="option | adfIsIncluded: selectedOptions : compareOption" *ngFor="let option of filteredOptions$ | async"
[ngClass]="(option | adfIsIncluded: selectedOptions : compareOption) && 'adf-autocomplete-added-option'">
{{option}}
</mat-option>
<ng-container *ngIf="optionInput.value.length > 0">
<mat-option
[disabled]="option | adfIsIncluded: selectedOptions : compareOption"
*ngFor="let option of filteredOptions" [value]="option" [matTooltipShowDelay]="tooltipShowDelay"
[matTooltipDisabled]="!option.fullPath" matTooltipPosition="right"
[matTooltip]="'SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath || option.value}"
[ngClass]="(option | adfIsIncluded: selectedOptions : compareOption) && 'adf-autocomplete-added-option'">
{{ option.fullPath || option.value }}
</mat-option>
</ng-container>
</mat-autocomplete>
</mat-form-field>
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ adf-search-chip-autocomplete-input {
}
}

.mat-tooltip-hide {
display: none;
}

.mat-option.adf-autocomplete-added-option {
background: var(--adf-theme-mat-grey-color-a200);
color: var(--adf-theme-primary-300);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ describe('SearchChipAutocompleteInputComponent', () => {
fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent);
component = fixture.componentInstance;
component.onReset$ = onResetSubject.asObservable();
component.autocompleteOptions = [{value: 'option1'}, {value: 'option2'}];
fixture.detectChanges();
component.autocompleteOptions = ['option1', 'option2'];
});

function getInput(): HTMLInputElement {
Expand Down Expand Up @@ -110,47 +110,52 @@ describe('SearchChipAutocompleteInputComponent', () => {
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
enterNewInputValue('op');
await fixture.whenStable();
fixture.detectChanges();

const matOptions = getOptionElements();
expect(matOptions.length).toBe(2);

const optionToClick = matOptions[0].nativeElement as HTMLElement;
optionToClick.click();

expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']);
expect(component.selectedOptions).toEqual(['option1']);
expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option1'}]);
expect(component.selectedOptions).toEqual([{value: 'option1'}]);
expect(getChipList().length).toBe(1);
});

it('should apply class to already selected options', async () => {
addNewOption('option1');
enterNewInputValue('op');

const addedOptions = getAddedOptionElements();

await fixture.whenStable();
fixture.detectChanges();

const addedOptions = getAddedOptionElements();
expect(addedOptions[0]).toBeTruthy();
expect(addedOptions.length).toBe(1);
});

it('should apply class to already selected options based on custom compareOption function', async () => {
component.allowOnlyPredefinedValues = false;
component.autocompleteOptions = ['.test1', 'test3', '.test2', 'test1.'];
component.compareOption = (option1, option2) => option1.split('.')[1] === option2;
component.autocompleteOptions = [{value: '.test1'}, {value: 'test3'}, {value: '.test2.'}, {value: 'test1'}];
component.compareOption = (option1, option2) => option1.value.split('.')[1] === option2.value;

fixture.detectChanges();
addNewOption('test1');
enterNewInputValue('t');

const addedOptions = getAddedOptionElements();
await fixture.whenStable();
expect(addedOptions.length).toBe(1);
fixture.detectChanges();

expect(getAddedOptionElements().length).toBe(1);
});

it('should limit autocomplete list to 15 values max', () => {
component.autocompleteOptions = ['a1','a2','a3','a4','a5','a6','a7','a8','a9','a10','a11','a12','a13','a14','a15','a16'];
it('should limit autocomplete list to 15 values max', async () => {
component.autocompleteOptions = Array.from({length: 16}, (_, i) => ({value: `a${i}`}));
enterNewInputValue('a');

await fixture.whenStable();
fixture.detectChanges();

expect(getOptionElements().length).toBe(15);
});

Expand All @@ -160,27 +165,33 @@ describe('SearchChipAutocompleteInputComponent', () => {
expect(getChipList().length).toBe(1);
});

it('should show autocomplete list if similar predefined values exists', () => {
it('should show autocomplete list if similar predefined values exists', async () => {
enterNewInputValue('op');
await fixture.whenStable();
fixture.detectChanges();
expect(getOptionElements().length).toBe(2);
});

it('should show autocomplete list based on custom filtering', () => {
component.autocompleteOptions = ['.test1', 'test1', 'test1.', '.test2', '.test12'];
component.filter = (options, value) => options.filter((option) => option.split('.')[1] === value);
it('should show autocomplete list based on custom filtering', async () => {
component.autocompleteOptions = [{value: '.test1'}, {value: 'test1'}, {value: 'test1.'}, {value: '.test2'}, {value: '.test12'}];
component.filter = (options, value) => options.filter((option) => option.value.split('.')[1] === value);
enterNewInputValue('test1');
await fixture.whenStable();
fixture.detectChanges();
expect(getOptionElements().length).toBe(1);
});

it('should not show autocomplete list if there are no similar predefined values', () => {
it('should not show autocomplete list if there are no similar predefined values', async () => {
enterNewInputValue('test');
await fixture.whenStable();
fixture.detectChanges();
expect(getOptionElements().length).toBe(0);
});

it('should emit new value when selected options changed', () => {
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
addNewOption('option1');
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']);
expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option1'}]);
expect(getChipList().length).toBe(1);
expect(getChipValue(0)).toBe('option1');
});
Expand Down Expand Up @@ -221,7 +232,23 @@ describe('SearchChipAutocompleteInputComponent', () => {
fixture.debugElement.query(By.directive(MatChipRemove)).nativeElement.click();
fixture.detectChanges();

expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option2']);
expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option2'}]);
expect(getChipList().length).toEqual(1);
});

it('should show full category path when fullPath provided', () => {
component.filteredOptions = [{id: 'test-id', value: 'test-value', fullPath: 'test-full-path'}];
enterNewInputValue('test-value');
const matOption = fixture.debugElement.query(By.css('.mat-option span')).nativeElement;
fixture.detectChanges();

expect(matOption.innerHTML).toEqual(' test-full-path ');
});

it('should emit input value when input changed', async () => {
const inputChangedSpy = spyOn(component.inputChanged, 'emit');
enterNewInputValue('test-value');
await fixture.whenStable();
expect(inputChangedSpy).toHaveBeenCalledOnceWith('test-value');
});
});
Loading

0 comments on commit 2a4507d

Please sign in to comment.