Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ACS-5266] Advanced Search - New component for Category facet #8764

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should take fullPath and if it's not present it should display value

[matTooltipDisabled]="!option.fullPath" [matTooltipShowDelay]="tooltipShowDelay">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to consider if we want to display tooltip when there is only value provided since it may happen that it will be too long to be displayed as a whole

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it as well, not sure if we need tooltips for other filters though. In the ticket for Tags & Location, there is no info about tooltips.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but after we release this, customers might use this component to define filters on some custom properties as well and we can't predict if they will contain longer values. But on the other hand they can always use the fullPath so I think we can leave it as it for now and in case of bugs modify later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tooltips for autocomplete in case of long values as long values shrink there, for chips we need tooltips only for Categories to show the full path.

{{ option.value }}
</span>
<button matChipRemove class="adf-option-chips-delete-button" [matTooltipDisabled]="!option.fullPath"
[matTooltip]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' \'' + option.fullPath + '\''"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here for tooltip

[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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for tooltip

[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
Loading