Skip to content

Commit

Permalink
Add onSelect prop to FilterSearch (#323)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nmanu1 committed Nov 11, 2022
1 parent 1166991 commit 893368d
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 47 deletions.
4 changes: 2 additions & 2 deletions docs/search-ui-react.filtersearch.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ A component which allows a user to search for filters associated with specific e
<b>Signature:</b>

```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) | |

<b>Returns:</b>

Expand Down
1 change: 1 addition & 0 deletions docs/search-ui-react.filtersearchprops.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface FilterSearchProps
| --- | --- | --- |
| [customCssClasses?](./search-ui-react.filtersearchprops.customcssclasses.md) | [FilterSearchCssClasses](./search-ui-react.filtersearchcssclasses.md) | <i>(Optional)</i> CSS classes for customizing the component styling. |
| [label?](./search-ui-react.filtersearchprops.label.md) | string | <i>(Optional)</i> The display label for the component. |
| [onSelect?](./search-ui-react.filtersearchprops.onselect.md) | (params: [OnSelectParams](./search-ui-react.onselectparams.md)<!-- -->) =&gt; void | <i>(Optional)</i> A function which is called when a filter is selected. |
| [placeholder?](./search-ui-react.filtersearchprops.placeholder.md) | string | <i>(Optional)</i> 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&lt;SearchParameterField, 'fetchEntities'&gt;\[\] | An array of fieldApiName and entityType which indicates what to perform the filter search against. |
| [searchOnSelect?](./search-ui-react.filtersearchprops.searchonselect.md) | boolean | <i>(Optional)</i> Whether to trigger a search when an option is selected. Defaults to false. |
Expand Down
13 changes: 13 additions & 0 deletions docs/search-ui-react.filtersearchprops.onselect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [FilterSearchProps](./search-ui-react.filtersearchprops.md) &gt; [onSelect](./search-ui-react.filtersearchprops.onselect.md)

## FilterSearchProps.onSelect property

A function which is called when a filter is selected.

<b>Signature:</b>

```typescript
onSelect?: (params: OnSelectParams) => void;
```
5 changes: 5 additions & 0 deletions docs/search-ui-react.filtersearchprops.searchonselect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<b>Signature:</b>
Expand Down
3 changes: 2 additions & 1 deletion docs/search-ui-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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. |
Expand Down
13 changes: 13 additions & 0 deletions docs/search-ui-react.onselectparams.currentfilter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnSelectParams](./search-ui-react.onselectparams.md) &gt; [currentFilter](./search-ui-react.onselectparams.currentfilter.md)

## OnSelectParams.currentFilter property

The previously selected filter.

<b>Signature:</b>

```typescript
currentFilter: StaticFilter | undefined;
```
13 changes: 13 additions & 0 deletions docs/search-ui-react.onselectparams.executefiltersearch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnSelectParams](./search-ui-react.onselectparams.md) &gt; [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.

<b>Signature:</b>

```typescript
executeFilterSearch: (query?: string) => Promise<FilterSearchResponse | undefined>;
```
24 changes: 24 additions & 0 deletions docs/search-ui-react.onselectparams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnSelectParams](./search-ui-react.onselectparams.md)

## OnSelectParams interface

The parameters that are passed into [FilterSearchProps.onSelect](./search-ui-react.filtersearchprops.onselect.md)<!-- -->.

<b>Signature:</b>

```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) =&gt; Promise&lt;FilterSearchResponse \| undefined&gt; | 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) =&gt; void | A function that sets which filter the component is currently associated with. |

13 changes: 13 additions & 0 deletions docs/search-ui-react.onselectparams.newdisplayname.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnSelectParams](./search-ui-react.onselectparams.md) &gt; [newDisplayName](./search-ui-react.onselectparams.newdisplayname.md)

## OnSelectParams.newDisplayName property

The display name of the newly selected filter.

<b>Signature:</b>

```typescript
newDisplayName: string;
```
13 changes: 13 additions & 0 deletions docs/search-ui-react.onselectparams.newfilter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnSelectParams](./search-ui-react.onselectparams.md) &gt; [newFilter](./search-ui-react.onselectparams.newfilter.md)

## OnSelectParams.newFilter property

The newly selected filter.

<b>Signature:</b>

```typescript
newFilter: FieldValueStaticFilter;
```
13 changes: 13 additions & 0 deletions docs/search-ui-react.onselectparams.setcurrentfilter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnSelectParams](./search-ui-react.onselectparams.md) &gt; [setCurrentFilter](./search-ui-react.onselectparams.setcurrentfilter.md)

## OnSelectParams.setCurrentFilter property

A function that sets which filter the component is currently associated with.

<b>Signature:</b>

```typescript
setCurrentFilter: (filter: StaticFilter) => void;
```
16 changes: 15 additions & 1 deletion etc/search-ui-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -252,8 +255,10 @@ export interface FilterSearchCssClasses extends AutocompleteResultCssClasses {
export interface FilterSearchProps {
customCssClasses?: FilterSearchCssClasses;
label?: string;
onSelect?: (params: OnSelectParams) => void;
placeholder?: string;
searchFields: Omit<SearchParameterField, 'fetchEntities'>[];
// @deprecated
searchOnSelect?: boolean;
sectioned?: boolean;
}
Expand Down Expand Up @@ -365,6 +370,15 @@ export type onSearchFunc = (searchEventData: {
query?: string;
}) => void;

// @public
export interface OnSelectParams {
currentFilter: StaticFilter | undefined;
executeFilterSearch: (query?: string) => Promise<FilterSearchResponse | undefined>;
newDisplayName: string;
newFilter: FieldValueStaticFilter;
setCurrentFilter: (filter: StaticFilter) => void;
}

// @public
export function Pagination(props: PaginationProps): JSX.Element | null;

Expand Down
102 changes: 62 additions & 40 deletions src/components/FilterSearch.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -34,6 +34,27 @@ const builtInCssClasses: Readonly<FilterSearchCssClasses> = {
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<FilterSearchResponse | undefined>
}

/**
* The props for the {@link FilterSearch} component.
*
Expand All @@ -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. */
Expand All @@ -70,6 +97,7 @@ export function FilterSearch({
label,
placeholder = 'Search here...',
searchOnSelect,
onSelect,
sectioned = false,
customCssClasses
}: FilterSearchProps): JSX.Element {
Expand All @@ -78,13 +106,9 @@ export function FilterSearch({
return { ...searchField, fetchEntities: false };
});
const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
const [currentFilter, setCurrentFilter] = useState<FieldValueStaticFilter>();
const [currentFilter, setCurrentFilter] = useState<StaticFilter>();
const [filterQuery, setFilterQuery] = useState<string>();
const staticFilters = useSearchState(state => state.filters.static);
const fieldValueFilters = useMemo(
() => getSelectableFieldValueFilters(staticFilters ?? []),
[staticFilters]
);

const [
filterSearchResponse,
Expand All @@ -99,56 +123,55 @@ 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) ?? [];
}, [filterSearchResponse?.sections]);

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, []);

Expand Down Expand Up @@ -199,7 +222,6 @@ export function FilterSearch({
<Dropdown
screenReaderText={getScreenReaderText(sections)}
onSelect={handleSelectDropdown}
onToggle={handleToggleDropdown}
alwaysSelectOption={true}
parentQuery={filterQuery}
>
Expand Down
3 changes: 2 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export {
export {
FilterSearch,
FilterSearchCssClasses,
FilterSearchProps
FilterSearchProps,
OnSelectParams
} from './FilterSearch';

export {
Expand Down
Loading

0 comments on commit 893368d

Please sign in to comment.