From 893368dadfc0b753017ade2a0950d4c74d52e482 Mon Sep 17 00:00:00 2001
From: nmanu1 <88398086+nmanu1@users.noreply.github.com>
Date: Fri, 11 Nov 2022 16:14:47 -0500
Subject: [PATCH] Add onSelect prop to FilterSearch (#323)
Add an `onSelect` prop to `FilterSearch` and deprecate `searchOnSelect`. If an `onSelect` function is passed in, the default selecting (and searching) logic is bypassed and we call the `onSelect`. If `searchOnSelect=true` is passed with an `onSelect`, we log a console warning that the former will be ignored. To allow for the UCSD use case mentioned in the spec, `currentFilter` was updated to allow any `StaticFilter`, not just a field value filter.
J=SLAP-2431
TEST=auto, manual
See that the added Jest tests pass. In the test-site, pass in an `onSelect` function that matches the current functionality with `searchOnSelect` as either true or false. Also, mimic a UCSD-like use case with compound filters and see that the component behaves correctly when selecting and un-selecting a filter. If both an `onSelect` and `searchOnSelect=true` are passed, see that a console warning is logged.
---
docs/search-ui-react.filtersearch.md | 4 +-
docs/search-ui-react.filtersearchprops.md | 1 +
...rch-ui-react.filtersearchprops.onselect.md | 13 +++
...-react.filtersearchprops.searchonselect.md | 5 +
docs/search-ui-react.md | 3 +-
...h-ui-react.onselectparams.currentfilter.md | 13 +++
...eact.onselectparams.executefiltersearch.md | 13 +++
docs/search-ui-react.onselectparams.md | 24 +++++
...-ui-react.onselectparams.newdisplayname.md | 13 +++
...earch-ui-react.onselectparams.newfilter.md | 13 +++
...i-react.onselectparams.setcurrentfilter.md | 13 +++
etc/search-ui-react.api.md | 16 ++-
src/components/FilterSearch.tsx | 102 +++++++++++-------
src/components/index.ts | 3 +-
src/utils/filterutils.tsx | 22 +++-
tests/components/FilterSearch.test.tsx | 53 ++++++++-
16 files changed, 264 insertions(+), 47 deletions(-)
create mode 100644 docs/search-ui-react.filtersearchprops.onselect.md
create mode 100644 docs/search-ui-react.onselectparams.currentfilter.md
create mode 100644 docs/search-ui-react.onselectparams.executefiltersearch.md
create mode 100644 docs/search-ui-react.onselectparams.md
create mode 100644 docs/search-ui-react.onselectparams.newdisplayname.md
create mode 100644 docs/search-ui-react.onselectparams.newfilter.md
create mode 100644 docs/search-ui-react.onselectparams.setcurrentfilter.md
diff --git a/docs/search-ui-react.filtersearch.md b/docs/search-ui-react.filtersearch.md
index 2e61f2a74..7fa93c8d2 100644
--- a/docs/search-ui-react.filtersearch.md
+++ b/docs/search-ui-react.filtersearch.md
@@ -9,14 +9,14 @@ A component which allows a user to search for filters associated with specific e
Signature:
```typescript
-export declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;
+export declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
-| { searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses } | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | |
+| { searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses } | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | |
Returns:
diff --git a/docs/search-ui-react.filtersearchprops.md b/docs/search-ui-react.filtersearchprops.md
index 15cfe8ec6..d7d9bcb18 100644
--- a/docs/search-ui-react.filtersearchprops.md
+++ b/docs/search-ui-react.filtersearchprops.md
@@ -18,6 +18,7 @@ export interface FilterSearchProps
| --- | --- | --- |
| [customCssClasses?](./search-ui-react.filtersearchprops.customcssclasses.md) | [FilterSearchCssClasses](./search-ui-react.filtersearchcssclasses.md) | (Optional) CSS classes for customizing the component styling. |
| [label?](./search-ui-react.filtersearchprops.label.md) | string | (Optional) The display label for the component. |
+| [onSelect?](./search-ui-react.filtersearchprops.onselect.md) | (params: [OnSelectParams](./search-ui-react.onselectparams.md)) => void | (Optional) A function which is called when a filter is selected. |
| [placeholder?](./search-ui-react.filtersearchprops.placeholder.md) | string | (Optional) The search input's placeholder text when no text has been entered by the user. Defaults to "Search here...". |
| [searchFields](./search-ui-react.filtersearchprops.searchfields.md) | Omit<SearchParameterField, 'fetchEntities'>\[\] | An array of fieldApiName and entityType which indicates what to perform the filter search against. |
| [searchOnSelect?](./search-ui-react.filtersearchprops.searchonselect.md) | boolean | (Optional) Whether to trigger a search when an option is selected. Defaults to false. |
diff --git a/docs/search-ui-react.filtersearchprops.onselect.md b/docs/search-ui-react.filtersearchprops.onselect.md
new file mode 100644
index 000000000..9a6ce464c
--- /dev/null
+++ b/docs/search-ui-react.filtersearchprops.onselect.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FilterSearchProps](./search-ui-react.filtersearchprops.md) > [onSelect](./search-ui-react.filtersearchprops.onselect.md)
+
+## FilterSearchProps.onSelect property
+
+A function which is called when a filter is selected.
+
+Signature:
+
+```typescript
+onSelect?: (params: OnSelectParams) => void;
+```
diff --git a/docs/search-ui-react.filtersearchprops.searchonselect.md b/docs/search-ui-react.filtersearchprops.searchonselect.md
index c6506c3f2..39ec94a54 100644
--- a/docs/search-ui-react.filtersearchprops.searchonselect.md
+++ b/docs/search-ui-react.filtersearchprops.searchonselect.md
@@ -4,6 +4,11 @@
## FilterSearchProps.searchOnSelect property
+> Warning: This API is now obsolete.
+>
+> Use the `onSelect` prop instead.
+>
+
Whether to trigger a search when an option is selected. Defaults to false.
Signature:
diff --git a/docs/search-ui-react.md b/docs/search-ui-react.md
index 110b5fde3..e11f7d74e 100644
--- a/docs/search-ui-react.md
+++ b/docs/search-ui-react.md
@@ -17,7 +17,7 @@
| [executeAutocomplete(searchActions)](./search-ui-react.executeautocomplete.md) | Executes a universal/vertical autocomplete search and return the corresponding response. |
| [executeSearch(searchActions)](./search-ui-react.executesearch.md) | Executes a universal/vertical search. |
| [FilterDivider({ className })](./search-ui-react.filterdivider.md) | A divider component used to separate NumericalFacets, HierarchicalFacets, StandardFacets, and StaticFilters. |
-| [FilterSearch({ searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses })](./search-ui-react.filtersearch.md) | A component which allows a user to search for filters associated with specific entities and fields. |
+| [FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses })](./search-ui-react.filtersearch.md) | A component which allows a user to search for filters associated with specific entities and fields. |
| [getSearchIntents(searchActions)](./search-ui-react.getsearchintents.md) | Get search intents of the current query stored in headless using autocomplete request. |
| [getUserLocation(geolocationOptions)](./search-ui-react.getuserlocation.md) | Retrieves user's location using navigator.geolocation API. |
| [HierarchicalFacets({ searchOnChange, collapsible, defaultExpanded, includedFieldIds, customCssClasses, delimiter, showMoreLimit })](./search-ui-react.hierarchicalfacets.md) | A component that displays hierarchical facets, in a tree level structure, applicable to the current vertical search. |
@@ -70,6 +70,7 @@
| [LocationBiasProps](./search-ui-react.locationbiasprops.md) | The props for the [LocationBias()](./search-ui-react.locationbias.md) component. |
| [NumericalFacetsCssClasses](./search-ui-react.numericalfacetscssclasses.md) | The CSS class interface for [NumericalFacets()](./search-ui-react.numericalfacets.md). |
| [NumericalFacetsProps](./search-ui-react.numericalfacetsprops.md) | Props for the [NumericalFacets()](./search-ui-react.numericalfacets.md) component. |
+| [OnSelectParams](./search-ui-react.onselectparams.md) | The parameters that are passed into [FilterSearchProps.onSelect](./search-ui-react.filtersearchprops.onselect.md). |
| [PaginationCssClasses](./search-ui-react.paginationcssclasses.md) | The CSS classes used for pagination. |
| [PaginationProps](./search-ui-react.paginationprops.md) | Props for [Pagination()](./search-ui-react.pagination.md) component |
| [RangeInputCssClasses](./search-ui-react.rangeinputcssclasses.md) | The CSS class interface for RangeInput. |
diff --git a/docs/search-ui-react.onselectparams.currentfilter.md b/docs/search-ui-react.onselectparams.currentfilter.md
new file mode 100644
index 000000000..e72f10ff1
--- /dev/null
+++ b/docs/search-ui-react.onselectparams.currentfilter.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [currentFilter](./search-ui-react.onselectparams.currentfilter.md)
+
+## OnSelectParams.currentFilter property
+
+The previously selected filter.
+
+Signature:
+
+```typescript
+currentFilter: StaticFilter | undefined;
+```
diff --git a/docs/search-ui-react.onselectparams.executefiltersearch.md b/docs/search-ui-react.onselectparams.executefiltersearch.md
new file mode 100644
index 000000000..ac7720e64
--- /dev/null
+++ b/docs/search-ui-react.onselectparams.executefiltersearch.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [executeFilterSearch](./search-ui-react.onselectparams.executefiltersearch.md)
+
+## OnSelectParams.executeFilterSearch property
+
+A function that executes a filter search and updates the input and dropdown options with the response.
+
+Signature:
+
+```typescript
+executeFilterSearch: (query?: string) => Promise;
+```
diff --git a/docs/search-ui-react.onselectparams.md b/docs/search-ui-react.onselectparams.md
new file mode 100644
index 000000000..d2bc68290
--- /dev/null
+++ b/docs/search-ui-react.onselectparams.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md)
+
+## OnSelectParams interface
+
+The parameters that are passed into [FilterSearchProps.onSelect](./search-ui-react.filtersearchprops.onselect.md).
+
+Signature:
+
+```typescript
+export interface OnSelectParams
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [currentFilter](./search-ui-react.onselectparams.currentfilter.md) | StaticFilter \| undefined | The previously selected filter. |
+| [executeFilterSearch](./search-ui-react.onselectparams.executefiltersearch.md) | (query?: string) => Promise<FilterSearchResponse \| undefined> | A function that executes a filter search and updates the input and dropdown options with the response. |
+| [newDisplayName](./search-ui-react.onselectparams.newdisplayname.md) | string | The display name of the newly selected filter. |
+| [newFilter](./search-ui-react.onselectparams.newfilter.md) | FieldValueStaticFilter | The newly selected filter. |
+| [setCurrentFilter](./search-ui-react.onselectparams.setcurrentfilter.md) | (filter: StaticFilter) => void | A function that sets which filter the component is currently associated with. |
+
diff --git a/docs/search-ui-react.onselectparams.newdisplayname.md b/docs/search-ui-react.onselectparams.newdisplayname.md
new file mode 100644
index 000000000..f47f95d6a
--- /dev/null
+++ b/docs/search-ui-react.onselectparams.newdisplayname.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [newDisplayName](./search-ui-react.onselectparams.newdisplayname.md)
+
+## OnSelectParams.newDisplayName property
+
+The display name of the newly selected filter.
+
+Signature:
+
+```typescript
+newDisplayName: string;
+```
diff --git a/docs/search-ui-react.onselectparams.newfilter.md b/docs/search-ui-react.onselectparams.newfilter.md
new file mode 100644
index 000000000..ff9ddae77
--- /dev/null
+++ b/docs/search-ui-react.onselectparams.newfilter.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [newFilter](./search-ui-react.onselectparams.newfilter.md)
+
+## OnSelectParams.newFilter property
+
+The newly selected filter.
+
+Signature:
+
+```typescript
+newFilter: FieldValueStaticFilter;
+```
diff --git a/docs/search-ui-react.onselectparams.setcurrentfilter.md b/docs/search-ui-react.onselectparams.setcurrentfilter.md
new file mode 100644
index 000000000..90356f104
--- /dev/null
+++ b/docs/search-ui-react.onselectparams.setcurrentfilter.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [setCurrentFilter](./search-ui-react.onselectparams.setcurrentfilter.md)
+
+## OnSelectParams.setCurrentFilter property
+
+A function that sets which filter the component is currently associated with.
+
+Signature:
+
+```typescript
+setCurrentFilter: (filter: StaticFilter) => void;
+```
diff --git a/etc/search-ui-react.api.md b/etc/search-ui-react.api.md
index c8352b55a..58a17225f 100644
--- a/etc/search-ui-react.api.md
+++ b/etc/search-ui-react.api.md
@@ -10,6 +10,8 @@ import { AnalyticsConfig } from '@yext/analytics';
import { AnalyticsService } from '@yext/analytics';
import { AutocompleteResponse } from '@yext/search-headless-react';
import { DirectAnswer as DirectAnswer_2 } from '@yext/search-headless-react';
+import { FieldValueStaticFilter } from '@yext/search-headless-react';
+import { FilterSearchResponse } from '@yext/search-headless-react';
import { HighlightedValue } from '@yext/search-headless-react';
import { Matcher } from '@yext/search-headless-react';
import { NumberRangeValue } from '@yext/search-headless-react';
@@ -20,6 +22,7 @@ import { SearchActions } from '@yext/search-headless-react';
import { SearchHeadless } from '@yext/search-headless-react';
import { SearchIntent } from '@yext/search-headless-react';
import { SearchParameterField } from '@yext/search-headless-react';
+import { StaticFilter } from '@yext/search-headless-react';
import { UniversalLimit } from '@yext/search-headless-react';
import { UnknownFieldValueDirectAnswer } from '@yext/search-headless-react';
import { VerticalResults as VerticalResults_2 } from '@yext/search-headless-react';
@@ -230,7 +233,7 @@ export interface FilterOptionConfig {
}
// @public
-export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;
+export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;
// @public
export interface FilterSearchCssClasses extends AutocompleteResultCssClasses {
@@ -252,8 +255,10 @@ export interface FilterSearchCssClasses extends AutocompleteResultCssClasses {
export interface FilterSearchProps {
customCssClasses?: FilterSearchCssClasses;
label?: string;
+ onSelect?: (params: OnSelectParams) => void;
placeholder?: string;
searchFields: Omit[];
+ // @deprecated
searchOnSelect?: boolean;
sectioned?: boolean;
}
@@ -365,6 +370,15 @@ export type onSearchFunc = (searchEventData: {
query?: string;
}) => void;
+// @public
+export interface OnSelectParams {
+ currentFilter: StaticFilter | undefined;
+ executeFilterSearch: (query?: string) => Promise;
+ newDisplayName: string;
+ newFilter: FieldValueStaticFilter;
+ setCurrentFilter: (filter: StaticFilter) => void;
+}
+
// @public
export function Pagination(props: PaginationProps): JSX.Element | null;
diff --git a/src/components/FilterSearch.tsx b/src/components/FilterSearch.tsx
index 58dda14f4..db5f8e81c 100644
--- a/src/components/FilterSearch.tsx
+++ b/src/components/FilterSearch.tsx
@@ -1,9 +1,9 @@
-import { AutocompleteResult, FieldValueStaticFilter, FilterSearchResponse, SearchParameterField, useSearchActions, useSearchState } from '@yext/search-headless-react';
+import { AutocompleteResult, FieldValueStaticFilter, FilterSearchResponse, SearchParameterField, StaticFilter, useSearchActions, useSearchState } from '@yext/search-headless-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useComposedCssClasses } from '../hooks/useComposedCssClasses';
import { useSynchronizedRequest } from '../hooks/useSynchronizedRequest';
import { executeSearch } from '../utils';
-import { getSelectableFieldValueFilters, isDuplicateFieldValueFilter } from '../utils/filterutils';
+import { isDuplicateStaticFilter } from '../utils/filterutils';
import { Dropdown } from './Dropdown/Dropdown';
import { DropdownInput } from './Dropdown/DropdownInput';
import { DropdownItem } from './Dropdown/DropdownItem';
@@ -34,6 +34,27 @@ const builtInCssClasses: Readonly = {
option: 'text-sm text-neutral-dark py-1 cursor-pointer hover:bg-gray-100 px-4'
};
+/**
+ * The parameters that are passed into {@link FilterSearchProps.onSelect}.
+ *
+ * @public
+ */
+export interface OnSelectParams {
+ /** The newly selected filter. */
+ newFilter: FieldValueStaticFilter,
+ /** The display name of the newly selected filter. */
+ newDisplayName: string,
+ /** The previously selected filter. */
+ currentFilter: StaticFilter | undefined,
+ /** A function that sets which filter the component is currently associated with. */
+ setCurrentFilter: (filter: StaticFilter) => void,
+ /**
+ * A function that executes a filter search and updates the input and dropdown options
+ * with the response.
+ */
+ executeFilterSearch: (query?: string) => Promise
+}
+
/**
* The props for the {@link FilterSearch} component.
*
@@ -49,8 +70,14 @@ export interface FilterSearchProps {
* Defaults to "Search here...".
*/
placeholder?: string,
- /** Whether to trigger a search when an option is selected. Defaults to false. */
+ /**
+ * Whether to trigger a search when an option is selected. Defaults to false.
+ *
+ * @deprecated Use the `onSelect` prop instead.
+ */
searchOnSelect?: boolean,
+ /** A function which is called when a filter is selected. */
+ onSelect?: (params: OnSelectParams) => void,
/** Determines whether or not the results of the filter search are separated by field. Defaults to false. */
sectioned?: boolean,
/** CSS classes for customizing the component styling. */
@@ -70,6 +97,7 @@ export function FilterSearch({
label,
placeholder = 'Search here...',
searchOnSelect,
+ onSelect,
sectioned = false,
customCssClasses
}: FilterSearchProps): JSX.Element {
@@ -78,13 +106,9 @@ export function FilterSearch({
return { ...searchField, fetchEntities: false };
});
const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
- const [currentFilter, setCurrentFilter] = useState();
+ const [currentFilter, setCurrentFilter] = useState();
const [filterQuery, setFilterQuery] = useState();
const staticFilters = useSearchState(state => state.filters.static);
- const fieldValueFilters = useMemo(
- () => getSelectableFieldValueFilters(staticFilters ?? []),
- [staticFilters]
- );
const [
filterSearchResponse,
@@ -99,14 +123,14 @@ export function FilterSearch({
);
useEffect(() => {
- if (currentFilter && fieldValueFilters?.find(f =>
- isDuplicateFieldValueFilter(f, currentFilter) && !f.selected
+ if (currentFilter && staticFilters?.find(f =>
+ isDuplicateStaticFilter(f.filter, currentFilter) && !f.selected
)) {
clearFilterSearchResponse();
setCurrentFilter(undefined);
setFilterQuery('');
}
- }, [clearFilterSearchResponse, currentFilter, fieldValueFilters]);
+ }, [clearFilterSearchResponse, currentFilter, staticFilters]);
const sections = useMemo(() => {
return filterSearchResponse?.sections.filter(section => section.results.length > 0) ?? [];
@@ -114,41 +138,40 @@ export function FilterSearch({
const hasResults = sections.flatMap(s => s.results).length > 0;
- const handleDropdownEvent = useCallback((value, itemData, select) => {
+ const handleSelectDropdown = useCallback((_value, _index, itemData) => {
const newFilter = itemData?.filter as FieldValueStaticFilter;
const newDisplayName = itemData?.displayName as string;
- if (newFilter && newDisplayName) {
- if (select) {
- if (currentFilter) {
- searchActions.setFilterOption({ filter: currentFilter, selected: false });
- }
- searchActions.setFilterOption({ filter: newFilter, displayName: newDisplayName, selected: true
- });
- setCurrentFilter(newFilter);
- setFilterQuery(newDisplayName);
- executeFilterSearch(newDisplayName);
- if (searchOnSelect) {
- searchActions.setOffset(0);
- searchActions.resetFacets();
- executeSearch(searchActions);
- }
- } else {
- setFilterQuery(value);
- executeFilterSearch(value);
+ if (!newFilter || !newDisplayName) {
+ return;
+ }
+
+ if (onSelect) {
+ if (searchOnSelect) {
+ console.warn('Both searchOnSelect and onSelect props were passed to the component.'
+ + ' Using onSelect instead of searchOnSelect as the latter is deprecated.');
}
+ return onSelect({
+ newFilter,
+ newDisplayName,
+ currentFilter,
+ setCurrentFilter,
+ executeFilterSearch
+ });
}
- }, [currentFilter, searchActions, executeFilterSearch, searchOnSelect]);
- const handleSelectDropdown = useCallback((value, _index, itemData) => {
- handleDropdownEvent(value, itemData, true);
- }, [handleDropdownEvent]);
+ if (currentFilter) {
+ searchActions.setFilterOption({ filter: currentFilter, selected: false });
+ }
+ searchActions.setFilterOption({ filter: newFilter, displayName: newDisplayName, selected: true });
+ setCurrentFilter(newFilter);
+ executeFilterSearch(newDisplayName);
- const handleToggleDropdown =
- useCallback((isActive, _prevValue, value, _index, itemData) => {
- if (!isActive) {
- handleDropdownEvent(value, itemData, false);
+ if (searchOnSelect) {
+ searchActions.setOffset(0);
+ searchActions.resetFacets();
+ executeSearch(searchActions);
}
- }, [handleDropdownEvent]);
+ }, [currentFilter, searchActions, executeFilterSearch, onSelect, searchOnSelect]);
const meetsSubmitCritera = useCallback(index => index >= 0, []);
@@ -199,7 +222,6 @@ export function FilterSearch({
diff --git a/src/components/index.ts b/src/components/index.ts
index 48b32ea48..19d29f50a 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -35,7 +35,8 @@ export {
export {
FilterSearch,
FilterSearchCssClasses,
- FilterSearchProps
+ FilterSearchProps,
+ OnSelectParams
} from './FilterSearch';
export {
diff --git a/src/utils/filterutils.tsx b/src/utils/filterutils.tsx
index acbe1c8d6..7347bf8f5 100644
--- a/src/utils/filterutils.tsx
+++ b/src/utils/filterutils.tsx
@@ -1,4 +1,4 @@
-import { NearFilterValue, FieldValueFilter, NumberRangeValue, Matcher, SearchActions, DisplayableFacet, SelectableStaticFilter } from '@yext/search-headless-react';
+import { NearFilterValue, FieldValueFilter, NumberRangeValue, Matcher, SearchActions, DisplayableFacet, SelectableStaticFilter, StaticFilter } from '@yext/search-headless-react';
import { isEqual } from 'lodash';
import { isNumberRangeFilter } from '../models/NumberRangeFilter';
import { SelectableFieldValueFilter } from '../models/SelectableFieldValueFilter';
@@ -43,6 +43,26 @@ export function isDuplicateFieldValueFilter(
return true;
}
+/**
+ * Returns true if the two given static filters are the same.
+ */
+export function isDuplicateStaticFilter(thisFilter: StaticFilter, otherFilter: StaticFilter): boolean {
+ if (thisFilter.kind === 'fieldValue') {
+ return otherFilter.kind === 'fieldValue'
+ ? isDuplicateFieldValueFilter(thisFilter, otherFilter)
+ : false;
+ }
+
+ if (otherFilter.kind === 'fieldValue') {
+ return false;
+ }
+
+ return thisFilter.combinator === otherFilter.combinator
+ && thisFilter.filters.length === otherFilter.filters.length
+ && thisFilter.filters.every(t => otherFilter.filters.some(o => isDuplicateStaticFilter(t, o)))
+ && otherFilter.filters.every(o => thisFilter.filters.some(t => isDuplicateStaticFilter(o, t)));
+}
+
/**
* Finds the {@link SelectableFieldValueFilter} from the list provided that matches
* the given {@link FieldValueFilter}. If no matching {@link SelectableFieldValueFilter}
diff --git a/tests/components/FilterSearch.test.tsx b/tests/components/FilterSearch.test.tsx
index 59e1afdbb..56da0cc95 100644
--- a/tests/components/FilterSearch.test.tsx
+++ b/tests/components/FilterSearch.test.tsx
@@ -179,6 +179,26 @@ describe('search with section labels', () => {
});
});
+ it('executes onSelect function when a filter is selected', async () => {
+ const mockedOnSelect = jest.fn();
+ const setFilterOption = jest.spyOn(SearchHeadless.prototype, 'setFilterOption');
+ const executeFilterSearch = jest
+ .spyOn(SearchHeadless.prototype, 'executeFilterSearch')
+ .mockResolvedValue(labeledFilterSearchResponse);
+ renderFilterSearch({ searchFields: searchFieldsProp, onSelect: mockedOnSelect });
+ const searchBarElement = screen.getByRole('textbox');
+
+ userEvent.type(searchBarElement, 'n');
+ await waitFor(() => screen.findByText('first name 1'));
+
+ userEvent.type(searchBarElement, '{enter}');
+ await waitFor(() => {
+ expect(executeFilterSearch).toHaveBeenCalled();
+ });
+ expect(mockedOnSelect).toBeCalled();
+ expect(setFilterOption).not.toBeCalled();
+ });
+
describe('searchOnSelect = true', () => {
it('triggers a search on pressing "enter" when an autocomplete result is selected', async () => {
const mockExecuteSearch = jest.spyOn(searchOperations, 'executeSearch');
@@ -281,6 +301,37 @@ describe('search with section labels', () => {
expect(setOffsetCallOrder).toBeLessThan(mockExecuteSearchCallOrder);
expect(resetFacetsCallOrder).toBeLessThan(mockExecuteSearchCallOrder);
});
+
+ describe('onSelect prop is passed', () => {
+ it('ignores searchOnSelect, gives a warning, and calls onSelect when a filter is selected', async () => {
+ const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation();
+ const mockedOnSelect = jest.fn();
+ const setFilterOption = jest.spyOn(SearchHeadless.prototype, 'setFilterOption');
+ const mockExecuteSearch = jest.spyOn(searchOperations, 'executeSearch');
+ const executeFilterSearch = jest
+ .spyOn(SearchHeadless.prototype, 'executeFilterSearch')
+ .mockResolvedValue(labeledFilterSearchResponse);
+ renderFilterSearch({
+ searchFields: searchFieldsProp,
+ searchOnSelect: true,
+ onSelect: mockedOnSelect
+ });
+ const searchBarElement = screen.getByRole('textbox');
+
+ userEvent.type(searchBarElement, 'n');
+ await waitFor(() => screen.findByText('first name 1'));
+
+ userEvent.type(searchBarElement, '{enter}');
+ await waitFor(() => {
+ expect(executeFilterSearch).toHaveBeenCalled();
+ });
+ expect(mockedOnSelect).toBeCalled();
+ expect(setFilterOption).not.toBeCalled();
+ expect(mockExecuteSearch).not.toBeCalled();
+ expect(consoleWarnSpy).toBeCalledWith('Both searchOnSelect and onSelect props were passed to the component.'
+ + ' Using onSelect instead of searchOnSelect as the latter is deprecated.');
+ });
+ });
});
describe('searchOnSelect = false', () => {
@@ -507,4 +558,4 @@ it('toggling the dropdown does not change selected filters', async () => {
userEvent.click(externalDiv);
expect(setFilterOption).toBeCalledTimes(1);
-});
\ No newline at end of file
+});