diff --git a/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png b/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png index 5649ea189..9fe7fcd1e 100644 Binary files a/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png and b/.storybook/snapshots/__snapshots__/mapboxmap--multiple-pins.png differ diff --git a/docs/search-ui-react.filtersearch.md b/docs/search-ui-react.filtersearch.md index 2afb7a96d..ce274298f 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 -declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; +declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, onDropdownInputChange, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses } | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | | +| { searchFields, label, placeholder, searchOnSelect, onSelect, onDropdownInputChange, 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 8242d65e1..c3f08ccf4 100644 --- a/docs/search-ui-react.filtersearchprops.md +++ b/docs/search-ui-react.filtersearchprops.md @@ -18,6 +18,7 @@ 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. | +| [onDropdownInputChange?](./search-ui-react.filtersearchprops.ondropdowninputchange.md) | | (params: [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md)) => void | _(Optional)_ A function which is called when the input element's value changes. Replaces the default behavior. | | [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. | diff --git a/docs/search-ui-react.filtersearchprops.ondropdowninputchange.md b/docs/search-ui-react.filtersearchprops.ondropdowninputchange.md new file mode 100644 index 000000000..188250a4a --- /dev/null +++ b/docs/search-ui-react.filtersearchprops.ondropdowninputchange.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FilterSearchProps](./search-ui-react.filtersearchprops.md) > [onDropdownInputChange](./search-ui-react.filtersearchprops.ondropdowninputchange.md) + +## FilterSearchProps.onDropdownInputChange property + +A function which is called when the input element's value changes. Replaces the default behavior. + +**Signature:** + +```typescript +onDropdownInputChange?: (params: OnDropdownInputChangeProps) => void; +``` diff --git a/docs/search-ui-react.md b/docs/search-ui-react.md index b4ec09bc3..e0acb9338 100644 --- a/docs/search-ui-react.md +++ b/docs/search-ui-react.md @@ -18,7 +18,7 @@ | [executeSearch(searchActions)](./search-ui-react.executesearch.md) | Executes a universal/vertical search. | | [Facets(props)](./search-ui-react.facets.md) | A component that displays all facets applicable to the current 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, onSelect, 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, onDropdownInputChange, sectioned, customCssClasses })](./search-ui-react.filtersearch.md) | A component which allows a user to search for filters associated with specific entities and fields. | | [Geolocation\_2({ geolocationOptions, radius, label, GeolocationIcon, handleClick, customCssClasses, })](./search-ui-react.geolocation_2.md) | A React Component which collects location information to create a location filter and perform a new search. | | [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. | @@ -84,6 +84,7 @@ | [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) | Props for the [StandardFacet()](./search-ui-react.standardfacet.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. | +| [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md) | The parameters that are passed into [FilterSearchProps.onDropdownInputChange](./search-ui-react.filtersearchprops.ondropdowninputchange.md). | | [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 | diff --git a/docs/search-ui-react.ondropdowninputchangeprops.executefiltersearch.md b/docs/search-ui-react.ondropdowninputchangeprops.executefiltersearch.md new file mode 100644 index 000000000..7ddd0d295 --- /dev/null +++ b/docs/search-ui-react.ondropdowninputchangeprops.executefiltersearch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md) > [executeFilterSearch](./search-ui-react.ondropdowninputchangeprops.executefiltersearch.md) + +## OnDropdownInputChangeProps.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.ondropdowninputchangeprops.md b/docs/search-ui-react.ondropdowninputchangeprops.md new file mode 100644 index 000000000..a5d704094 --- /dev/null +++ b/docs/search-ui-react.ondropdowninputchangeprops.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md) + +## OnDropdownInputChangeProps interface + +The parameters that are passed into [FilterSearchProps.onDropdownInputChange](./search-ui-react.filtersearchprops.ondropdowninputchange.md). + +**Signature:** + +```typescript +interface OnDropdownInputChangeProps +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [executeFilterSearch](./search-ui-react.ondropdowninputchangeprops.executefiltersearch.md) | | (query?: string) => Promise<FilterSearchResponse \| undefined> | A function that executes a filter search and updates the input and dropdown options with the response. | +| [value](./search-ui-react.ondropdowninputchangeprops.value.md) | | string | The input element's new value after the change | + diff --git a/docs/search-ui-react.ondropdowninputchangeprops.value.md b/docs/search-ui-react.ondropdowninputchangeprops.value.md new file mode 100644 index 000000000..b3d64d4c4 --- /dev/null +++ b/docs/search-ui-react.ondropdowninputchangeprops.value.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md) > [value](./search-ui-react.ondropdowninputchangeprops.value.md) + +## OnDropdownInputChangeProps.value property + +The input element's new value after the change + +**Signature:** + +```typescript +value: string; +``` diff --git a/etc/search-ui-react.api.md b/etc/search-ui-react.api.md index cc5f7ecc2..76865d2a1 100644 --- a/etc/search-ui-react.api.md +++ b/etc/search-ui-react.api.md @@ -268,7 +268,7 @@ export interface FilterOptionConfig { } // @public -export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; +export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, onDropdownInputChange, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; // @public export interface FilterSearchCssClasses extends AutocompleteResultCssClasses { @@ -290,6 +290,7 @@ export interface FilterSearchCssClasses extends AutocompleteResultCssClasses { export interface FilterSearchProps { customCssClasses?: FilterSearchCssClasses; label?: string; + onDropdownInputChange?: (params: OnDropdownInputChangeProps) => void; onSelect?: (params: OnSelectParams) => void; placeholder?: string; searchFields: Omit[]; @@ -461,6 +462,12 @@ export interface NumericalFacetsProps extends Omit void; +// @public +export interface OnDropdownInputChangeProps { + executeFilterSearch: (query?: string) => Promise; + value: string; +} + // @public export type onSearchFunc = (searchEventData: { verticalKey?: string; diff --git a/src/components/FilterSearch.tsx b/src/components/FilterSearch.tsx index 7fd421ef4..18b9b9b04 100644 --- a/src/components/FilterSearch.tsx +++ b/src/components/FilterSearch.tsx @@ -55,6 +55,21 @@ export interface OnSelectParams { executeFilterSearch: (query?: string) => Promise } +/** + * The parameters that are passed into {@link FilterSearchProps.onDropdownInputChange}. + * + * @public + */ +export interface OnDropdownInputChangeProps { + /** The input element's new value after the change */ + value: string, + /** + * 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. * @@ -78,6 +93,8 @@ export interface FilterSearchProps { searchOnSelect?: boolean, /** A function which is called when a filter is selected. */ onSelect?: (params: OnSelectParams) => void, + /** A function which is called when the input element's value changes. Replaces the default behavior. */ + onDropdownInputChange?: (params: OnDropdownInputChangeProps) => 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. */ @@ -98,6 +115,7 @@ export function FilterSearch({ placeholder = 'Search here...', searchOnSelect, onSelect, + onDropdownInputChange, sectioned = false, customCssClasses }: FilterSearchProps): JSX.Element { @@ -224,6 +242,16 @@ export function FilterSearch({ matchingFieldIds ]); + const handleInputChange = useCallback((value) => { + onDropdownInputChange ? onDropdownInputChange({ + value, + executeFilterSearch + }) : executeFilterSearch(value); + }, [ + onDropdownInputChange, + executeFilterSearch + ]); + const meetsSubmitCritera = useCallback(index => index >= 0, []); const itemDataMatrix = useMemo(() => { @@ -279,7 +307,7 @@ export function FilterSearch({ diff --git a/src/components/index.ts b/src/components/index.ts index a44d52e21..8ee06ebca 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -36,7 +36,8 @@ export { FilterSearch, FilterSearchCssClasses, FilterSearchProps, - OnSelectParams + OnSelectParams, + OnDropdownInputChangeProps } from './FilterSearch'; export { diff --git a/test-site/src/pages/PeoplePage.tsx b/test-site/src/pages/PeoplePage.tsx index 91c6befdd..81a329f87 100644 --- a/test-site/src/pages/PeoplePage.tsx +++ b/test-site/src/pages/PeoplePage.tsx @@ -1,5 +1,5 @@ import { useLayoutEffect } from 'react'; -import { useSearchActions } from '@yext/search-headless-react'; +import { FieldValueStaticFilter, SelectableStaticFilter, useSearchActions } from '@yext/search-headless-react'; import { AppliedFilters, FilterSearch, @@ -19,12 +19,14 @@ import { NumericalFacets, AlternativeVerticals, StandardFacet, - NumericalFacet + NumericalFacet, + OnDropdownInputChangeProps } from '@yext/search-ui-react'; // import { CustomCard } from '../components/CustomCard'; const hierarchicalFacetFieldIds = ['c_hierarchicalFacet']; const filterSearchFields = [{ fieldApiName: 'name', entityType: 'ce_person' }]; +const employeeFilterSearchFields = [{fieldApiName: 'c_employeeDepartment', entityType: 'ce_person'}]; const employeeFilterConfigs = [ { value: 'Consulting' }, { value: 'Technology' } @@ -43,6 +45,31 @@ export function PeoplePage() { searchActions.executeVerticalQuery(); }); + /** + * This example function that's being used for onDropdownInputChange allows for clearing the filter in the search state when the input is empty. + * This is especially useful for implementations that have multiple FilterSearch components. + * Ex. a user can search using both inputs initially, but then wants to clear one of the FilterSearch inputs and re-run a search. + */ + const removeAssociatedFilterWhenInputIsEmpty = (searchFields: { fieldApiName: string; entityType: string; }[]) => (params: OnDropdownInputChangeProps) => { + const { value, executeFilterSearch } = params; + // If there is still an input value, execute the filter search as normal + if (value !== "") { + executeFilterSearch(value); + } + // When the input is empty, remove the associated filter from the search state while keeping any other filters that are applied. + else { + const fieldIds = searchFields.map((field: {fieldApiName: string, entityType: string}) => field.fieldApiName); + const filtersToKeep: SelectableStaticFilter[] = []; + searchActions.state.filters.static?.forEach((staticFilter) => { + const filter = staticFilter.filter as FieldValueStaticFilter; + if (!fieldIds.includes(filter.fieldId)) { + filtersToKeep.push(staticFilter); + } + }); + searchActions.setStaticFilters(filtersToKeep); + } + } + return (
@@ -51,7 +78,14 @@ export function PeoplePage() { + = { } }, args: { - label: 'Filter' + label: 'Filter', + onDropdownInputChange: undefined, } }; export default meta; diff --git a/tests/components/FilterSearch.test.tsx b/tests/components/FilterSearch.test.tsx index fcbce6c4e..f0fffe3a6 100644 --- a/tests/components/FilterSearch.test.tsx +++ b/tests/components/FilterSearch.test.tsx @@ -668,6 +668,16 @@ describe('search without section labels', () => { userEvent.keyboard('{enter}'); expect(inputNode).toHaveValue('first name 1'); }); + + it('when an onDropdownInputChange prop is specified, it gets called each time after the input changes and executeFilterSearch does not', async () => { + const mockedOnDropdownInputChange = jest.fn(); + const executeFilterSearch = jest + .spyOn(SearchHeadless.prototype, 'executeFilterSearch'); + renderFilterSearch({ searchFields: searchFieldsProp, onDropdownInputChange: mockedOnDropdownInputChange}); + userEvent.type(screen.getByRole('textbox'), 'a'); + await waitFor(() => expect(mockedOnDropdownInputChange).toHaveBeenCalledTimes(1)); + expect(executeFilterSearch).toHaveBeenCalledTimes(0); + }) }); describe('screen reader', () => {