diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4ead6d09d..3eab2d352 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,5 +1,5 @@ # This workflow will run our tests, generate an lcov code coverage file, -# and send that coverage to Coveralls +# and send that coverage to Coveralls name: Code Coverage diff --git a/.github/workflows/sync-sites-branch.yml b/.github/workflows/sync-sites-branch.yml index dbee82df6..5ceccf24f 100644 --- a/.github/workflows/sync-sites-branch.yml +++ b/.github/workflows/sync-sites-branch.yml @@ -6,21 +6,12 @@ on: - main jobs: - sync-branches: - runs-on: ubuntu-latest - name: Syncing branches - steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: devmasx/merge-branch@v1.4.0 - with: - type: now - from_branch: ${{ github.event.repository.default_branch }} - target_branch: storybook-site - github_token: ${{ secrets.GITHUB_TOKEN }} - - uses: devmasx/merge-branch@v1.4.0 - with: - type: now - from_branch: ${{ github.event.repository.default_branch }} - target_branch: test-site - github_token: ${{ secrets.GITHUB_TOKEN }} + call_sync_branches: + strategy: + matrix: + target_branch: ["storybook-site", "test-site"] + uses: yext/slapshot-reusable-workflows/.github/workflows/sync_default_branch.yml@v1 + with: + target_branch: ${{ matrix.target_branch }} + secrets: + caller_github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.storybook/main.js b/.storybook/main.js index 545d0e4d2..0b27d3606 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -3,6 +3,7 @@ module.exports = { '../tests/**/*.stories.tsx' ], addons: [ + '@etchteam/storybook-addon-status', '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions', diff --git a/.storybook/preview.js b/.storybook/preview.js index 9bf99b604..aba45a122 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -27,6 +27,10 @@ export const parameters = { 'DirectAnswer', 'FilterSearch', 'StaticFilters', + 'Facets', + 'StandardFacet', + 'NumericalFacet', + 'HierarchicalFacet', 'StandardFacets', 'NumericalFacets', 'HierarchicalFacets', @@ -36,6 +40,7 @@ export const parameters = { 'AlternativeVerticals', 'SpellCheck', 'ResultsCount', + 'Geolocation', 'LocationBias', 'Dropdown' ] diff --git a/README.md b/README.md index 680e377fe..d5b509ac5 100644 --- a/README.md +++ b/README.md @@ -78,4 +78,8 @@ To use the Component Library's Styling without adding Tailwind to your project, ```tsx import '@yext/search-ui-react/bundle.css' -``` \ No newline at end of file +``` + +## Compatibility Notes + +This library and its dependencies use optional chaining and other modern TS syntax that is not inherently supported by Webpack <5 (e.g. via `create-react-app@4`). Additional Babel plugins are needed for transpiling if using legacy versions. \ No newline at end of file diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 17221cb2e..934140ddd 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -979,7 +979,7 @@ These definitions were written by Chi Vinh Le . The following NPM package may be included in this product: - - @yext/analytics@0.2.0-beta.3 + - @yext/analytics@0.5.0 This package contains the following license and notice below: @@ -1020,7 +1020,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following NPM package may be included in this product: - - @yext/search-core@2.3.0 + - @yext/search-core@2.4.0 This package contains the following license and notice below: @@ -1064,7 +1064,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following NPM package may be included in this product: - - @yext/search-headless-react@2.2.0 + - @yext/search-headless-react@2.3.0 This package contains the following license and notice below: @@ -1108,7 +1108,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following NPM package may be included in this product: - - @yext/search-headless@2.3.0 + - @yext/search-headless@2.4.0 This package contains the following license and notice below: diff --git a/docs/search-ui-react.facetprops.md b/docs/search-ui-react.facetprops.md new file mode 100644 index 000000000..c0f1e3594 --- /dev/null +++ b/docs/search-ui-react.facetprops.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetProps](./search-ui-react.facetprops.md) + +## FacetProps type + +Props for a single facet component. + +Signature: + +```typescript +export declare type FacetProps = StandardFacetProps | NumericalFacetProps | HierarchicalFacetProps; +``` +References: [StandardFacetProps](./search-ui-react.standardfacetprops.md), [NumericalFacetProps](./search-ui-react.numericalfacetprops.md), [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) + diff --git a/docs/search-ui-react.facets.md b/docs/search-ui-react.facets.md new file mode 100644 index 000000000..09a361d52 --- /dev/null +++ b/docs/search-ui-react.facets.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [Facets](./search-ui-react.facets.md) + +## Facets() function + +A component that displays all facets applicable to the current vertical search. + +Signature: + +```typescript +export declare function Facets(props: FacetsProps): JSX.Element; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| props | [FacetsProps](./search-ui-react.facetsprops.md) | [FacetsProps](./search-ui-react.facetsprops.md) | + +Returns: + +JSX.Element + +A React component for facets + +## Remarks + +This component is a quick way of getting facets on the page, and it will render standard facets, numerical facets, and hierarchical facets. The [StandardFacet()](./search-ui-react.standardfacet.md), [NumericalFacet()](./search-ui-react.numericalfacet.md), and [HierarchicalFacet()](./search-ui-react.hierarchicalfacet.md) components can be used to override the default facet configuration. + diff --git a/docs/search-ui-react.facetscssclasses.divider.md b/docs/search-ui-react.facetscssclasses.divider.md new file mode 100644 index 000000000..7a478a5f7 --- /dev/null +++ b/docs/search-ui-react.facetscssclasses.divider.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsCssClasses](./search-ui-react.facetscssclasses.md) > [divider](./search-ui-react.facetscssclasses.divider.md) + +## FacetsCssClasses.divider property + +Signature: + +```typescript +divider?: string; +``` diff --git a/docs/search-ui-react.facetscssclasses.facetscontainer.md b/docs/search-ui-react.facetscssclasses.facetscontainer.md new file mode 100644 index 000000000..1e90d3795 --- /dev/null +++ b/docs/search-ui-react.facetscssclasses.facetscontainer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsCssClasses](./search-ui-react.facetscssclasses.md) > [facetsContainer](./search-ui-react.facetscssclasses.facetscontainer.md) + +## FacetsCssClasses.facetsContainer property + +Signature: + +```typescript +facetsContainer?: string; +``` diff --git a/docs/search-ui-react.facetscssclasses.md b/docs/search-ui-react.facetscssclasses.md new file mode 100644 index 000000000..e78debde6 --- /dev/null +++ b/docs/search-ui-react.facetscssclasses.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsCssClasses](./search-ui-react.facetscssclasses.md) + +## FacetsCssClasses interface + +The CSS class interface for [Facets()](./search-ui-react.facets.md). Any [FilterGroupCssClasses](./search-ui-react.filtergroupcssclasses.md) props will be overridden by the same props from customCssClasses on [StandardFacetProps](./search-ui-react.standardfacetprops.md), [NumericalFacetProps](./search-ui-react.numericalfacetprops.md), or [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md). + +Signature: + +```typescript +export interface FacetsCssClasses extends FilterGroupCssClasses +``` +Extends: [FilterGroupCssClasses](./search-ui-react.filtergroupcssclasses.md) + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [divider?](./search-ui-react.facetscssclasses.divider.md) | string | (Optional) | +| [facetsContainer?](./search-ui-react.facetscssclasses.facetscontainer.md) | string | (Optional) | + diff --git a/docs/search-ui-react.facetsprops.children.md b/docs/search-ui-react.facetsprops.children.md new file mode 100644 index 000000000..1f661f275 --- /dev/null +++ b/docs/search-ui-react.facetsprops.children.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) > [children](./search-ui-react.facetsprops.children.md) + +## FacetsProps.children property + +The custom facet components that will override the default rendering. + +Signature: + +```typescript +children?: ReactElement[] | ReactElement | undefined | null; +``` + +## Remarks + +Supported components include [StandardFacet()](./search-ui-react.standardfacet.md), [NumericalFacet()](./search-ui-react.numericalfacet.md). + diff --git a/docs/search-ui-react.facetsprops.customcssclasses.md b/docs/search-ui-react.facetsprops.customcssclasses.md new file mode 100644 index 000000000..67d9622f6 --- /dev/null +++ b/docs/search-ui-react.facetsprops.customcssclasses.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) > [customCssClasses](./search-ui-react.facetsprops.customcssclasses.md) + +## FacetsProps.customCssClasses property + +CSS classes for customizing the component styling. + +Signature: + +```typescript +customCssClasses?: FacetsCssClasses; +``` diff --git a/docs/search-ui-react.facetsprops.excludedfieldids.md b/docs/search-ui-react.facetsprops.excludedfieldids.md new file mode 100644 index 000000000..35c5f2335 --- /dev/null +++ b/docs/search-ui-react.facetsprops.excludedfieldids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) > [excludedFieldIds](./search-ui-react.facetsprops.excludedfieldids.md) + +## FacetsProps.excludedFieldIds property + +List of field ids that should not be displayed. + +Signature: + +```typescript +excludedFieldIds?: string[]; +``` diff --git a/docs/search-ui-react.facetsprops.hierarchicalfieldids.md b/docs/search-ui-react.facetsprops.hierarchicalfieldids.md new file mode 100644 index 000000000..615e21cf1 --- /dev/null +++ b/docs/search-ui-react.facetsprops.hierarchicalfieldids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) > [hierarchicalFieldIds](./search-ui-react.facetsprops.hierarchicalfieldids.md) + +## FacetsProps.hierarchicalFieldIds property + +List of field ids that should be rendered as hierarchical facets. + +Signature: + +```typescript +hierarchicalFieldIds?: string[]; +``` diff --git a/docs/search-ui-react.facetsprops.md b/docs/search-ui-react.facetsprops.md new file mode 100644 index 000000000..926b794a7 --- /dev/null +++ b/docs/search-ui-react.facetsprops.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) + +## FacetsProps interface + +Props for the [Facets()](./search-ui-react.facets.md) component. + +Signature: + +```typescript +export interface FacetsProps +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [children?](./search-ui-react.facetsprops.children.md) | ReactElement\[\] \| ReactElement \| undefined \| null | (Optional) The custom facet components that will override the default rendering. | +| [customCssClasses?](./search-ui-react.facetsprops.customcssclasses.md) | [FacetsCssClasses](./search-ui-react.facetscssclasses.md) | (Optional) CSS classes for customizing the component styling. | +| [excludedFieldIds?](./search-ui-react.facetsprops.excludedfieldids.md) | string\[\] | (Optional) List of field ids that should not be displayed. | +| [hierarchicalFieldIds?](./search-ui-react.facetsprops.hierarchicalfieldids.md) | string\[\] | (Optional) List of field ids that should be rendered as hierarchical facets. | +| [onlyRenderChildren?](./search-ui-react.facetsprops.onlyrenderchildren.md) | boolean | (Optional) If set to true, only the facets specified in the children are rendered. If set to false, all facets are rendered with the ones specified in the children overridden. Default to false. | +| [searchOnChange?](./search-ui-react.facetsprops.searchonchange.md) | boolean | (Optional) Whether or not a search is automatically run when a filter is selected. Defaults to true. | + diff --git a/docs/search-ui-react.facetsprops.onlyrenderchildren.md b/docs/search-ui-react.facetsprops.onlyrenderchildren.md new file mode 100644 index 000000000..f836cdc32 --- /dev/null +++ b/docs/search-ui-react.facetsprops.onlyrenderchildren.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) > [onlyRenderChildren](./search-ui-react.facetsprops.onlyrenderchildren.md) + +## FacetsProps.onlyRenderChildren property + +If set to true, only the facets specified in the children are rendered. If set to false, all facets are rendered with the ones specified in the children overridden. Default to false. + +Signature: + +```typescript +onlyRenderChildren?: boolean; +``` diff --git a/docs/search-ui-react.facetsprops.searchonchange.md b/docs/search-ui-react.facetsprops.searchonchange.md new file mode 100644 index 000000000..90c0cb05e --- /dev/null +++ b/docs/search-ui-react.facetsprops.searchonchange.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FacetsProps](./search-ui-react.facetsprops.md) > [searchOnChange](./search-ui-react.facetsprops.searchonchange.md) + +## FacetsProps.searchOnChange property + +Whether or not a search is automatically run when a filter is selected. Defaults to true. + +Signature: + +```typescript +searchOnChange?: boolean; +``` diff --git a/docs/search-ui-react.geolocation_2.md b/docs/search-ui-react.geolocation_2.md new file mode 100644 index 000000000..5143a96a4 --- /dev/null +++ b/docs/search-ui-react.geolocation_2.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [Geolocation\_2](./search-ui-react.geolocation_2.md) + +## Geolocation\_2() function + +A React Component which collects location information to create a location filter and perform a new search. + +Signature: + +```typescript +export declare function Geolocation({ geolocationOptions, radius, label, GeolocationIcon, handleClick, customCssClasses, }: GeolocationProps): JSX.Element | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { geolocationOptions, radius, label, GeolocationIcon, handleClick, customCssClasses, } | [GeolocationProps](./search-ui-react.geolocationprops.md) | | + +Returns: + +JSX.Element \| null + +A react component for geolocation + diff --git a/docs/search-ui-react.geolocationcssclasses.button.md b/docs/search-ui-react.geolocationcssclasses.button.md new file mode 100644 index 000000000..a140dab75 --- /dev/null +++ b/docs/search-ui-react.geolocationcssclasses.button.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationCssClasses](./search-ui-react.geolocationcssclasses.md) > [button](./search-ui-react.geolocationcssclasses.button.md) + +## GeolocationCssClasses.button property + +Signature: + +```typescript +button?: string; +``` diff --git a/docs/search-ui-react.geolocationcssclasses.geolocationcontainer.md b/docs/search-ui-react.geolocationcssclasses.geolocationcontainer.md new file mode 100644 index 000000000..377c506d5 --- /dev/null +++ b/docs/search-ui-react.geolocationcssclasses.geolocationcontainer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationCssClasses](./search-ui-react.geolocationcssclasses.md) > [geolocationContainer](./search-ui-react.geolocationcssclasses.geolocationcontainer.md) + +## GeolocationCssClasses.geolocationContainer property + +Signature: + +```typescript +geolocationContainer?: string; +``` diff --git a/docs/search-ui-react.geolocationcssclasses.iconcontainer.md b/docs/search-ui-react.geolocationcssclasses.iconcontainer.md new file mode 100644 index 000000000..467401468 --- /dev/null +++ b/docs/search-ui-react.geolocationcssclasses.iconcontainer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationCssClasses](./search-ui-react.geolocationcssclasses.md) > [iconContainer](./search-ui-react.geolocationcssclasses.iconcontainer.md) + +## GeolocationCssClasses.iconContainer property + +Signature: + +```typescript +iconContainer?: string; +``` diff --git a/docs/search-ui-react.geolocationcssclasses.md b/docs/search-ui-react.geolocationcssclasses.md new file mode 100644 index 000000000..d56002fda --- /dev/null +++ b/docs/search-ui-react.geolocationcssclasses.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationCssClasses](./search-ui-react.geolocationcssclasses.md) + +## GeolocationCssClasses interface + +The CSS class interface for the Geolocation component. + +Signature: + +```typescript +export interface GeolocationCssClasses +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [button?](./search-ui-react.geolocationcssclasses.button.md) | string | (Optional) | +| [geolocationContainer?](./search-ui-react.geolocationcssclasses.geolocationcontainer.md) | string | (Optional) | +| [iconContainer?](./search-ui-react.geolocationcssclasses.iconcontainer.md) | string | (Optional) | + diff --git a/docs/search-ui-react.geolocationprops.customcssclasses.md b/docs/search-ui-react.geolocationprops.customcssclasses.md new file mode 100644 index 000000000..b4b7a71f1 --- /dev/null +++ b/docs/search-ui-react.geolocationprops.customcssclasses.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationProps](./search-ui-react.geolocationprops.md) > [customCssClasses](./search-ui-react.geolocationprops.customcssclasses.md) + +## GeolocationProps.customCssClasses property + +CSS classes for customizing the component styling. + +Signature: + +```typescript +customCssClasses?: GeolocationCssClasses; +``` diff --git a/docs/search-ui-react.geolocationprops.geolocationicon.md b/docs/search-ui-react.geolocationprops.geolocationicon.md new file mode 100644 index 000000000..afcf079c9 --- /dev/null +++ b/docs/search-ui-react.geolocationprops.geolocationicon.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationProps](./search-ui-react.geolocationprops.md) > [GeolocationIcon](./search-ui-react.geolocationprops.geolocationicon.md) + +## GeolocationProps.GeolocationIcon property + +Custom icon component to display along with the button. + +Signature: + +```typescript +GeolocationIcon?: React.FunctionComponent; +``` diff --git a/docs/search-ui-react.geolocationprops.geolocationoptions.md b/docs/search-ui-react.geolocationprops.geolocationoptions.md new file mode 100644 index 000000000..2b3a2020f --- /dev/null +++ b/docs/search-ui-react.geolocationprops.geolocationoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationProps](./search-ui-react.geolocationprops.md) > [geolocationOptions](./search-ui-react.geolocationprops.geolocationoptions.md) + +## GeolocationProps.geolocationOptions property + +Configuration used when collecting the user's location. Definition: [https://w3c.github.io/geolocation-api/\#position\_options\_interface](https://w3c.github.io/geolocation-api/#position_options_interface). + +Signature: + +```typescript +geolocationOptions?: PositionOptions; +``` diff --git a/docs/search-ui-react.geolocationprops.handleclick.md b/docs/search-ui-react.geolocationprops.handleclick.md new file mode 100644 index 000000000..5e86891a8 --- /dev/null +++ b/docs/search-ui-react.geolocationprops.handleclick.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationProps](./search-ui-react.geolocationprops.md) > [handleClick](./search-ui-react.geolocationprops.handleclick.md) + +## GeolocationProps.handleClick property + +A function which is called when the geolocation button is clicked, after user's position is successfully determined. + +Signature: + +```typescript +handleClick?: (position: GeolocationPosition) => void; +``` diff --git a/docs/search-ui-react.geolocationprops.label.md b/docs/search-ui-react.geolocationprops.label.md new file mode 100644 index 000000000..128d01079 --- /dev/null +++ b/docs/search-ui-react.geolocationprops.label.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationProps](./search-ui-react.geolocationprops.md) > [label](./search-ui-react.geolocationprops.label.md) + +## GeolocationProps.label property + +The label for the button. Defaults to 'Use my location'. + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/search-ui-react.geolocationprops.md b/docs/search-ui-react.geolocationprops.md new file mode 100644 index 000000000..565771b33 --- /dev/null +++ b/docs/search-ui-react.geolocationprops.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationProps](./search-ui-react.geolocationprops.md) + +## GeolocationProps interface + +The props for the Geolocation component. + +Signature: + +```typescript +export interface GeolocationProps +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [customCssClasses?](./search-ui-react.geolocationprops.customcssclasses.md) | [GeolocationCssClasses](./search-ui-react.geolocationcssclasses.md) | (Optional) CSS classes for customizing the component styling. | +| [GeolocationIcon?](./search-ui-react.geolocationprops.geolocationicon.md) | React.FunctionComponent | (Optional) Custom icon component to display along with the button. | +| [geolocationOptions?](./search-ui-react.geolocationprops.geolocationoptions.md) | PositionOptions | (Optional) Configuration used when collecting the user's location. Definition: [https://w3c.github.io/geolocation-api/\#position\_options\_interface](https://w3c.github.io/geolocation-api/#position_options_interface). | +| [handleClick?](./search-ui-react.geolocationprops.handleclick.md) | (position: GeolocationPosition) => void | (Optional) A function which is called when the geolocation button is clicked, after user's position is successfully determined. | +| [label?](./search-ui-react.geolocationprops.label.md) | string | (Optional) The label for the button. Defaults to 'Use my location'. | +| [radius?](./search-ui-react.geolocationprops.radius.md) | number | (Optional) The radius, in miles, around the user's location to find results. Defaults to 50. If location accuracy is low, a larger radius may be used automatically. | + diff --git a/docs/search-ui-react.geolocationprops.radius.md b/docs/search-ui-react.geolocationprops.radius.md new file mode 100644 index 000000000..62d171e06 --- /dev/null +++ b/docs/search-ui-react.geolocationprops.radius.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [GeolocationProps](./search-ui-react.geolocationprops.md) > [radius](./search-ui-react.geolocationprops.radius.md) + +## GeolocationProps.radius property + +The radius, in miles, around the user's location to find results. Defaults to 50. If location accuracy is low, a larger radius may be used automatically. + +Signature: + +```typescript +radius?: number; +``` diff --git a/docs/search-ui-react.hierarchicalfacet.md b/docs/search-ui-react.hierarchicalfacet.md new file mode 100644 index 000000000..448f86662 --- /dev/null +++ b/docs/search-ui-react.hierarchicalfacet.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [HierarchicalFacet](./search-ui-react.hierarchicalfacet.md) + +## HierarchicalFacet() function + +A component that displays a single hierarchical facet, in a tree level structure, applicable to the current vertical search. Use this to override the default rendering. + +Signature: + +```typescript +export declare function HierarchicalFacet(props: HierarchicalFacetProps): null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| props | [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) | [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) | + +Returns: + +null + +ReactElement + diff --git a/docs/search-ui-react.hierarchicalfacetprops.customcssclasses.md b/docs/search-ui-react.hierarchicalfacetprops.customcssclasses.md new file mode 100644 index 000000000..95f63b014 --- /dev/null +++ b/docs/search-ui-react.hierarchicalfacetprops.customcssclasses.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) > [customCssClasses](./search-ui-react.hierarchicalfacetprops.customcssclasses.md) + +## HierarchicalFacetProps.customCssClasses property + +CSS classes for customizing the component styling. + +Signature: + +```typescript +customCssClasses?: HierarchicalFacetCustomCssClasses; +``` diff --git a/docs/search-ui-react.hierarchicalfacetprops.delimiter.md b/docs/search-ui-react.hierarchicalfacetprops.delimiter.md new file mode 100644 index 000000000..571bc3323 --- /dev/null +++ b/docs/search-ui-react.hierarchicalfacetprops.delimiter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) > [delimiter](./search-ui-react.hierarchicalfacetprops.delimiter.md) + +## HierarchicalFacetProps.delimiter property + +The delimiter for determining facet hierarchies, defaults to ">". + +Signature: + +```typescript +delimiter?: string; +``` diff --git a/docs/search-ui-react.hierarchicalfacetprops.md b/docs/search-ui-react.hierarchicalfacetprops.md new file mode 100644 index 000000000..1ec8e98f5 --- /dev/null +++ b/docs/search-ui-react.hierarchicalfacetprops.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) + +## HierarchicalFacetProps interface + +Props for the [StandardFacet()](./search-ui-react.standardfacet.md) component. + +Signature: + +```typescript +export interface HierarchicalFacetProps extends Omit +``` +Extends: Omit<[StandardFacetProps](./search-ui-react.standardfacetprops.md), 'transformOptions' \| 'showOptionCounts'> + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [customCssClasses?](./search-ui-react.hierarchicalfacetprops.customcssclasses.md) | HierarchicalFacetCustomCssClasses | (Optional) CSS classes for customizing the component styling. | +| [delimiter?](./search-ui-react.hierarchicalfacetprops.delimiter.md) | string | (Optional) The delimiter for determining facet hierarchies, defaults to ">". | +| [showMoreLimit?](./search-ui-react.hierarchicalfacetprops.showmorelimit.md) | number | (Optional) The maximum number of options to render before displaying the "Show more/less" button. Defaults to 4. | +| [transformOptions?](./search-ui-react.hierarchicalfacetprops.transformoptions.md) | (options: DisplayableFacetOption\[\]) => DisplayableFacetOption\[\] | (Optional) A function to transform facet's options. The returned options need to be delimited to keep the hierarchy. | + diff --git a/docs/search-ui-react.hierarchicalfacetprops.showmorelimit.md b/docs/search-ui-react.hierarchicalfacetprops.showmorelimit.md new file mode 100644 index 000000000..6469cb024 --- /dev/null +++ b/docs/search-ui-react.hierarchicalfacetprops.showmorelimit.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) > [showMoreLimit](./search-ui-react.hierarchicalfacetprops.showmorelimit.md) + +## HierarchicalFacetProps.showMoreLimit property + +The maximum number of options to render before displaying the "Show more/less" button. Defaults to 4. + +Signature: + +```typescript +showMoreLimit?: number; +``` diff --git a/docs/search-ui-react.hierarchicalfacetprops.transformoptions.md b/docs/search-ui-react.hierarchicalfacetprops.transformoptions.md new file mode 100644 index 000000000..8a789b5c9 --- /dev/null +++ b/docs/search-ui-react.hierarchicalfacetprops.transformoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) > [transformOptions](./search-ui-react.hierarchicalfacetprops.transformoptions.md) + +## HierarchicalFacetProps.transformOptions property + +A function to transform facet's options. The returned options need to be delimited to keep the hierarchy. + +Signature: + +```typescript +transformOptions?: (options: DisplayableFacetOption[]) => DisplayableFacetOption[]; +``` diff --git a/docs/search-ui-react.hierarchicalfacets.md b/docs/search-ui-react.hierarchicalfacets.md index f2f3f74aa..5bb066c90 100644 --- a/docs/search-ui-react.hierarchicalfacets.md +++ b/docs/search-ui-react.hierarchicalfacets.md @@ -4,6 +4,11 @@ ## HierarchicalFacets() function +> Warning: This API is now obsolete. +> +> Use [HierarchicalFacet()](./search-ui-react.hierarchicalfacet.md) with [Facets()](./search-ui-react.facets.md) instead. +> + A component that displays hierarchical facets, in a tree level structure, applicable to the current vertical search. Signature: diff --git a/docs/search-ui-react.hierarchicalfacetsprops.md b/docs/search-ui-react.hierarchicalfacetsprops.md index 92d294366..6cc1e4caa 100644 --- a/docs/search-ui-react.hierarchicalfacetsprops.md +++ b/docs/search-ui-react.hierarchicalfacetsprops.md @@ -4,6 +4,11 @@ ## HierarchicalFacetsProps interface +> Warning: This API is now obsolete. +> +> Use [HierarchicalFacet()](./search-ui-react.hierarchicalfacet.md) with [Facets()](./search-ui-react.facets.md) instead. +> + Props for the [HierarchicalFacets()](./search-ui-react.hierarchicalfacets.md) component. Signature: diff --git a/docs/search-ui-react.locationbias.md b/docs/search-ui-react.locationbias.md index 7b7064a82..70cde1e2f 100644 --- a/docs/search-ui-react.locationbias.md +++ b/docs/search-ui-react.locationbias.md @@ -4,6 +4,11 @@ ## LocationBias() function +> Warning: This API is now obsolete. +> +> LocationBias component has been superseded by Geolocation component. +> + A React Component which displays and collects location information in order to bias searches. Signature: diff --git a/docs/search-ui-react.locationbiascssclasses.md b/docs/search-ui-react.locationbiascssclasses.md index 691f9b288..91a36b6fa 100644 --- a/docs/search-ui-react.locationbiascssclasses.md +++ b/docs/search-ui-react.locationbiascssclasses.md @@ -4,6 +4,11 @@ ## LocationBiasCssClasses interface +> Warning: This API is now obsolete. +> +> LocationBias component has been superseded by Geolocation component. +> + The CSS class interface for the [LocationBias()](./search-ui-react.locationbias.md) component. Signature: diff --git a/docs/search-ui-react.locationbiasprops.md b/docs/search-ui-react.locationbiasprops.md index 98ecc0b9f..fcc56ec34 100644 --- a/docs/search-ui-react.locationbiasprops.md +++ b/docs/search-ui-react.locationbiasprops.md @@ -4,6 +4,11 @@ ## LocationBiasProps interface +> Warning: This API is now obsolete. +> +> LocationBias component has been superseded by Geolocation component. +> + The props for the [LocationBias()](./search-ui-react.locationbias.md) component. Signature: diff --git a/docs/search-ui-react.md b/docs/search-ui-react.md index b5da0d550..b4ec09bc3 100644 --- a/docs/search-ui-react.md +++ b/docs/search-ui-react.md @@ -16,14 +16,18 @@ | [DropdownItem(\_props)](./search-ui-react.dropdownitem.md) | A wrapper component for specifying a DropdownItemWithIndex. The index will be automatically provided by the Dropdown component instance. | | [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. | +| [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. | +| [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. | +| [HierarchicalFacet(props)](./search-ui-react.hierarchicalfacet.md) | A component that displays a single hierarchical facet, in a tree level structure, applicable to the current vertical search. Use this to override the default rendering. | | [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. | | [isCtaData(data)](./search-ui-react.isctadata.md) | Type guard for CtaData. | | [LocationBias({ geolocationOptions, customCssClasses })](./search-ui-react.locationbias.md) | A React Component which displays and collects location information in order to bias searches. | | [MapboxMap({ mapboxAccessToken, mapboxOptions, PinComponent, getCoordinate, onDrag })](./search-ui-react.mapboxmap.md) | A component that renders a map with markers to show result locations using Mapbox GL. | +| [NumericalFacet(props)](./search-ui-react.numericalfacet.md) | A component that displays a single numerical facet. Use this to override the default rendering. | | [NumericalFacets({ searchOnChange, includedFieldIds, getFilterDisplayName, inputPrefix, customCssClasses, ...filterGroupProps })](./search-ui-react.numericalfacets.md) | A component that displays numerical facets applicable to the current vertical search. | | [Pagination(props)](./search-ui-react.pagination.md) | Renders a component that divide a series of vertical results into chunks across multiple pages and enable user to navigate between those pages. | | [renderHighlightedValue(highlightedValueOrString, customCssClasses)](./search-ui-react.renderhighlightedvalue.md) | Renders a HighlightedValue with highlighting based on its matchedSubstrings. | @@ -31,6 +35,7 @@ | [SearchBar({ placeholder, geolocationOptions, hideRecentSearches, visualAutocompleteConfig, showVerticalLinks, onSelectVerticalLink, verticalKeyToLabel, recentSearchesLimit, customCssClasses, onSearch })](./search-ui-react.searchbar.md) | Renders a SearchBar that is hooked up with an InputDropdown component. | | [SpellCheck({ customCssClasses, onClick })](./search-ui-react.spellcheck.md) | Renders a suggested query if the Search API provides one. | | [StandardCard(props)](./search-ui-react.standardcard.md) | This Component renders the base result card. | +| [StandardFacet(props)](./search-ui-react.standardfacet.md) | A component that displays a single standard facet. Use this to override the default rendering. | | [StandardFacets(props)](./search-ui-react.standardfacets.md) | A component that displays simple facets applicable to the current vertical search. | | [StandardSection(props)](./search-ui-react.standardsection.md) | A component that displays all the results for a vertical using a standard section template. | | [StaticFilters(props)](./search-ui-react.staticfilters.md) | A component that displays a group of user-configured field value filters that will be applied to the current vertical search. | @@ -59,18 +64,24 @@ | [CtaData](./search-ui-react.ctadata.md) | The shape of a StandardCard CTA field's data. | | [DirectAnswerCssClasses](./search-ui-react.directanswercssclasses.md) | The CSS class interface for [DirectAnswer()](./search-ui-react.directanswer.md). | | [DirectAnswerProps](./search-ui-react.directanswerprops.md) | Props for [DirectAnswer()](./search-ui-react.directanswer.md). | +| [FacetsCssClasses](./search-ui-react.facetscssclasses.md) | The CSS class interface for [Facets()](./search-ui-react.facets.md). Any [FilterGroupCssClasses](./search-ui-react.filtergroupcssclasses.md) props will be overridden by the same props from customCssClasses on [StandardFacetProps](./search-ui-react.standardfacetprops.md), [NumericalFacetProps](./search-ui-react.numericalfacetprops.md), or [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md). | +| [FacetsProps](./search-ui-react.facetsprops.md) | Props for the [Facets()](./search-ui-react.facets.md) component. | | [FilterGroupCssClasses](./search-ui-react.filtergroupcssclasses.md) | The CSS class interface for FilterGroup. | | [FilterGroupProps](./search-ui-react.filtergroupprops.md) | Props for the FilterGroup component. | | [FilterOptionConfig](./search-ui-react.filteroptionconfig.md) | The configuration data for a field value filter option. | | [FilterSearchCssClasses](./search-ui-react.filtersearchcssclasses.md) | The CSS class interface for [FilterSearch()](./search-ui-react.filtersearch.md). | | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | The props for the [FilterSearch()](./search-ui-react.filtersearch.md) component. | +| [GeolocationCssClasses](./search-ui-react.geolocationcssclasses.md) | The CSS class interface for the Geolocation component. | +| [GeolocationProps](./search-ui-react.geolocationprops.md) | The props for the Geolocation component. | | [HierarchicalFacetDisplayCssClasses](./search-ui-react.hierarchicalfacetdisplaycssclasses.md) | The CSS class interface for HierarchicalFacetDisplay. | +| [HierarchicalFacetProps](./search-ui-react.hierarchicalfacetprops.md) | Props for the [StandardFacet()](./search-ui-react.standardfacet.md) component. | | [HierarchicalFacetsCssClasses](./search-ui-react.hierarchicalfacetscssclasses.md) | The CSS class interface for [HierarchicalFacets()](./search-ui-react.hierarchicalfacets.md). | | [HierarchicalFacetsProps](./search-ui-react.hierarchicalfacetsprops.md) | Props for the [HierarchicalFacets()](./search-ui-react.hierarchicalfacets.md) component. | | [HighlightedValueCssClasses](./search-ui-react.highlightedvaluecssclasses.md) | The CSS class interface for [renderHighlightedValue()](./search-ui-react.renderhighlightedvalue.md). | | [LocationBiasCssClasses](./search-ui-react.locationbiascssclasses.md) | The CSS class interface for the [LocationBias()](./search-ui-react.locationbias.md) component. | | [LocationBiasProps](./search-ui-react.locationbiasprops.md) | The props for the [LocationBias()](./search-ui-react.locationbias.md) component. | | [MapboxMapProps](./search-ui-react.mapboxmapprops.md) | Props for the [MapboxMap()](./search-ui-react.mapboxmap.md) component. The type param "T" represents the type of "rawData" field of the results use in the map. | +| [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. | | [OnSelectParams](./search-ui-react.onselectparams.md) | The parameters that are passed into [FilterSearchProps.onSelect](./search-ui-react.filtersearchprops.onselect.md). | @@ -88,6 +99,7 @@ | [StandardCardCssClasses](./search-ui-react.standardcardcssclasses.md) | The CSS class interface used for the StandardCardDisplay. | | [StandardCardData](./search-ui-react.standardcarddata.md) | The data used by the [StandardCard()](./search-ui-react.standardcard.md) and taken from the original Result. | | [StandardCardProps](./search-ui-react.standardcardprops.md) | Props for a StandardCard. | +| [StandardFacetProps](./search-ui-react.standardfacetprops.md) | Props for the [StandardFacet()](./search-ui-react.standardfacet.md) component. | | [StandardFacetsCssClasses](./search-ui-react.standardfacetscssclasses.md) | The CSS class interface for [StandardFacets()](./search-ui-react.standardfacets.md). | | [StandardFacetsProps](./search-ui-react.standardfacetsprops.md) | Props for the [StandardFacets()](./search-ui-react.standardfacets.md) component. | | [StandardSectionCssClasses](./search-ui-react.standardsectioncssclasses.md) | The CSS class interface used for [StandardSection()](./search-ui-react.standardsection.md). | @@ -123,6 +135,7 @@ | [CoordinateGetter](./search-ui-react.coordinategetter.md) | A function use to derive a result's coordinate. | | [DefaultRawDataType](./search-ui-react.defaultrawdatatype.md) | The default type for "rawData" field of type Result. | | [DropdownItemProps](./search-ui-react.dropdownitemprops.md) | Props for the [DropdownItem()](./search-ui-react.dropdownitem.md). | +| [FacetProps](./search-ui-react.facetprops.md) | Props for a single facet component. | | [FeedbackType](./search-ui-react.feedbacktype.md) | Analytics event types for quality feedback. | | [FocusedItemData](./search-ui-react.focuseditemdata.md) | The data associated with the currently focused item. | | [OnDragHandler](./search-ui-react.ondraghandler.md) | A function which is called when user drag the map. | diff --git a/docs/search-ui-react.numericalfacet.md b/docs/search-ui-react.numericalfacet.md new file mode 100644 index 000000000..d192a66ed --- /dev/null +++ b/docs/search-ui-react.numericalfacet.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [NumericalFacet](./search-ui-react.numericalfacet.md) + +## NumericalFacet() function + +A component that displays a single numerical facet. Use this to override the default rendering. + +Signature: + +```typescript +export declare function NumericalFacet(props: NumericalFacetProps): null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| props | [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) | [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) | + +Returns: + +null + +ReactElement + diff --git a/docs/search-ui-react.numericalfacetprops.customcssclasses.md b/docs/search-ui-react.numericalfacetprops.customcssclasses.md new file mode 100644 index 000000000..602d8a32c --- /dev/null +++ b/docs/search-ui-react.numericalfacetprops.customcssclasses.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) > [customCssClasses](./search-ui-react.numericalfacetprops.customcssclasses.md) + +## NumericalFacetProps.customCssClasses property + +CSS classes for customizing the component styling. + +Signature: + +```typescript +customCssClasses?: FilterGroupCssClasses & RangeInputCssClasses; +``` diff --git a/docs/search-ui-react.numericalfacetprops.getfilterdisplayname.md b/docs/search-ui-react.numericalfacetprops.getfilterdisplayname.md new file mode 100644 index 000000000..6f4034e70 --- /dev/null +++ b/docs/search-ui-react.numericalfacetprops.getfilterdisplayname.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) > [getFilterDisplayName](./search-ui-react.numericalfacetprops.getfilterdisplayname.md) + +## NumericalFacetProps.getFilterDisplayName property + +Returns the filter's display name based on the range values which is used when the filter is displayed by other components such as AppliedFilters. + +Signature: + +```typescript +getFilterDisplayName?: (value: NumberRangeValue) => string; +``` + +## Remarks + +By default, the displayName separates the range with a dash such as '10 - 20'. If the range is unbounded, it will display as 'Up to 20' or 'Over 10'. + diff --git a/docs/search-ui-react.numericalfacetprops.inputprefix.md b/docs/search-ui-react.numericalfacetprops.inputprefix.md new file mode 100644 index 000000000..50b0b3877 --- /dev/null +++ b/docs/search-ui-react.numericalfacetprops.inputprefix.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) > [inputPrefix](./search-ui-react.numericalfacetprops.inputprefix.md) + +## NumericalFacetProps.inputPrefix property + +An optional element which renders in front of the input text. + +Signature: + +```typescript +inputPrefix?: JSX.Element; +``` diff --git a/docs/search-ui-react.numericalfacetprops.md b/docs/search-ui-react.numericalfacetprops.md new file mode 100644 index 000000000..0fc6c4b7f --- /dev/null +++ b/docs/search-ui-react.numericalfacetprops.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) + +## NumericalFacetProps interface + +Props for the [StandardFacet()](./search-ui-react.standardfacet.md) component. + +Signature: + +```typescript +export interface NumericalFacetProps extends StandardFacetProps +``` +Extends: [StandardFacetProps](./search-ui-react.standardfacetprops.md) + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [customCssClasses?](./search-ui-react.numericalfacetprops.customcssclasses.md) | [FilterGroupCssClasses](./search-ui-react.filtergroupcssclasses.md) & [RangeInputCssClasses](./search-ui-react.rangeinputcssclasses.md) | (Optional) CSS classes for customizing the component styling. | +| [getFilterDisplayName?](./search-ui-react.numericalfacetprops.getfilterdisplayname.md) | (value: NumberRangeValue) => string | (Optional) Returns the filter's display name based on the range values which is used when the filter is displayed by other components such as AppliedFilters. | +| [inputPrefix?](./search-ui-react.numericalfacetprops.inputprefix.md) | JSX.Element | (Optional) An optional element which renders in front of the input text. | +| [showOptionCounts?](./search-ui-react.numericalfacetprops.showoptioncounts.md) | boolean | (Optional) Whether or not to show the option counts for each filter. Defaults to false. | + diff --git a/docs/search-ui-react.numericalfacetprops.showoptioncounts.md b/docs/search-ui-react.numericalfacetprops.showoptioncounts.md new file mode 100644 index 000000000..8f510813b --- /dev/null +++ b/docs/search-ui-react.numericalfacetprops.showoptioncounts.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) > [showOptionCounts](./search-ui-react.numericalfacetprops.showoptioncounts.md) + +## NumericalFacetProps.showOptionCounts property + +Whether or not to show the option counts for each filter. Defaults to false. + +Signature: + +```typescript +showOptionCounts?: boolean; +``` diff --git a/docs/search-ui-react.numericalfacets.md b/docs/search-ui-react.numericalfacets.md index c9eca4523..c82440f03 100644 --- a/docs/search-ui-react.numericalfacets.md +++ b/docs/search-ui-react.numericalfacets.md @@ -4,6 +4,11 @@ ## NumericalFacets() function +> Warning: This API is now obsolete. +> +> Use [NumericalFacet()](./search-ui-react.numericalfacet.md) with [Facets()](./search-ui-react.facets.md) instead. +> + A component that displays numerical facets applicable to the current vertical search. Signature: diff --git a/docs/search-ui-react.numericalfacetsprops.md b/docs/search-ui-react.numericalfacetsprops.md index 49ff39a94..7c1a2cbe5 100644 --- a/docs/search-ui-react.numericalfacetsprops.md +++ b/docs/search-ui-react.numericalfacetsprops.md @@ -4,6 +4,11 @@ ## NumericalFacetsProps interface +> Warning: This API is now obsolete. +> +> Use [NumericalFacet()](./search-ui-react.numericalfacet.md) with [Facets()](./search-ui-react.facets.md) instead. +> + Props for the [NumericalFacets()](./search-ui-react.numericalfacets.md) component. Signature: diff --git a/docs/search-ui-react.standardfacet.md b/docs/search-ui-react.standardfacet.md new file mode 100644 index 000000000..6592863d3 --- /dev/null +++ b/docs/search-ui-react.standardfacet.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacet](./search-ui-react.standardfacet.md) + +## StandardFacet() function + +A component that displays a single standard facet. Use this to override the default rendering. + +Signature: + +```typescript +export declare function StandardFacet(props: StandardFacetProps): null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| props | [StandardFacetProps](./search-ui-react.standardfacetprops.md) | [StandardFacetProps](./search-ui-react.standardfacetprops.md) | + +Returns: + +null + +ReactElement + diff --git a/docs/search-ui-react.standardfacetprops.collapsible.md b/docs/search-ui-react.standardfacetprops.collapsible.md new file mode 100644 index 000000000..82508d336 --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.collapsible.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [collapsible](./search-ui-react.standardfacetprops.collapsible.md) + +## StandardFacetProps.collapsible property + +Whether or not the filter is collapsible. Defaults to true. + +Signature: + +```typescript +collapsible?: boolean; +``` diff --git a/docs/search-ui-react.standardfacetprops.customcssclasses.md b/docs/search-ui-react.standardfacetprops.customcssclasses.md new file mode 100644 index 000000000..0c8758dcd --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.customcssclasses.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [customCssClasses](./search-ui-react.standardfacetprops.customcssclasses.md) + +## StandardFacetProps.customCssClasses property + +CSS classes for customizing the component styling. + +Signature: + +```typescript +customCssClasses?: FilterGroupCssClasses; +``` diff --git a/docs/search-ui-react.standardfacetprops.defaultexpanded.md b/docs/search-ui-react.standardfacetprops.defaultexpanded.md new file mode 100644 index 000000000..c8d2e25d6 --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.defaultexpanded.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [defaultExpanded](./search-ui-react.standardfacetprops.defaultexpanded.md) + +## StandardFacetProps.defaultExpanded property + +If the filter group is collapsible, whether or not it should start out expanded. Defaults to true. + +Signature: + +```typescript +defaultExpanded?: boolean; +``` diff --git a/docs/search-ui-react.standardfacetprops.fieldid.md b/docs/search-ui-react.standardfacetprops.fieldid.md new file mode 100644 index 000000000..73096426d --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.fieldid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [fieldId](./search-ui-react.standardfacetprops.fieldid.md) + +## StandardFacetProps.fieldId property + +The fieldId corresponding to the facet + +Signature: + +```typescript +fieldId: string; +``` diff --git a/docs/search-ui-react.standardfacetprops.label.md b/docs/search-ui-react.standardfacetprops.label.md new file mode 100644 index 000000000..a1e0c7a0f --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.label.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [label](./search-ui-react.standardfacetprops.label.md) + +## StandardFacetProps.label property + +The label of the facet. Defaults to facet's displayName if not provided. + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/search-ui-react.standardfacetprops.md b/docs/search-ui-react.standardfacetprops.md new file mode 100644 index 000000000..da208c1ef --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) + +## StandardFacetProps interface + +Props for the [StandardFacet()](./search-ui-react.standardfacet.md) component. + +Signature: + +```typescript +export interface StandardFacetProps +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [collapsible?](./search-ui-react.standardfacetprops.collapsible.md) | boolean | (Optional) Whether or not the filter is collapsible. Defaults to true. | +| [customCssClasses?](./search-ui-react.standardfacetprops.customcssclasses.md) | [FilterGroupCssClasses](./search-ui-react.filtergroupcssclasses.md) | (Optional) CSS classes for customizing the component styling. | +| [defaultExpanded?](./search-ui-react.standardfacetprops.defaultexpanded.md) | boolean | (Optional) If the filter group is collapsible, whether or not it should start out expanded. Defaults to true. | +| [fieldId](./search-ui-react.standardfacetprops.fieldid.md) | string | The fieldId corresponding to the facet | +| [label?](./search-ui-react.standardfacetprops.label.md) | string | (Optional) The label of the facet. Defaults to facet's displayName if not provided. | +| [showMoreLimit?](./search-ui-react.standardfacetprops.showmorelimit.md) | number | (Optional) The maximum number of options to render before displaying the "Show more/less" button. Defaults to 10. | +| [showOptionCounts?](./search-ui-react.standardfacetprops.showoptioncounts.md) | boolean | (Optional) Whether or not to show the option counts for each filter. Defaults to true. | +| [transformOptions?](./search-ui-react.standardfacetprops.transformoptions.md) | (options: DisplayableFacetOption\[\]) => DisplayableFacetOption\[\] | (Optional) A function to transform facet's options. | + diff --git a/docs/search-ui-react.standardfacetprops.showmorelimit.md b/docs/search-ui-react.standardfacetprops.showmorelimit.md new file mode 100644 index 000000000..ec85f8dc1 --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.showmorelimit.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [showMoreLimit](./search-ui-react.standardfacetprops.showmorelimit.md) + +## StandardFacetProps.showMoreLimit property + +The maximum number of options to render before displaying the "Show more/less" button. Defaults to 10. + +Signature: + +```typescript +showMoreLimit?: number; +``` diff --git a/docs/search-ui-react.standardfacetprops.showoptioncounts.md b/docs/search-ui-react.standardfacetprops.showoptioncounts.md new file mode 100644 index 000000000..a449f9916 --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.showoptioncounts.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [showOptionCounts](./search-ui-react.standardfacetprops.showoptioncounts.md) + +## StandardFacetProps.showOptionCounts property + +Whether or not to show the option counts for each filter. Defaults to true. + +Signature: + +```typescript +showOptionCounts?: boolean; +``` diff --git a/docs/search-ui-react.standardfacetprops.transformoptions.md b/docs/search-ui-react.standardfacetprops.transformoptions.md new file mode 100644 index 000000000..fb174ecb3 --- /dev/null +++ b/docs/search-ui-react.standardfacetprops.transformoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [StandardFacetProps](./search-ui-react.standardfacetprops.md) > [transformOptions](./search-ui-react.standardfacetprops.transformoptions.md) + +## StandardFacetProps.transformOptions property + +A function to transform facet's options. + +Signature: + +```typescript +transformOptions?: (options: DisplayableFacetOption[]) => DisplayableFacetOption[]; +``` diff --git a/docs/search-ui-react.standardfacets.md b/docs/search-ui-react.standardfacets.md index f786896b3..ae2d5ce08 100644 --- a/docs/search-ui-react.standardfacets.md +++ b/docs/search-ui-react.standardfacets.md @@ -4,6 +4,11 @@ ## StandardFacets() function +> Warning: This API is now obsolete. +> +> Use [Facets()](./search-ui-react.facets.md) instead. +> + A component that displays simple facets applicable to the current vertical search. Signature: diff --git a/docs/search-ui-react.standardfacetscssclasses.md b/docs/search-ui-react.standardfacetscssclasses.md index f7cec29b4..2152492e5 100644 --- a/docs/search-ui-react.standardfacetscssclasses.md +++ b/docs/search-ui-react.standardfacetscssclasses.md @@ -4,6 +4,11 @@ ## StandardFacetsCssClasses interface +> Warning: This API is now obsolete. +> +> Use [StandardFacet()](./search-ui-react.standardfacet.md) with [Facets()](./search-ui-react.facets.md) instead. +> + The CSS class interface for [StandardFacets()](./search-ui-react.standardfacets.md). Signature: diff --git a/docs/search-ui-react.standardfacetsprops.md b/docs/search-ui-react.standardfacetsprops.md index 81260b627..1aafac0f4 100644 --- a/docs/search-ui-react.standardfacetsprops.md +++ b/docs/search-ui-react.standardfacetsprops.md @@ -4,6 +4,11 @@ ## StandardFacetsProps interface +> Warning: This API is now obsolete. +> +> Use [StandardFacet()](./search-ui-react.standardfacet.md) with [Facets()](./search-ui-react.facets.md) instead. +> + Props for the [StandardFacets()](./search-ui-react.standardfacets.md) component. Signature: diff --git a/etc/search-ui-react.api.md b/etc/search-ui-react.api.md index a05aa14d7..5c4a2ea04 100644 --- a/etc/search-ui-react.api.md +++ b/etc/search-ui-react.api.md @@ -10,6 +10,7 @@ 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 { DisplayableFacetOption } 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'; @@ -21,6 +22,7 @@ import { Matcher } from '@yext/search-headless-react'; import { NumberRangeValue } from '@yext/search-headless-react'; import { PropsWithChildren } from 'react'; import { QuerySource } from '@yext/search-headless-react'; +import { ReactElement } from 'react'; import { Result } from '@yext/search-headless-react'; import { SearchActions } from '@yext/search-headless-react'; import { SearchHeadless } from '@yext/search-headless-react'; @@ -200,6 +202,30 @@ export function executeAutocomplete(searchActions: SearchActions): Promise; +// @public +export type FacetProps = StandardFacetProps | NumericalFacetProps | HierarchicalFacetProps; + +// @public +export function Facets(props: FacetsProps): JSX.Element; + +// @public +export interface FacetsCssClasses extends FilterGroupCssClasses { + // (undocumented) + divider?: string; + // (undocumented) + facetsContainer?: string; +} + +// @public +export interface FacetsProps { + children?: ReactElement[] | ReactElement | undefined | null; + customCssClasses?: FacetsCssClasses; + excludedFieldIds?: string[]; + hierarchicalFieldIds?: string[]; + onlyRenderChildren?: boolean; + searchOnChange?: boolean; +} + // @public export type FeedbackType = 'THUMBS_UP' | 'THUMBS_DOWN'; @@ -279,12 +305,39 @@ export interface FilterSearchProps { // @public export type FocusedItemData = Record; +// @public +function Geolocation_2({ geolocationOptions, radius, label, GeolocationIcon, handleClick, customCssClasses, }: GeolocationProps): JSX.Element | null; +export { Geolocation_2 as Geolocation } + +// @public +export interface GeolocationCssClasses { + // (undocumented) + button?: string; + // (undocumented) + geolocationContainer?: string; + // (undocumented) + iconContainer?: string; +} + +// @public +export interface GeolocationProps { + customCssClasses?: GeolocationCssClasses; + GeolocationIcon?: React.FunctionComponent; + geolocationOptions?: PositionOptions; + handleClick?: (position: GeolocationPosition) => void; + label?: string; + radius?: number; +} + // @public export function getSearchIntents(searchActions: SearchActions): Promise; // @public export function getUserLocation(geolocationOptions?: PositionOptions): Promise; +// @public +export function HierarchicalFacet(props: HierarchicalFacetProps): null; + // @public export interface HierarchicalFacetDisplayCssClasses { // (undocumented) @@ -306,6 +359,15 @@ export interface HierarchicalFacetDisplayCssClasses { } // @public +export interface HierarchicalFacetProps extends Omit { + // Warning: (ae-forgotten-export) The symbol "HierarchicalFacetCustomCssClasses" needs to be exported by the entry point index.d.ts + customCssClasses?: HierarchicalFacetCustomCssClasses; + delimiter?: string; + showMoreLimit?: number; + transformOptions?: (options: DisplayableFacetOption[]) => DisplayableFacetOption[]; +} + +// @public @deprecated export function HierarchicalFacets({ searchOnChange, collapsible, defaultExpanded, includedFieldIds, customCssClasses, delimiter, showMoreLimit }: HierarchicalFacetsProps): JSX.Element; // @public @@ -316,7 +378,7 @@ export interface HierarchicalFacetsCssClasses extends HierarchicalFacetDisplayCs hierarchicalFacetsContainer?: string; } -// @public +// @public @deprecated export interface HierarchicalFacetsProps extends Omit { customCssClasses?: HierarchicalFacetsCssClasses; delimiter?: string; @@ -335,10 +397,10 @@ export interface HighlightedValueCssClasses { // @public export function isCtaData(data: unknown): data is CtaData; -// @public +// @public @deprecated export function LocationBias({ geolocationOptions, customCssClasses }: LocationBiasProps): JSX.Element | null; -// @public +// @public @deprecated export interface LocationBiasCssClasses { // (undocumented) button?: string; @@ -352,7 +414,7 @@ export interface LocationBiasCssClasses { source?: string; } -// @public +// @public @deprecated export interface LocationBiasProps { customCssClasses?: LocationBiasCssClasses; geolocationOptions?: PositionOptions; @@ -371,6 +433,17 @@ export interface MapboxMapProps { } // @public +export function NumericalFacet(props: NumericalFacetProps): null; + +// @public +export interface NumericalFacetProps extends StandardFacetProps { + customCssClasses?: FilterGroupCssClasses & RangeInputCssClasses; + getFilterDisplayName?: (value: NumberRangeValue) => string; + inputPrefix?: JSX.Element; + showOptionCounts?: boolean; +} + +// @public @deprecated export function NumericalFacets({ searchOnChange, includedFieldIds, getFilterDisplayName, inputPrefix, customCssClasses, ...filterGroupProps }: NumericalFacetsProps): JSX.Element; // @public @@ -381,7 +454,7 @@ export interface NumericalFacetsCssClasses extends FilterGroupCssClasses, RangeI numericalFacetsContainer?: string; } -// @public +// @public @deprecated export interface NumericalFacetsProps extends Omit { customCssClasses?: NumericalFacetsCssClasses; getFilterDisplayName?: (value: NumberRangeValue) => string; @@ -644,9 +717,24 @@ export interface StandardCardProps extends CardProps } // @public -export function StandardFacets(props: StandardFacetsProps): JSX.Element; +export function StandardFacet(props: StandardFacetProps): null; // @public +export interface StandardFacetProps { + collapsible?: boolean; + customCssClasses?: FilterGroupCssClasses; + defaultExpanded?: boolean; + fieldId: string; + label?: string; + showMoreLimit?: number; + showOptionCounts?: boolean; + transformOptions?: (options: DisplayableFacetOption[]) => DisplayableFacetOption[]; +} + +// @public @deprecated +export function StandardFacets(props: StandardFacetsProps): JSX.Element; + +// @public @deprecated export interface StandardFacetsCssClasses extends FilterGroupCssClasses { // (undocumented) divider?: string; @@ -654,7 +742,7 @@ export interface StandardFacetsCssClasses extends FilterGroupCssClasses { standardFacetsContainer?: string; } -// @public +// @public @deprecated export interface StandardFacetsProps { collapsible?: boolean; customCssClasses?: StandardFacetsCssClasses; diff --git a/package-lock.json b/package-lock.json index 6632835d5..a89d23b17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yext/search-ui-react", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@yext/search-ui-react", - "version": "1.2.1", + "version": "1.3.0", "license": "BSD-3-Clause", "dependencies": { "@microsoft/api-documenter": "^7.15.3", @@ -14,7 +14,7 @@ "@reach/auto-id": "^0.18.0", "@restart/ui": "^1.0.1", "@tailwindcss/forms": "^0.5.0", - "@yext/analytics": "^0.2.0-beta.3", + "@yext/analytics": "^0.5.0", "classnames": "^2.3.1", "lodash": "^4.17.21", "mapbox-gl": "^2.9.2", @@ -29,6 +29,7 @@ "@babel/preset-env": "^7.14.7", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.14.5", + "@etchteam/storybook-addon-status": "^4.2.2", "@percy/cli": "^1.8.0", "@percy/storybook": "^4.3.3", "@reduxjs/toolkit": "^1.8.6", @@ -53,7 +54,7 @@ "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "@yext/eslint-config-slapshot": "^0.5.0", - "@yext/search-headless-react": "^2.2.0", + "@yext/search-headless-react": "2.3.0", "axe-playwright": "^1.1.11", "babel-jest": "^27.0.6", "eslint": "^8.11.0", @@ -70,7 +71,7 @@ "typescript": "~4.5.5" }, "peerDependencies": { - "@yext/search-headless-react": "^2.2.0", + "@yext/search-headless-react": "2.3.0", "react": "^16.14 || ^17", "react-dom": "^16.14 || ^17" } @@ -2282,6 +2283,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@etchteam/storybook-addon-status": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@etchteam/storybook-addon-status/-/storybook-addon-status-4.2.2.tgz", + "integrity": "sha512-iCOoA0+Izu/SixxjjJ9BB4YBVT2reCJ/80dXHBiCqoGSPTAvYGeeoQKexC913eSFv/0u3u4lv5fRZN/KMPAurw==", + "dev": true, + "dependencies": { + "@storybook/addons": "^6.2.9", + "@storybook/api": "^6.2.9", + "@storybook/client-logger": "^6.2.9", + "@storybook/components": "^6.2.9", + "@storybook/core-events": "^6.2.9", + "@storybook/theming": "^6.2.9", + "core-js": "^3.0.1", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "util-deprecate": "^1.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17.0.2 || ^18.0.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -10678,10 +10700,11 @@ "dev": true }, "node_modules/@yext/analytics": { - "version": "0.2.0-beta.3", - "integrity": "sha512-bHhxVoMwEAsodg7n7W0dB6oTiWY/KnGL20XB2u9tw2FU9Fn/xlJV8aln1JakhiSatNqXNIHdC38PabqCWFuOrg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@yext/analytics/-/analytics-0.5.0.tgz", + "integrity": "sha512-KfM8Lh9U/1sm0tr+JL12hpnZdPuL09L14dYoAVH5FEd4U8TE4e6SZI6gMJ5BbjuL27EiqzsqD3QO04B+W8BO+w==", "dependencies": { - "cross-fetch": "^3.0.0" + "cross-fetch": "^3.1.5" } }, "node_modules/@yext/eslint-config-slapshot": { @@ -10716,9 +10739,9 @@ } }, "node_modules/@yext/search-core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@yext/search-core/-/search-core-2.3.0.tgz", - "integrity": "sha512-vSvNXWv9E/6s4oRB1og4zHfRTTEHrmUm2sh95Y1Dn94U2mkjNDGSsshEeamU2UIJO7Ee5oT6K6JDU7XAVOxC4A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@yext/search-core/-/search-core-2.4.0.tgz", + "integrity": "sha512-slPiKO3lENIB8aqr509ljjkaHJ9UJXnBu0iSv8CAdFsoZomdXTQExlbGBPHVZ6q0rEBahDR4PSMWlvFp9b7yAQ==", "dev": true, "dependencies": { "@babel/runtime-corejs3": "^7.12.5", @@ -10729,24 +10752,24 @@ } }, "node_modules/@yext/search-headless": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@yext/search-headless/-/search-headless-2.3.0.tgz", - "integrity": "sha512-Uh5DVeV99dkaeF6ayuUEkcUbI8wHn/7bz4aHrjtdDyl3F/6GX3cyHbs/BQh1kWCr+t8EJUWVPl5s4jmL3tst/Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@yext/search-headless/-/search-headless-2.4.0.tgz", + "integrity": "sha512-N4++EBuV9KWiS6sX29g/EeXUvprycx3QXS3OHZLYVy4n0Y4xNsItXAeb4qZywj6AN8Wi+lEPgUsWQA00WzxBcw==", "dev": true, "dependencies": { "@reduxjs/toolkit": "^1.8.1", - "@yext/search-core": "^2.3.0", + "@yext/search-core": "^2.4.0", "js-levenshtein": "^1.1.6", "lodash": "^4.17.21" } }, "node_modules/@yext/search-headless-react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@yext/search-headless-react/-/search-headless-react-2.2.0.tgz", - "integrity": "sha512-x2Sx7uS3w5E8RfuIpPsQZGWTGNKS9kRWDlQReUngzxP+UhoBQ8L7qNTGGbJLZ+LUpjRqTS1RuZP4scljbla//g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@yext/search-headless-react/-/search-headless-react-2.3.0.tgz", + "integrity": "sha512-E3b+o90FwVrBc/84UYS8cClirsTU+9U8sa4UBMAnamPYPhge3DwNWQeA/hfQhTU6dGoaUnEiz4uTkSh8bhY3Rg==", "dev": true, "dependencies": { - "@yext/search-headless": "^2.3.0", + "@yext/search-headless": "^2.4.0", "use-sync-external-store": "^1.1.0" }, "peerDependencies": { @@ -34687,6 +34710,24 @@ } } }, + "@etchteam/storybook-addon-status": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@etchteam/storybook-addon-status/-/storybook-addon-status-4.2.2.tgz", + "integrity": "sha512-iCOoA0+Izu/SixxjjJ9BB4YBVT2reCJ/80dXHBiCqoGSPTAvYGeeoQKexC913eSFv/0u3u4lv5fRZN/KMPAurw==", + "dev": true, + "requires": { + "@storybook/addons": "^6.2.9", + "@storybook/api": "^6.2.9", + "@storybook/client-logger": "^6.2.9", + "@storybook/components": "^6.2.9", + "@storybook/core-events": "^6.2.9", + "@storybook/theming": "^6.2.9", + "core-js": "^3.0.1", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "util-deprecate": "^1.0.2" + } + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -41039,10 +41080,11 @@ "dev": true }, "@yext/analytics": { - "version": "0.2.0-beta.3", - "integrity": "sha512-bHhxVoMwEAsodg7n7W0dB6oTiWY/KnGL20XB2u9tw2FU9Fn/xlJV8aln1JakhiSatNqXNIHdC38PabqCWFuOrg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@yext/analytics/-/analytics-0.5.0.tgz", + "integrity": "sha512-KfM8Lh9U/1sm0tr+JL12hpnZdPuL09L14dYoAVH5FEd4U8TE4e6SZI6gMJ5BbjuL27EiqzsqD3QO04B+W8BO+w==", "requires": { - "cross-fetch": "^3.0.0" + "cross-fetch": "^3.1.5" } }, "@yext/eslint-config-slapshot": { @@ -41068,9 +41110,9 @@ } }, "@yext/search-core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@yext/search-core/-/search-core-2.3.0.tgz", - "integrity": "sha512-vSvNXWv9E/6s4oRB1og4zHfRTTEHrmUm2sh95Y1Dn94U2mkjNDGSsshEeamU2UIJO7Ee5oT6K6JDU7XAVOxC4A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@yext/search-core/-/search-core-2.4.0.tgz", + "integrity": "sha512-slPiKO3lENIB8aqr509ljjkaHJ9UJXnBu0iSv8CAdFsoZomdXTQExlbGBPHVZ6q0rEBahDR4PSMWlvFp9b7yAQ==", "dev": true, "requires": { "@babel/runtime-corejs3": "^7.12.5", @@ -41078,24 +41120,24 @@ } }, "@yext/search-headless": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@yext/search-headless/-/search-headless-2.3.0.tgz", - "integrity": "sha512-Uh5DVeV99dkaeF6ayuUEkcUbI8wHn/7bz4aHrjtdDyl3F/6GX3cyHbs/BQh1kWCr+t8EJUWVPl5s4jmL3tst/Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@yext/search-headless/-/search-headless-2.4.0.tgz", + "integrity": "sha512-N4++EBuV9KWiS6sX29g/EeXUvprycx3QXS3OHZLYVy4n0Y4xNsItXAeb4qZywj6AN8Wi+lEPgUsWQA00WzxBcw==", "dev": true, "requires": { "@reduxjs/toolkit": "^1.8.1", - "@yext/search-core": "^2.3.0", + "@yext/search-core": "^2.4.0", "js-levenshtein": "^1.1.6", "lodash": "^4.17.21" } }, "@yext/search-headless-react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@yext/search-headless-react/-/search-headless-react-2.2.0.tgz", - "integrity": "sha512-x2Sx7uS3w5E8RfuIpPsQZGWTGNKS9kRWDlQReUngzxP+UhoBQ8L7qNTGGbJLZ+LUpjRqTS1RuZP4scljbla//g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@yext/search-headless-react/-/search-headless-react-2.3.0.tgz", + "integrity": "sha512-E3b+o90FwVrBc/84UYS8cClirsTU+9U8sa4UBMAnamPYPhge3DwNWQeA/hfQhTU6dGoaUnEiz4uTkSh8bhY3Rg==", "dev": true, "requires": { - "@yext/search-headless": "^2.3.0", + "@yext/search-headless": "^2.4.0", "use-sync-external-store": "^1.1.0" } }, diff --git a/package.json b/package.json index 6fbdac8bb..c138651e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yext/search-ui-react", - "version": "1.2.1", + "version": "1.3.0", "description": "A library of React Components for powering Yext Search integrations", "author": "slapshot@yext.com", "license": "BSD-3-Clause", @@ -42,7 +42,7 @@ "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", "wcag": "test-storybook", - "test:unit": "jest --coverage --coverageDirectory=coverage/unit", + "test:unit": "jest", "test:visual": "./tests/scripts/visual-coverage.sh", "test": "./tests/scripts/combined-coverage.sh" }, @@ -51,6 +51,7 @@ "@babel/preset-env": "^7.14.7", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.14.5", + "@etchteam/storybook-addon-status": "^4.2.2", "@percy/cli": "^1.8.0", "@percy/storybook": "^4.3.3", "@reduxjs/toolkit": "^1.8.6", @@ -75,7 +76,7 @@ "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "@yext/eslint-config-slapshot": "^0.5.0", - "@yext/search-headless-react": "^2.2.0", + "@yext/search-headless-react": "2.3.0", "axe-playwright": "^1.1.11", "babel-jest": "^27.0.6", "eslint": "^8.11.0", @@ -92,18 +93,19 @@ "typescript": "~4.5.5" }, "peerDependencies": { - "@yext/search-headless-react": "^2.2.0", + "@yext/search-headless-react": "2.3.0", "react": "^16.14 || ^17", "react-dom": "^16.14 || ^17" }, "jest": { "bail": 0, "verbose": true, - "collectCoverage": false, + "collectCoverage": true, "collectCoverageFrom": [ "src/**", "!src/models/**" ], + "coverageDirectory": "coverage/unit", "moduleFileExtensions": [ "js", "ts", @@ -134,14 +136,14 @@ "restoreMocks": true }, "dependencies": { - "lodash": "^4.17.21", "@microsoft/api-documenter": "^7.15.3", "@microsoft/api-extractor": "^7.19.4", "@reach/auto-id": "^0.18.0", "@restart/ui": "^1.0.1", "@tailwindcss/forms": "^0.5.0", - "@yext/analytics": "^0.2.0-beta.3", + "@yext/analytics": "^0.5.0", "classnames": "^2.3.1", + "lodash": "^4.17.21", "mapbox-gl": "^2.9.2", "prop-types": "^15.8.1", "react-collapsed": "3.6.0", diff --git a/src/components/AppliedFilters.tsx b/src/components/AppliedFilters.tsx index 26f20d6d4..311f66475 100644 --- a/src/components/AppliedFilters.tsx +++ b/src/components/AppliedFilters.tsx @@ -45,7 +45,7 @@ export interface AppliedFiltersProps { customCssClasses?: AppliedFiltersCssClasses } -const DEFUALT_HIDDEN_FIELDS = ['builtin.entityType']; +const DEFAULT_HIDDEN_FIELDS = ['builtin.entityType']; /** * A component that displays a list of filters applied to the current vertical @@ -61,7 +61,7 @@ export function AppliedFilters(props: AppliedFiltersProps): JSX.Element { const isLoading = useSearchState(state => state.searchStatus.isLoading); const { - hiddenFields = DEFUALT_HIDDEN_FIELDS, + hiddenFields = DEFAULT_HIDDEN_FIELDS, customCssClasses = {}, hierarchicalFacetsDelimiter = DEFAULT_HIERARCHICAL_DELIMITER, hierarchicalFacetsFieldIds diff --git a/src/components/FacetProps.tsx b/src/components/FacetProps.tsx new file mode 100644 index 000000000..2966ec393 --- /dev/null +++ b/src/components/FacetProps.tsx @@ -0,0 +1,134 @@ +import { DisplayableFacetOption } from '@yext/search-headless-react'; +import { FilterGroupCssClasses } from './FilterGroup'; +import { ReactElement } from 'react'; +import { NumberRangeValue } from '@yext/search-headless-react'; +import { HierarchicalFacetDisplayCssClasses, RangeInputCssClasses } from './Filters'; + +/** + * The CSS class interface for {@link Facets}. Any {@link FilterGroupCssClasses} props will be + * overridden by the same props from customCssClasses on {@link StandardFacetProps}, + * {@link NumericalFacetProps}, or {@link HierarchicalFacetProps}. + * + * @public + */ +export interface FacetsCssClasses extends FilterGroupCssClasses { + facetsContainer?: string, + divider?: string +} + +/** + * Props for the {@link Facets} component. + * + * @public + */ +export interface FacetsProps { + /** Whether or not a search is automatically run when a filter is selected. Defaults to true. */ + searchOnChange?: boolean, + /** If set to true, only the facets specified in the children are rendered. If set to false, all + * facets are rendered with the ones specified in the children overridden. Default to false. */ + onlyRenderChildren?: boolean, + /** CSS classes for customizing the component styling. */ + customCssClasses?: FacetsCssClasses, + /** List of field ids that should not be displayed. */ + excludedFieldIds?: string[], + /** List of field ids that should be rendered as hierarchical facets. */ + hierarchicalFieldIds?: string[], + /** The custom facet components that will override the default rendering. + * + * @remarks + * Supported components include {@link StandardFacet}, {@link NumericalFacet}. + */ + children?: ReactElement[] | ReactElement | undefined | null +} + +/** + * Props for the {@link StandardFacet} component. + * + * @public + */ +export interface StandardFacetProps { + /** The fieldId corresponding to the facet */ + fieldId: string, + /** The label of the facet. Defaults to facet's displayName if not provided. */ + label?: string, + /** A function to transform facet's options. */ + transformOptions?: (options: DisplayableFacetOption[]) => DisplayableFacetOption[], + /** {@inheritDoc FilterGroupProps.collapsible} */ + collapsible?: boolean, + /** {@inheritDoc FilterGroupProps.defaultExpanded} */ + defaultExpanded?: boolean, + /** Whether or not to show the option counts for each filter. Defaults to true. */ + showOptionCounts?: boolean, + /** + * The maximum number of options to render before displaying the "Show more/less" button. + * Defaults to 10. + */ + showMoreLimit?: number, + /** CSS classes for customizing the component styling. */ + customCssClasses?: FilterGroupCssClasses +} + +/** + * Props for the {@link StandardFacet} component. + * + * @public + */ +export interface NumericalFacetProps extends StandardFacetProps { + /** Whether or not to show the option counts for each filter. Defaults to false. */ + showOptionCounts?: boolean, + /** CSS classes for customizing the component styling. */ + customCssClasses?: FilterGroupCssClasses & RangeInputCssClasses, + /** + * Returns the filter's display name based on the range values which is used when the filter + * is displayed by other components such as AppliedFilters. + * + * @remarks + * By default, the displayName separates the range with a dash such as '10 - 20'. + * If the range is unbounded, it will display as 'Up to 20' or 'Over 10'. + */ + getFilterDisplayName?: (value: NumberRangeValue) => string, + /** + * An optional element which renders in front of the input text. + */ + inputPrefix?: JSX.Element +} + +/** + * Props for the {@link StandardFacet} component. + * + * @public + */ +export interface HierarchicalFacetCustomCssClasses extends HierarchicalFacetDisplayCssClasses { + /** CSS classes for customizing the title label styling. */ + titleLabel?: string +} + +/** + * Props for the {@link StandardFacet} component. + * + * @public + */ +export interface HierarchicalFacetProps extends + Omit { + /** + * A function to transform facet's options. The returned options need to be delimited to keep + * the hierarchy. + */ + transformOptions?: (options: DisplayableFacetOption[]) => DisplayableFacetOption[], + /** + * The maximum number of options to render before displaying the "Show more/less" button. + * Defaults to 4. + */ + showMoreLimit?: number, + /** CSS classes for customizing the component styling. */ + customCssClasses?: HierarchicalFacetCustomCssClasses, + /** The delimiter for determining facet hierarchies, defaults to "\>". */ + delimiter?: string +} + +/** + * Props for a single facet component. + * + * @public + */ +export type FacetProps = StandardFacetProps | NumericalFacetProps | HierarchicalFacetProps; diff --git a/src/components/FacetTiltle.tsx b/src/components/FacetTiltle.tsx new file mode 100644 index 000000000..38f5022a4 --- /dev/null +++ b/src/components/FacetTiltle.tsx @@ -0,0 +1,47 @@ +import { builtInCollapsibleLabelCssClasses, CollapsibleLabel, CollapsibleLabelCssClasses } from './Filters'; +import { Fragment, useMemo } from 'react'; +import { twMerge } from '../hooks/useComposedCssClasses'; +import { FilterGroupCssClasses } from './FilterGroup'; + +/** + * Props for the {@link FacetTitle} component. + * + * @internal + */ +export interface FacetTitleProps { + /** The label to use in the title. */ + label?: string, + /** {@inheritDoc FilterGroupProps.collapsible} */ + collapsible?: boolean, + /** CSS classes for customizing the component styling. */ + customCssClasses?: FilterGroupCssClasses +} + +/** + * The tile of a facet component. + * + * @param props - props to render the component + * @returns A React component + * + * @internal + */ +export function FacetTitle({ + label, + customCssClasses, + collapsible = true, +}: FacetTitleProps) { + const collapsibleLabelCssClasses: CollapsibleLabelCssClasses = useMemo(() => { + return { + label: customCssClasses?.titleLabel + }; + }, [customCssClasses]); + + return + {collapsible + ? + : (label &&
+ {label} +
)} +
; +} diff --git a/src/components/Facets.tsx b/src/components/Facets.tsx new file mode 100644 index 000000000..ee44e6030 --- /dev/null +++ b/src/components/Facets.tsx @@ -0,0 +1,227 @@ +import { FacetsProvider } from './Filters'; +import { StandardFacetContent } from './StandardFacetContent'; +import { + FacetProps, + FacetsProps, HierarchicalFacetProps, NumericalFacetProps, + StandardFacetProps +} from './FacetProps'; +import { isNumericalFacet, isStringFacet } from '../utils/filterutils'; +import { FilterDivider } from './FilterDivider'; +import { Fragment, ReactElement } from 'react'; +import { NumericalFacetContent } from './NumericalFacetContent'; +import { HierarchicalFacetContent } from './HierarchicalFacetContent'; +import { DisplayableFacet } from '@yext/search-headless-react'; + +/** @internal */ +enum FacetType { + STANDARD = 'STANDARD', + NUMERICAL = 'NUMERICAL', + HIERARCHICAL = 'HIERARCHICAL' +} + +/** + * A component that displays all facets applicable to the current vertical search. + * + * @remarks + * This component is a quick way of getting facets on the page, and it will render standard facets, + * numerical facets, and hierarchical facets. The {@link StandardFacet}, {@link NumericalFacet}, + * and {@link HierarchicalFacet} components can be used to override the default facet configuration. + * + * @param props - {@link FacetsProps} + * @returns A React component for facets + * + * @public + */ +export function Facets(props: FacetsProps) { + const { + searchOnChange, + onlyRenderChildren = false, + children, + hierarchicalFieldIds, + excludedFieldIds = [], + customCssClasses = {}, + } = props; + + const fieldIdToCustomFacetProps = new Map(); + const fieldIds: string[] = []; + if (children) { + (Array.isArray(children) ? children : [children]) + .filter(child => child?.props?.fieldId) + .forEach(child => { + fieldIdToCustomFacetProps.set(child.props.fieldId, child); + fieldIds.push(child.props.fieldId); + }); + } + + return ( +
+ + {facets => { + if (!facets || !facets.length) { + return; + } + + if (!onlyRenderChildren) { + facets.forEach(facet => { + if (!fieldIds.includes(facet.fieldId)) { + fieldIds.push(facet.fieldId); + } + }); + } + + const fieldIdToFacet = new Map(); + facets.forEach(facet => fieldIdToFacet.set(facet.fieldId, facet)); + + return fieldIds + .filter(fieldId => + !excludedFieldIds.includes(fieldId) + && fieldIdToFacet.get(fieldId).options.length > 0 + && (!onlyRenderChildren || fieldIdToCustomFacetProps.has(fieldId))) + .map((fieldId, i) => { + const facet: DisplayableFacet = fieldIdToFacet.get(fieldId); + + return ( + + + {(i < facets.length - 1) + && } + + ); + }); + } + } + +
+ ); +} + +/** + * A component that displays a single standard facet. Use this to override the default rendering. + * + * @param props - {@link StandardFacetProps} + * @returns ReactElement + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function StandardFacet(props: StandardFacetProps) { return null; } + +/** + * A component that displays a single numerical facet. Use this to override the default rendering. + * + * @param props - {@link NumericalFacetProps} + * @returns ReactElement + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function NumericalFacet(props: NumericalFacetProps) { return null; } + +/** + * A component that displays a single hierarchical facet, in a tree level structure, applicable to + * the current vertical search. Use this to override the default rendering. + * + * @param props - {@link HierarchicalFacetProps} + * @returns ReactElement + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function HierarchicalFacet(props: HierarchicalFacetProps) { return null; } + +/** + * A component that represents a single facet. + * + * @param facet - {@link DisplayableFacet} + * @param facetsCustomCssClasses - {@link FacetsCssClasses} + * @param fieldIdToCustomFacetProps - a map of fieldId to facet props + * @param hierarchicalFieldIds - a list of hierarchical field ids + * @returns {@link ReactElement} + * + * @internal + */ +export function Facet({ + facet, + facetsCustomCssClasses, + fieldIdToCustomFacetProps, + hierarchicalFieldIds, +}) { + let facetType: FacetType; + let facetProps: FacetProps = { + fieldId: facet.fieldId, + label: facet.displayName, + }; + if (fieldIdToCustomFacetProps.has(facet.fieldId)) { + const customFacetElement: ReactElement = fieldIdToCustomFacetProps.get(facet.fieldId); + facetProps = { ...facetProps, ...customFacetElement.props }; + facetType = getFacetTypeFromReactElementType( + (typeof customFacetElement.type === 'function') + ? customFacetElement.type.name : ''); + } else { + facetType = getFacetTypeFromFacet(facet, hierarchicalFieldIds); + } + + facetProps = { + ...facetProps, + customCssClasses: { + ...facetsCustomCssClasses, + ...facetProps.customCssClasses, + }, + }; + + switch (facetType) { + case FacetType.NUMERICAL: + return (); + case FacetType.HIERARCHICAL: + return (); + case FacetType.STANDARD: + // fall through + default: + return (); + } +} + +/** + * Returns the type of the facet based on the props. + * @param elementType - string + * @returns {@link FacetType} + * + * @internal + */ +export function getFacetTypeFromReactElementType(elementType: string) { + switch (elementType) { + case NumericalFacet.name.toString(): + return FacetType.NUMERICAL; + case HierarchicalFacet.name.toString(): + return FacetType.HIERARCHICAL; + case StandardFacet.name.toString(): + // fall through + default: + return FacetType.STANDARD; + } +} + +/** + * Returns the type of the facet based on facet. + * @param facet - {@link DisplayableFacet} + * @param hierarchicalFieldIds - string + * @returns {@link FacetType} + * + * @internal + */ +export function getFacetTypeFromFacet( + facet: DisplayableFacet, + hierarchicalFieldIds: string[] = [], +) { + if (hierarchicalFieldIds.includes(facet.fieldId)) { + return FacetType.HIERARCHICAL; + } else if (isStringFacet(facet)) { + return FacetType.STANDARD; + } else if (isNumericalFacet(facet)) { + return FacetType.NUMERICAL; + } + + return FacetType.STANDARD; +} diff --git a/src/components/FeaturedSnippetDirectAnswer.tsx b/src/components/FeaturedSnippetDirectAnswer.tsx index 97edc630f..f5c15b870 100644 --- a/src/components/FeaturedSnippetDirectAnswer.tsx +++ b/src/components/FeaturedSnippetDirectAnswer.tsx @@ -1,5 +1,6 @@ import { FeaturedSnippetDirectAnswer as FeaturedSnippetDirectAnswerType } from '@yext/search-headless-react'; import { renderHighlightedValue } from './utils/renderHighlightedValue'; +import { useMemo } from 'react'; /** * Props for {@link FeaturedSnippetDirectAnswer}. @@ -29,17 +30,32 @@ interface FeaturedSnippetDirectAnswerCssClasses { * * @internal */ + +const unsupportedTextFormats: string[] = ['rich_text', 'rich_text_v2', 'markdown']; + export function FeaturedSnippetDirectAnswer({ result, readMoreClickHandler, cssClasses = {} }: FeaturedSnippetDirectAnswerProps): JSX.Element { const answer = result.fieldType === 'multi_line_text' && result.value; - // TODO: SLAP-2340, update rich text snippets to convert the markdown - if (result.fieldType === 'rich_text') { - console.warn('Rendering markdown for rich text direct answer is currently not supported. Displaying the unrendered markdown string as is.'); + if (unsupportedTextFormats.includes(result.fieldType)) { + console.warn('Rendering ' + result.fieldType + ' direct answer is currently not supported. ' + + 'You can modify your search configuration to convert ' + result.fieldType + ' to HTML to be rendered ' + + 'on the page.'); + } + let snippet: JSX.Element; + const snippetValue = useMemo(() => + { return { __html: result.snippet?.value }; }, [result.snippet?.value]); + + if (result.fieldType === 'html') { + snippet = ( +
+ ); + } + else { + snippet = renderHighlightedValue(result.snippet, { highlighted: cssClasses.highlighted }); } - const snippet = renderHighlightedValue(result.snippet, { highlighted: cssClasses.highlighted }); const link = result.relatedResult.link || result.relatedResult.rawData.landingPageUrl as string; const name = result.relatedResult.name; const snippetLinkMessage = 'Read more about '; diff --git a/src/components/FilterGroup.tsx b/src/components/FilterGroup.tsx index 51e8eda9c..5f8ad7c65 100644 --- a/src/components/FilterGroup.tsx +++ b/src/components/FilterGroup.tsx @@ -1,11 +1,7 @@ import { useSearchUtilities } from '@yext/search-headless-react'; import { PropsWithChildren, useMemo, useState } from 'react'; -import { twMerge } from '../hooks/useComposedCssClasses'; import { CheckboxOption, - CollapsibleLabel, - CollapsibleLabelCssClasses, - builtInCollapsibleLabelCssClasses, CollapsibleSection, FilterOptionConfig, SearchInput, @@ -13,6 +9,7 @@ import { useFilterGroupContext, CheckboxCssClasses } from './Filters'; +import { FacetTitle } from './FacetTiltle'; /** * The CSS class interface for FilterGroup. @@ -79,27 +76,16 @@ export function FilterGroup({ }; }, [customCssClasses]); - const collapsibleLabelCssClasses: CollapsibleLabelCssClasses = useMemo(() => { - return { - label: cssClasses.titleLabel - }; - }, [cssClasses]); - - function renderTitle() { - return collapsible - ? - : (title && -
- {title} -
); - } - return ( - {renderTitle()} + {searchable && } = { showMoreButton: 'ml-4 text-sm font-medium text-primary' }; -export const DEFAULT_HIERARCHICAL_DELIMITER = '>'; - /** * A HierarchicalFacetDisplay takes a `DisplayableFacet` and renders the facet in a way * to represent multiple levels of "hierarchies". diff --git a/src/components/Geolocation.tsx b/src/components/Geolocation.tsx new file mode 100644 index 000000000..e7d425ef6 --- /dev/null +++ b/src/components/Geolocation.tsx @@ -0,0 +1,87 @@ +import { useComposedCssClasses } from '../hooks/useComposedCssClasses'; +import LoadingIndicator from '../icons/LoadingIndicator'; +import { YextIcon } from '../icons/YextIcon'; +import { useGeolocationHandler } from '../hooks/useGeolocationHandler'; + +/** + * The CSS class interface for the Geolocation component. + * + * @public + */ +export interface GeolocationCssClasses { + geolocationContainer?: string, + button?: string, + iconContainer?: string +} + +const builtInCssClasses: Readonly = { + geolocationContainer: 'text-sm text-neutral text-center justify-center items-center flex flex-row', + button: 'text-primary font-semibold hover:underline focus:underline', + iconContainer: 'w-4 ml-2' +}; + +/** + * The props for the Geolocation component. + * + * @public + */ +export interface GeolocationProps { + /** + * Configuration used when collecting the user's location. + * Definition: {@link https://w3c.github.io/geolocation-api/#position_options_interface}. + */ + geolocationOptions?: PositionOptions, + /** + * The radius, in miles, around the user's location to find results. Defaults to 50. + * If location accuracy is low, a larger radius may be used automatically. + */ + radius?: number, + /** The label for the button. Defaults to 'Use my location'. */ + label?: string, + /** Custom icon component to display along with the button. */ + GeolocationIcon?: React.FunctionComponent, + /** + * A function which is called when the geolocation button is clicked, + * after user's position is successfully determined. + */ + handleClick?: (position: GeolocationPosition) => void, + /** CSS classes for customizing the component styling. */ + customCssClasses?: GeolocationCssClasses +} + +/** + * A React Component which collects location information to create a + * location filter and perform a new search. + * + * @public + * + * @param props - {@link GeolocationProps} + * @returns A react component for geolocation + */ +export function Geolocation({ + geolocationOptions, + radius = 50, + label = 'Use my location', + //TODO: replace default icon with SVG create from design team + GeolocationIcon = YextIcon, + handleClick, + customCssClasses, +}: GeolocationProps): JSX.Element | null { + const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses); + const [handleGeolocationClick, isFetchingUserLocation] = useGeolocationHandler({ + geolocationOptions, + radius, + handleUserPosition: handleClick + }); + + return ( +
+ +
+ {isFetchingUserLocation ? : } +
+
+ ); +} diff --git a/src/components/HierarchicalFacetContent.tsx b/src/components/HierarchicalFacetContent.tsx new file mode 100644 index 000000000..6f35d4adf --- /dev/null +++ b/src/components/HierarchicalFacetContent.tsx @@ -0,0 +1,53 @@ +import { DisplayableFacet } from '@yext/search-headless-react'; +import { HierarchicalFacetProps } from './FacetProps'; +import { + CollapsibleSection, + FilterGroupProvider, + HierarchicalFacetDisplay +} from './Filters'; +import { FacetTitle } from './FacetTiltle'; + +/** + * A component that displays the content of a hierarchical facet. + * + * @param props - props to render the component + * @returns A React component for the content of a hierarchical facet + * + * @internal + */ +export function HierarchicalFacetContent({ + fieldId, + label, + transformOptions, + customCssClasses, + delimiter, + facet, + collapsible = true, + defaultExpanded = true, + showMoreLimit = 4, +}: HierarchicalFacetProps & { facet: DisplayableFacet }) { + const options = facet.options || []; + const transformedOptions = transformOptions ? (transformOptions(options) || []) : options; + + return ( + + + + + + + ); +} diff --git a/src/components/HierarchicalFacets.tsx b/src/components/HierarchicalFacets.tsx index fe162869c..f9e3724e1 100644 --- a/src/components/HierarchicalFacets.tsx +++ b/src/components/HierarchicalFacets.tsx @@ -23,6 +23,7 @@ export interface HierarchicalFacetsCssClasses extends HierarchicalFacetDisplayCs /** * Props for the {@link HierarchicalFacets} component. * + * @deprecated Use {@link HierarchicalFacet} with {@link Facets} instead. * @public */ export interface HierarchicalFacetsProps extends Omit { @@ -43,6 +44,7 @@ export interface HierarchicalFacetsProps extends Omit = { * The props for the {@link LocationBias} component. * * @public + * + * @deprecated LocationBias component has been superseded by Geolocation component. */ export interface LocationBiasProps { /** Configuration used when collecting the user's location. @@ -45,6 +49,8 @@ export interface LocationBiasProps { * * @public * + * @deprecated LocationBias component has been superseded by Geolocation component. + * * @param props - {@link LocationBiasProps} * @returns A react component for Location Bias */ diff --git a/src/components/NumericalFacetContent.tsx b/src/components/NumericalFacetContent.tsx new file mode 100644 index 000000000..d3c7bb65b --- /dev/null +++ b/src/components/NumericalFacetContent.tsx @@ -0,0 +1,50 @@ +import { FilterGroup } from './FilterGroup'; +import { DisplayableFacet } from '@yext/search-headless-react'; +import { NumericalFacetProps } from './FacetProps'; +import { RangeInput } from './Filters'; + +const DEFAULT_RANGE_INPUT_PREFIX = <>$; + +/** + * A component that displays the content of a numerical facet. + * + * @param props - props to render the component + * @returns A React component for the content of a standard facet + * + * @internal + */ +export function NumericalFacetContent({ + fieldId, + label, + transformOptions, + customCssClasses, + getFilterDisplayName, + facet, + showMoreLimit = 10, + showOptionCounts = false, + inputPrefix = DEFAULT_RANGE_INPUT_PREFIX, + ...filterGroupProps +}: NumericalFacetProps & { facet: DisplayableFacet }) { + const options = facet.options || []; + const transformedOptions = transformOptions ? (transformOptions(options) || []) : options; + + return ( + { + return showOptionCounts ? { ...o, resultsCount: o.count } : o; + })} + title={label || facet.displayName} + customCssClasses={customCssClasses} + showMoreLimit={showMoreLimit} + searchable={facet?.options.length > showMoreLimit} + {...filterGroupProps} + > + + + ); +} diff --git a/src/components/NumericalFacets.tsx b/src/components/NumericalFacets.tsx index 02d06e834..9a5e40e46 100644 --- a/src/components/NumericalFacets.tsx +++ b/src/components/NumericalFacets.tsx @@ -19,6 +19,7 @@ export interface NumericalFacetsCssClasses extends FilterGroupCssClasses, RangeI /** * Props for the {@link NumericalFacets} component. * + * @deprecated Use {@link NumericalFacet} with {@link Facets} instead. * @public */ export interface NumericalFacetsProps extends Omit { @@ -49,6 +50,7 @@ const DEFAULT_RANGE_INPUT_PREFIX = <>$; * @param props - {@link NumericalFacetsProps} * @returns A React component for facets * + * @deprecated Use {@link NumericalFacet} with {@link Facets} instead. * @public */ export function NumericalFacets({ diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 12384a43c..2675b943c 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -18,7 +18,6 @@ import { VerticalDividerIcon } from '../icons/VerticalDividerIcon'; import { HistoryIcon as RecentSearchIcon } from '../icons/HistoryIcon'; import { CloseIcon } from '../icons/CloseIcon'; import { MagnifyingGlassIcon } from '../icons/MagnifyingGlassIcon'; -import { YextIcon } from '../icons/YextIcon'; import { Dropdown } from './Dropdown/Dropdown'; import { useDropdownContext } from './Dropdown/DropdownContext'; import { DropdownInput } from './Dropdown/DropdownInput'; @@ -42,14 +41,14 @@ import { recursivelyMapChildren } from './utils/recursivelyMapChildren'; const builtInCssClasses: Readonly = { searchBarContainer: 'h-12 mb-6', inputDivider: 'border-t border-gray-200 mx-2.5', - inputElement: 'outline-none flex-grow border-none h-full pl-0.5 pr-2 text-neutral-dark text-base placeholder:text-neutral-light', + inputElement: 'outline-none flex-grow border-none h-11 pl-5 pr-2 text-neutral-dark text-base placeholder:text-neutral-light', searchButtonContainer: ' w-8 h-full mx-2 flex flex-col justify-center items-center', searchButton: 'h-7 w-7', focusedOption: 'bg-gray-100', clearButton: 'h-3 w-3 mr-3.5', verticalDivider: 'mr-0.5', - recentSearchesIcon: 'w-5 mr-1 text-gray-400', - recentSearchesOption: 'pl-3 text-neutral-dark', + recentSearchesIcon: 'w-5 mr-1 flex-shrink-0 h-full text-gray-400', + recentSearchesOption: 'whitespace-no-wrap max-w-full px-3 text-neutral-dark truncate', recentSearchesNonHighlighted: 'font-normal', // Swap this to semibold once we apply highlighting to recent searches verticalLink: 'ml-12 pl-1 text-neutral italic', entityPreviewsDivider: 'h-px bg-gray-200 mt-1 mb-4 mx-3.5', @@ -406,9 +405,6 @@ export function SearchBar({ onToggle={handleToggleDropdown} >
-
- -
{renderInput()} {query && renderClearButton()} { + return showOptionCounts ? { ...o, resultsCount: o.count } : o; + })} + title={label || facet.displayName} + customCssClasses={customCssClasses} + showMoreLimit={showMoreLimit} + searchable={facet?.options.length > showMoreLimit} + {...filterGroupProps} + /> + ); +} diff --git a/src/components/StandardFacets.tsx b/src/components/StandardFacets.tsx index afa4607ca..a5bf6074c 100644 --- a/src/components/StandardFacets.tsx +++ b/src/components/StandardFacets.tsx @@ -1,12 +1,13 @@ import { FacetsProvider } from './Filters'; import { FilterGroup, FilterGroupCssClasses } from './FilterGroup'; import { Fragment } from 'react'; -import { DisplayableFacet } from '@yext/search-headless-react'; import { FilterDivider } from './FilterDivider'; +import { isStringFacet } from '../utils/filterutils'; /** * The CSS class interface for {@link StandardFacets}. * + * @deprecated Use {@link StandardFacet} with {@link Facets} instead. * @public */ export interface StandardFacetsCssClasses extends FilterGroupCssClasses { @@ -17,6 +18,7 @@ export interface StandardFacetsCssClasses extends FilterGroupCssClasses { /** * Props for the {@link StandardFacets} component. * + * @deprecated Use {@link StandardFacet} with {@link Facets} instead. * @public */ export interface StandardFacetsProps { @@ -56,6 +58,7 @@ export interface StandardFacetsProps { * @param props - {@link StandardFacetsProps} * @returns A React component for facets * + * @deprecated Use {@link Facets} instead. * @public */ export function StandardFacets(props: StandardFacetsProps) { @@ -93,7 +96,3 @@ export function StandardFacets(props: StandardFacetsProps) { ); } - -function isStringFacet(facet: DisplayableFacet): boolean { - return facet.options.length > 0 && typeof facet.options[0].value === 'string'; -} \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts index 44dd1e1a5..fe2969523 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -45,6 +45,12 @@ export { LocationBiasProps } from './LocationBias'; +export { + Geolocation, + GeolocationCssClasses, + GeolocationProps +} from './Geolocation'; + export { AppliedFilters, AppliedFiltersCssClasses, @@ -119,6 +125,22 @@ export { NumericalFacetsProps } from './NumericalFacets'; +export { + Facets, + StandardFacet, + NumericalFacet, + HierarchicalFacet +} from './Facets'; + +export { + FacetsCssClasses, + FacetsProps, + FacetProps, + StandardFacetProps, + NumericalFacetProps, + HierarchicalFacetProps +} from './FacetProps'; + export { FilterGroupProps, FilterGroupCssClasses diff --git a/src/components/utils/renderAutocompleteResult.tsx b/src/components/utils/renderAutocompleteResult.tsx index da1d0098e..9f762bd00 100644 --- a/src/components/utils/renderAutocompleteResult.tsx +++ b/src/components/utils/renderAutocompleteResult.tsx @@ -14,8 +14,8 @@ export interface AutocompleteResultCssClasses { } export const builtInCssClasses: Readonly = { - option: 'flex whitespace-pre-wrap h-6.5 pl-3 text-neutral-dark', - icon: 'w-6 text-gray-400' + option: 'whitespace-no-wrap max-w-full px-3 text-neutral-dark truncate', + icon: 'w-6 h-full flex-shrink-0 text-gray-400' }; /** diff --git a/src/hooks/useGeolocationHandler.ts b/src/hooks/useGeolocationHandler.ts new file mode 100644 index 000000000..457f20274 --- /dev/null +++ b/src/hooks/useGeolocationHandler.ts @@ -0,0 +1,82 @@ +import { Matcher, SelectableStaticFilter, useSearchActions, useSearchState } from '@yext/search-headless-react'; +import { executeSearch } from '../utils/search-operations'; +import { getUserLocation } from '../utils/location-operations'; +import { useCallback, useState } from 'react'; + +const GEOLOCATION_FIELD_ID = 'builtin.location'; +const LOCATION_FIELD_IDS = [GEOLOCATION_FIELD_ID, 'builtin.region', 'address.countryCode']; +const METERS_PER_MILE = 1609.344; + +/** + * The props for {@link useGeolocationHandler} hook. + * + * @internal + */ +interface GeolocationHandlerArgs { + /** Configuration used when collecting the user's location. */ + geolocationOptions?: PositionOptions, + /** + * The radius, in miles, around the user's location to find results. Defaults to 50. + * If location accuracy is low, a larger radius may be used automatically. + */ + radius?: number, + /** Custom handler function to call after user's position is successfully determined. */ + handleUserPosition?: (position: GeolocationPosition) => void +} + +/** + * Creates a function to collect user's geolocation and, by default, will set + * a built-in location filter and execute a search. + * + * @internal + * + * @param props - {@link GeolocationHandlerArgs} + * @returns - A function to collect and process user's geolocation + * - A boolean to indicate if user's geolocation is being fetch + */ +export function useGeolocationHandler({ + geolocationOptions, + radius = 50, + handleUserPosition +}: GeolocationHandlerArgs): [() => Promise, boolean] { + const [isFetchingUserLocation, setIsFetchingUserLocation] = useState(false); + const searchActions = useSearchActions(); + const staticFilters = useSearchState(s => s.filters.static || []); + + const defaultHandleUserPosition = useCallback((position: GeolocationPosition) => { + const { latitude, longitude, accuracy } = position.coords; + const locationFilter: SelectableStaticFilter = { + displayName: 'Current Location', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: GEOLOCATION_FIELD_ID, + matcher: Matcher.Near, + value: { + lat: latitude, + lng: longitude, + radius: Math.max(accuracy, radius * METERS_PER_MILE) + }, + } + }; + const nonLocationFilters = staticFilters.filter(filter => { + return !(filter.filter.kind === 'fieldValue' + && LOCATION_FIELD_IDS.includes(filter.filter.fieldId)); + }); + searchActions.setStaticFilters([...nonLocationFilters, locationFilter]); + executeSearch(searchActions); + }, [radius, searchActions, staticFilters]); + + const geolocationHandler = useCallback(async () => { + setIsFetchingUserLocation(true); + try { + const position = await getUserLocation(geolocationOptions); + (handleUserPosition ?? defaultHandleUserPosition)(position); + } catch (e) { + console.warn(e); + } finally { + setIsFetchingUserLocation(false); + } + }, [setIsFetchingUserLocation, geolocationOptions, handleUserPosition, defaultHandleUserPosition]); + return [geolocationHandler, isFetchingUserLocation]; +} diff --git a/src/utils/filterutils.tsx b/src/utils/filterutils.tsx index 7347bf8f5..3e0d9e7c5 100644 --- a/src/utils/filterutils.tsx +++ b/src/utils/filterutils.tsx @@ -17,11 +17,19 @@ export function isNumberRangeValue(obj: unknown): obj is NumberRangeValue { return typeof obj === 'object' && !!obj && ('start' in obj || 'end' in obj); } +/** + * Checks if the facet is a string facet with string options. + */ +export function isStringFacet(facet: DisplayableFacet): boolean { + return facet.options.length > 0 && typeof facet.options[0].value === 'string'; +} + /** * Checks if the facet is a numerical facet with number range filter options. */ export function isNumericalFacet(facet: DisplayableFacet): boolean { - return facet.options.length > 0 && isNumberRangeFilter(facet.options[0]); + return facet.options.length > 0 && + facet.options.some(option => isNumberRangeFilter(option)); } /** diff --git a/test-site/package-lock.json b/test-site/package-lock.json index ec4d5bc19..a7d5d9940 100644 --- a/test-site/package-lock.json +++ b/test-site/package-lock.json @@ -32,7 +32,7 @@ }, "..": { "name": "@yext/search-ui-react", - "version": "1.2.0", + "version": "1.3.0", "license": "BSD-3-Clause", "dependencies": { "@microsoft/api-documenter": "^7.15.3", @@ -40,12 +40,12 @@ "@reach/auto-id": "^0.18.0", "@restart/ui": "^1.0.1", "@tailwindcss/forms": "^0.5.0", - "@yext/analytics": "^0.2.0-beta.3", + "@yext/analytics": "^0.5.0", "classnames": "^2.3.1", "lodash": "^4.17.21", "mapbox-gl": "^2.9.2", "prop-types": "^15.8.1", - "react-collapsed": "^3.3.0", + "react-collapsed": "3.6.0", "recent-searches": "^1.0.5", "tailwind-merge": "^1.3.0", "use-isomorphic-layout-effect": "^1.1.2" @@ -55,6 +55,7 @@ "@babel/preset-env": "^7.14.7", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.14.5", + "@etchteam/storybook-addon-status": "^4.2.2", "@percy/cli": "^1.8.0", "@percy/storybook": "^4.3.3", "@reduxjs/toolkit": "^1.8.6", @@ -79,7 +80,7 @@ "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "@yext/eslint-config-slapshot": "^0.5.0", - "@yext/search-headless-react": "^2.2.0", + "@yext/search-headless-react": "2.3.0", "axe-playwright": "^1.1.11", "babel-jest": "^27.0.6", "eslint": "^8.11.0", @@ -96,9 +97,9 @@ "typescript": "~4.5.5" }, "peerDependencies": { - "@yext/search-headless-react": "^2.2.0", - "react": "^16.14 || ^17 || ^18", - "react-dom": "^16.14 || ^17 || ^18" + "@yext/search-headless-react": "2.3.0", + "react": "^16.14 || ^17", + "react-dom": "^16.14 || ^17" } }, "../node_modules/eslint-config-react-app": { @@ -20080,6 +20081,7 @@ "@babel/preset-env": "^7.14.7", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.14.5", + "@etchteam/storybook-addon-status": "^4.2.2", "@microsoft/api-documenter": "^7.15.3", "@microsoft/api-extractor": "^7.19.4", "@percy/cli": "^1.8.0", @@ -20108,9 +20110,9 @@ "@types/react": "^17.0.38", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", - "@yext/analytics": "^0.2.0-beta.3", + "@yext/analytics": "^0.5.0", "@yext/eslint-config-slapshot": "^0.5.0", - "@yext/search-headless-react": "^2.2.0", + "@yext/search-headless-react": "2.3.0", "axe-playwright": "^1.1.11", "babel-jest": "^27.0.6", "classnames": "^2.3.1", @@ -20126,7 +20128,7 @@ "msw": "^0.36.8", "prop-types": "^15.8.1", "react": "^17.0.2", - "react-collapsed": "^3.3.0", + "react-collapsed": "3.6.0", "react-dom": "^17.0.2", "recent-searches": "^1.0.5", "tailwind-merge": "^1.3.0", diff --git a/test-site/src/pages/PeoplePage.tsx b/test-site/src/pages/PeoplePage.tsx index accb81fda..91c6befdd 100644 --- a/test-site/src/pages/PeoplePage.tsx +++ b/test-site/src/pages/PeoplePage.tsx @@ -10,12 +10,16 @@ import { LocationBias, StaticFilters, StandardFacets, + Facets, + HierarchicalFacet, HierarchicalFacets, FilterDivider, ApplyFiltersButton, Pagination, NumericalFacets, - AlternativeVerticals + AlternativeVerticals, + StandardFacet, + NumericalFacet } from '@yext/search-ui-react'; // import { CustomCard } from '../components/CustomCard'; @@ -55,11 +59,13 @@ export function PeoplePage() { title='Static Employee Department' filterOptions={employeeFilterConfigs} /> + + + + + + + + + + +
diff --git a/test-site/src/pages/ProductsPage.tsx b/test-site/src/pages/ProductsPage.tsx index 5bc2af79c..52c6a0640 100644 --- a/test-site/src/pages/ProductsPage.tsx +++ b/test-site/src/pages/ProductsPage.tsx @@ -5,7 +5,7 @@ import { SearchBar, StandardCard, VerticalResults, - LocationBias, + Geolocation, NumericalFacets, Pagination } from '@yext/search-ui-react'; @@ -34,7 +34,7 @@ export function ProductsPage() { CardComponent={StandardCard} /> - + diff --git a/tests/__fixtures__/data/filters.ts b/tests/__fixtures__/data/filters.ts index 504144a7b..06165e324 100644 --- a/tests/__fixtures__/data/filters.ts +++ b/tests/__fixtures__/data/filters.ts @@ -1,6 +1,7 @@ import { DisplayableFacet, Matcher, SelectableStaticFilter } from '@yext/search-headless-react'; import { RemovableFilter } from '../../../src/components/AppliedFiltersDisplay'; import { StaticFiltersProps } from '../../../src/components/StaticFilters'; +import { createHierarchicalFacet } from '../../__utils__/hierarchicalfacets'; function createRemovableFilter(value: string) { return { @@ -72,7 +73,13 @@ export const DisplayableFacets: DisplayableFacet[] = [ } ], displayName: 'Price' - } + }, + createHierarchicalFacet([ + 'food', + 'food > fruit', + { value: 'food > fruit > banana', selected: true }, + 'food > fruit > apple', + ]) ]; export const staticFilters: SelectableStaticFilter[] = [ diff --git a/tests/__utils__/facets.ts b/tests/__utils__/facets.ts new file mode 100644 index 000000000..9738ecf3e --- /dev/null +++ b/tests/__utils__/facets.ts @@ -0,0 +1,18 @@ +import { DisplayableFacetOption, FacetOption, SearchActions } from '@yext/search-headless-react'; + +export function getOptionLabelTextWithCount(option: DisplayableFacetOption) { + return `${option.displayName} (${option.count})`; +} + +export function expectFacetOptionSet( + actions: SearchActions, + fieldId: string, + option: FacetOption, + selected: boolean +) { + expect(actions.setFacetOption).toHaveBeenCalledWith( + fieldId, + { matcher: option.matcher, value: option.value }, + selected + ); +} diff --git a/tests/components/Facets.stories.tsx b/tests/components/Facets.stories.tsx new file mode 100644 index 000000000..e24affa12 --- /dev/null +++ b/tests/components/Facets.stories.tsx @@ -0,0 +1,35 @@ +import { ComponentMeta, Story } from '@storybook/react'; +import { SearchHeadlessContext, State } from '@yext/search-headless-react'; +import { generateMockedHeadless } from '../__fixtures__/search-headless'; +import { RecursivePartial } from '../__utils__/mocks'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; +import { Facets, FacetsProps } from '../../src'; +import { createHierarchicalFacet } from '../__utils__/hierarchicalfacets'; + +const meta: ComponentMeta = { + title: 'Facets', + component: Facets +}; +export default meta; + +const mockedHeadlessState: RecursivePartial = { + filters: { + facets: [ + ...DisplayableFacets, + createHierarchicalFacet([ + 'food', + 'food > fruit', + { value: 'food > fruit > banana', selected: true }, + 'food > fruit > apple', + ]) + ] + } +}; + +export const Primary: Story = (args) => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/tests/components/Facets.test.tsx b/tests/components/Facets.test.tsx new file mode 100644 index 000000000..65d0678aa --- /dev/null +++ b/tests/components/Facets.test.tsx @@ -0,0 +1,281 @@ +import { render, screen } from '@testing-library/react'; +import { Source, State } from '@yext/search-headless-react'; +import { mockAnswersHooks, mockAnswersState, spyOnActions } from '../__utils__/mocks'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; +import { + Facets, + StandardFacet, + StandardFacetProps, + NumericalFacet, + NumericalFacetProps, + HierarchicalFacet, HierarchicalFacetProps +} from '../../src'; +import { expectFacetOptionSet, getOptionLabelTextWithCount } from '../__utils__/facets'; +import { DisplayableFacetOption } from '@yext/search-core'; +import userEvent from '@testing-library/user-event'; + +const mockedState: Partial = { + filters: { + static: [], + facets: DisplayableFacets + }, + vertical: { + verticalKey: 'vertical', + results: [{ + source: Source.KnowledgeManager, + rawData: {} + }], + appliedQueryFilters: [] + }, + searchStatus: { + isLoading: false + }, + meta: { + searchType: 'vertical' + } +}; + +const mockedActions = { + state: mockedState, + setOffset: jest.fn(), + setFacetOption: jest.fn(), + executeVerticalQuery: jest.fn() +}; + +const mockedUtils = { + isCloseMatch: () => true +}; + +jest.mock('@yext/search-headless-react'); + +describe('Facets', () => { + beforeEach(() => { + mockAnswersHooks({ mockedState, mockedActions, mockedUtils }); + }); + it('Properly renders standard facets if present', () => { + render(); + const facet = DisplayableFacets[0]; + + expect(screen.getByText(facet.displayName)).toBeDefined(); + facet.options.forEach(o => { + expect(screen.getByText(getOptionLabelTextWithCount(o))).toBeDefined(); + }); + }); + + it('Properly renders numerical facets if present', () => { + render(); + const numericalFilter = DisplayableFacets[1]; + + expect(screen.getByText(numericalFilter.displayName)).toBeDefined(); + numericalFilter.options.forEach(o => { + expect(screen.getByText(o.displayName)).toBeDefined(); + }); + }); + + it('Does not render filters if no facets are present', () => { + mockAnswersState({ + ...mockedState, + filters: { + static: [], + facets: [] + } + }); + render(); + const facet = DisplayableFacets[0]; + const numericalFilter = DisplayableFacets[1]; + + expect(screen.queryByText(numericalFilter.displayName)).toBeNull(); + numericalFilter.options.forEach(o => { + expect(screen.queryByText(o.displayName)).toBeNull(); + }); + + expect(screen.queryByText(facet.displayName)).toBeNull(); + facet.options.forEach(o => { + expect(screen.queryByText(`${o.displayName} (${o.count})}`)).toBeNull(); + }); + }); + + it('Properly renders an override standard facet if present', () => { + const mockTransformOptions = (options: DisplayableFacetOption[]) => + options.map(option => ({ ...option, displayName: `my ${option.displayName}` })); + + const overrideFieldId = 'products'; + const overrideLabel = 'My Products'; + const props: StandardFacetProps = { + fieldId: overrideFieldId, + label: overrideLabel, + transformOptions: mockTransformOptions, + }; + + render( + + + ); + const facet = DisplayableFacets[0]; + + expect(screen.getByText(overrideLabel)).toBeDefined(); + expect(screen.queryByText(facet.displayName)).toBeNull(); + expect( + screen + .getByText( + `my ${facet.options[0].displayName} (${facet.options[0].count})`)) + .toBeDefined(); + }); + + it('Properly renders an override numerical facet if present', () => { + const mockTransformOptions = (options: DisplayableFacetOption[]) => + options.map(option => ({ ...option, displayName: `Price is ${option.displayName}` })); + + const overrideFieldId = 'price'; + const overrideLabel = 'My Price'; + const props: NumericalFacetProps = { + fieldId: overrideFieldId, + label: overrideLabel, + transformOptions: mockTransformOptions, + }; + + render( + + + ); + const facet = DisplayableFacets[1]; + + expect(screen.getByText(overrideLabel)).toBeDefined(); + expect(screen.queryByText(facet.displayName)).toBeNull(); + expect(screen.getByText(`Price is ${facet.options[0].displayName}`)).toBeDefined(); + }); + + it('Properly renders an override hierarchical facet if present', () => { + const overrideFieldId = 'hier'; + const overrideLabel = 'My Fruit'; + const props: HierarchicalFacetProps = { + fieldId: overrideFieldId, + label: overrideLabel, + }; + + render( + + + ); + const facet = DisplayableFacets[2]; + + expect(screen.getByText(overrideLabel)).toBeDefined(); + expect(screen.queryByText(facet.displayName)).toBeNull(); + }); + + it('Clicking a facet option executes a search by default', () => { + mockAnswersState({ + ...mockedState, + filters: { facets: [DisplayableFacets[0]] } + }); + const actions = spyOnActions(); + render(); + + const facet = DisplayableFacets[0]; + const coffeeCheckbox: HTMLInputElement = screen.getByLabelText( + getOptionLabelTextWithCount(facet.options[0]) + ); + expect(coffeeCheckbox.checked).toBeFalsy(); + + userEvent.click(coffeeCheckbox); + expectFacetOptionSet(actions, facet.fieldId, facet.options[0], true); + expect(actions.executeVerticalQuery).toBeCalled(); + }); + + it('Clicking a facet option does not execute a search when searchOnChange is false', () => { + mockAnswersState({ + ...mockedState, + filters: { facets: [DisplayableFacets[0]] } + }); + const actions = spyOnActions(); + render(); + + const facet = DisplayableFacets[0]; + const coffeeCheckbox: HTMLInputElement = screen.getByLabelText( + getOptionLabelTextWithCount(facet.options[0]) + ); + expect(coffeeCheckbox.checked).toBeFalsy(); + + userEvent.click(coffeeCheckbox); + expectFacetOptionSet(actions, facet.fieldId, facet.options[0], true); + expect(actions.executeVerticalQuery).not.toBeCalled(); + }); + + it('Renders all facets by default', () => { + const overrideFieldId = 'products'; + const overrideLabel = 'My Products'; + const props: StandardFacetProps = { + fieldId: overrideFieldId, + label: overrideLabel, + }; + + render( + + + ); + + expect(screen.getByText(overrideLabel)).toBeDefined(); + expect(screen.getByText(DisplayableFacets[1].displayName)).toBeDefined(); + expect(screen.getByText(DisplayableFacets[2].displayName)).toBeDefined(); + }); + + it('Only render customize facets if onlyRenderChildren is set to true', () => { + const overrideFieldId = 'products'; + const overrideLabel = 'My Products'; + const props: StandardFacetProps = { + fieldId: overrideFieldId, + label: overrideLabel, + }; + + render( + + + ); + + expect(screen.getByText(overrideLabel)).toBeDefined(); + expect(screen.queryByText(DisplayableFacets[1].displayName)).toBeNull(); + expect(screen.queryByText(DisplayableFacets[2].displayName)).toBeNull(); + }); + + it('Use FilterGroupCssClasses provided on the Facets level if not provided on the singular facet', + () => { + const overrideFieldId = 'products'; + const overrideLabel = 'My Products'; + const facetsTitleLabelClass = 'facets-title-label-class'; + const props: StandardFacetProps = { + fieldId: overrideFieldId, + label: overrideLabel, + }; + + render( + + + ); + + expect(screen.getByText(overrideLabel)).toBeDefined(); + expect(screen.getByText(overrideLabel)).toHaveClass(facetsTitleLabelClass); + expect(screen.getByText(DisplayableFacets[1].displayName)).toBeDefined(); + expect(screen.getByText(DisplayableFacets[1].displayName)).toHaveClass(facetsTitleLabelClass); + }); + + it('Use FilterGroupCssClasses provided on the singular facet level if provided', + () => { + const overrideFieldId = 'products'; + const overrideLabel = 'My Products'; + const facetsTitleLabelClass = 'facets-title-label-class'; + const standardFacetTitleLabelClass = 'standard-facet-title-label-class'; + const props: StandardFacetProps = { + fieldId: overrideFieldId, + label: overrideLabel, + customCssClasses: { titleLabel: standardFacetTitleLabelClass }, + }; + + render( + + + ); + + expect(screen.getByText(overrideLabel)).toBeDefined(); + expect(screen.getByText(overrideLabel)).toHaveClass(standardFacetTitleLabelClass); + }); +}); diff --git a/tests/components/Geolocation.stories.tsx b/tests/components/Geolocation.stories.tsx new file mode 100644 index 000000000..e74cc8614 --- /dev/null +++ b/tests/components/Geolocation.stories.tsx @@ -0,0 +1,45 @@ +import { ComponentMeta, Story } from '@storybook/react'; +import { SearchHeadlessContext } from '@yext/search-headless-react'; + +import { decorator as LocationOperationDecorator } from '../__fixtures__/utils/location-operations'; +import { generateMockedHeadless } from '../__fixtures__/search-headless'; +import { VerticalSearcherState } from '../__fixtures__/headless-state'; +import { userEvent, within } from '@storybook/testing-library'; +import { Geolocation, GeolocationProps } from '../../src/components/Geolocation'; + +const meta: ComponentMeta = { + title: 'Geolocation', + component: Geolocation, + argTypes: { + geolocationOptions: { + control: false + }, + GeolocationIcon: { + control: false + }, + handleClick: { + control: false + }, + } +}; +export default meta; + +export const Primary: Story = (args) => { + return ( + + + + ); +}; + +export const Loading = Primary.bind({}); +Loading.decorators = [LocationOperationDecorator]; +Loading.parameters = { + geoLocation: { + isFetching: true + } +}; +Loading.play = ({ canvasElement }) => { + const canvas = within(canvasElement); + userEvent.click(canvas.getByText('Use my location')); +}; diff --git a/tests/components/Geolocation.test.tsx b/tests/components/Geolocation.test.tsx new file mode 100644 index 000000000..d5139a2f7 --- /dev/null +++ b/tests/components/Geolocation.test.tsx @@ -0,0 +1,266 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Geolocation } from '../../src/components/Geolocation'; +import { Matcher, SelectableStaticFilter, State } from '@yext/search-headless-react'; +import * as locationOperations from '../../src/utils/location-operations'; +import { mockAnswersHooks, mockAnswersState, spyOnActions } from '../__utils__/mocks'; + +jest.mock('@yext/search-headless-react'); + +const mockedState: Partial = { + filters: { + static: [] + }, + vertical: { + verticalKey: 'jobs', + }, + searchStatus: { + isLoading: false + }, + meta: { + searchType: 'vertical' + } +}; + +const mockedStateWithFilters: Partial = { + ...mockedState, + filters: { + static: [ + { + displayName: 'Some Location', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'builtin.location', + matcher: Matcher.Near, + value: { + lat: 1, + lng: 1, + radius: 10 + }, + } + }, + { + displayName: 'Current Location', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'builtin.location', + matcher: Matcher.Near, + value: { + lat: 2, + lng: 3, + radius: 10 + }, + } + }, + { + displayName: 'Virginia, US', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'builtin.region', + matcher: Matcher.Equals, + value: 'US-VA', + } + }, + { + displayName: 'United States', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'address.countryCode', + matcher: Matcher.Equals, + value: 'US', + } + }, + { + displayName: 'My name', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'employeeName', + matcher: Matcher.Equals, + value: 'Bob', + } + } + ] + } +}; + +const newGeoPosition: GeolocationPosition = { + coords: { + accuracy: 0, + altitude: null, + altitudeAccuracy: null, + heading: null, + latitude: 40.741591687843005, + longitude: -74.00530254443494, + speed: null, + }, + timestamp: 0 +}; + +const newGeoPositionWithLowAccuracy: GeolocationPosition = { + coords: { + ...newGeoPosition.coords, + accuracy: 100000, + }, + timestamp: 0 +}; + +beforeEach(() => { + mockAnswersHooks({ + mockedState, + mockedActions: { + state: mockedState, + setStaticFilters: jest.fn(), + executeVerticalQuery: jest.fn() + } + }); + jest.spyOn(locationOperations, 'getUserLocation').mockResolvedValue(newGeoPosition); +}); + +it('renders custom label when provided', () => { + render(); + const updateLocationButton = screen.getByRole('button', { name: 'Click me!' }); + expect(updateLocationButton).toBeDefined(); +}); + +it('renders custom icon when provided', () => { + render( Custom Icon} />); + const LocationIcon = screen.getByAltText('Custom Icon'); + expect(LocationIcon).toBeDefined(); +}); + +describe('custom click handler', () => { + it('executes handleClick when user\'s location is successfully determined', async () => { + const mockedHandleClickFn = jest.fn(); + const actions = spyOnActions(); + render(); + clickUpdateLocation(); + await waitFor(() => { + expect(mockedHandleClickFn).toHaveBeenCalledWith(newGeoPosition); + }); + expect(actions.executeVerticalQuery).not.toBeCalled(); + }); + + it('does not execute handleClick when error occurs from collecting user\'s location', async () => { + const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); + jest.spyOn(locationOperations, 'getUserLocation').mockRejectedValue('mocked error!'); + const mockedHandleClickFn = jest.fn(); + render(); + clickUpdateLocation(); + await waitFor(() => { + expect(consoleWarnSpy).toBeCalledWith('mocked error!'); + }); + expect(mockedHandleClickFn).not.toBeCalled(); + }); +}); + +describe('default click handler', () => { + it('sets a location filter using provided radius', async () => { + const actions = spyOnActions(); + render(); + clickUpdateLocation(); + + const expectedLocationFilter: SelectableStaticFilter = createLocationFilter(10 * 1609.344); + await waitFor(() => { + expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]); + }); + }); + + it('sets a location filter with user\'s coordinates in static filters state when clicked', async () => { + const actions = spyOnActions(); + render(); + clickUpdateLocation(); + + const expectedLocationFilter: SelectableStaticFilter = createLocationFilter(); + expect(locationOperations.getUserLocation).toBeCalled(); + await waitFor(() => { + expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]); + }); + }); + + it('replaces existing location filters with a new location filter in static filters state', async () => { + mockAnswersState(mockedStateWithFilters); + const actions = spyOnActions(); + render(); + clickUpdateLocation(); + + const expectedStaticFilters = [ + { + displayName: 'My name', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'employeeName', + matcher: Matcher.Equals, + value: 'Bob', + } + }, + createLocationFilter() + ]; + await waitFor(() => { + expect(actions.setStaticFilters).toBeCalledWith(expectedStaticFilters); + }); + }); + + it('sets a location filter using a larger radius than provided value due to low accuracy of user coordinate', async () => { + jest.spyOn(locationOperations, 'getUserLocation').mockResolvedValue(newGeoPositionWithLowAccuracy); + const actions = spyOnActions(); + render(); + clickUpdateLocation(); + + const accuracy = newGeoPositionWithLowAccuracy.coords.accuracy; + const expectedLocationFilter: SelectableStaticFilter = createLocationFilter(accuracy); + await waitFor(() => { + expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]); + }); + }); + + it('executes a new search when clicked', async () => { + const actions = spyOnActions(); + render(); + clickUpdateLocation(); + + await waitFor(() => { + expect(actions.executeVerticalQuery).toBeCalled(); + }); + }); + + it('does not execute default handleClick when error occurs from collecting user\'s location', async () => { + const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); + jest.spyOn(locationOperations, 'getUserLocation').mockRejectedValue('mocked error!'); + const actions = spyOnActions(); + render(); + clickUpdateLocation(); + await waitFor(() => { + expect(consoleWarnSpy).toBeCalledWith('mocked error!'); + }); + expect(actions.setStaticFilters).not.toBeCalled(); + expect(actions.executeVerticalQuery).not.toBeCalled(); + }); +}); + +function clickUpdateLocation() { + const updateLocationButton = screen.getByRole('button'); + userEvent.click(updateLocationButton); +} + +function createLocationFilter(radius: number = 50 * 1609.344): SelectableStaticFilter { + return { + displayName: 'Current Location', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'builtin.location', + matcher: Matcher.Near, + value: { + lat: newGeoPosition.coords.latitude, + lng: newGeoPosition.coords.longitude, + radius + }, + } + }; +} diff --git a/tests/components/HierarchicalFacet.stories.tsx b/tests/components/HierarchicalFacet.stories.tsx new file mode 100644 index 000000000..b2868fa05 --- /dev/null +++ b/tests/components/HierarchicalFacet.stories.tsx @@ -0,0 +1,37 @@ +import { ComponentMeta, Story } from '@storybook/react'; +import { Facets, HierarchicalFacetProps, HierarchicalFacet } from '../../src'; +import { SearchHeadlessContext, State } from '@yext/search-headless-react'; +import { generateMockedHeadless } from '../__fixtures__/search-headless'; +import { RecursivePartial } from '../__utils__/mocks'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; +import { createHierarchicalFacet } from '../__utils__/hierarchicalfacets'; + +const meta: ComponentMeta = { + title: 'Facets', + component: Facets +}; +export default meta; + +const mockedHeadlessState: RecursivePartial = { + filters: { + facets: [ + ...DisplayableFacets, + createHierarchicalFacet([ + 'food', + 'food > fruit', + { value: 'food > fruit > banana', selected: true }, + 'food > fruit > apple', + ]) + ] + } +}; + +export const Primary: Story = (args) => { + return ( + + + + + + ); +}; \ No newline at end of file diff --git a/tests/components/HierarchicalFacetContent.test.tsx b/tests/components/HierarchicalFacetContent.test.tsx new file mode 100644 index 000000000..c76c136fd --- /dev/null +++ b/tests/components/HierarchicalFacetContent.test.tsx @@ -0,0 +1,141 @@ +import { render, screen } from '@testing-library/react'; +import { Matcher, Source, State } from '@yext/search-headless-react'; +import { spyOnActions, mockAnswersState, mockAnswersHooks } from '../__utils__/mocks'; +import userEvent from '@testing-library/user-event'; +import { HierarchicalFacetProps } from '../../src'; +import { HierarchicalFacetContent } from '../../src/components/HierarchicalFacetContent'; +import { FacetsProvider } from '../../src/components/Filters'; +import { createHierarchicalFacet } from '../__utils__/hierarchicalfacets'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; + +const hierarchicalFacet = DisplayableFacets[2]; + +const mockedState: Partial = { + filters: { + static: [], + facets: [hierarchicalFacet] + }, + vertical: { + verticalKey: 'vertical', + results: [{ + source: Source.KnowledgeManager, + rawData: {} + }], + appliedQueryFilters: [] + }, + searchStatus: { + isLoading: false + }, + meta: { + searchType: 'vertical' + } +}; + +const mockedActions = { + state: mockedState, + executeVerticalQuery: jest.fn(), + setOffset: jest.fn(), + setFilterOption: jest.fn(), + setFacetOption: jest.fn(), + resetFacets: jest.fn(), + setStaticFilters: jest.fn() +}; + +jest.mock('@yext/search-headless-react'); + +const mockHierarchicalFacet = (props?: HierarchicalFacetProps) => { + return ( + + {facets => facets.map(facet => ( + ))} + ); +}; + +describe('HierarchicalFacetsContent', () => { + beforeEach(() => { + mockAnswersHooks({ mockedState, mockedActions }); + }); + + it('Properly renders hierarchical facets', () => { + render(mockHierarchicalFacet()); + + expect(screen.getByRole('button', { name: /food/i })).toBeTruthy(); + expect(screen.getAllByRole('button', { name: /fruit/i })).toBeTruthy(); + expect(screen.getByRole('button', { name: /banana/i })).toBeTruthy(); + expect(screen.getByRole('button', { name: /apple/i })).toBeTruthy(); + }); + it('Clicking the currently selected option deselects it and selects its parent', () => { + const actions = spyOnActions(); + render(mockHierarchicalFacet()); + + const bananaButton = screen.getByRole('button', { name: /banana/i }); + userEvent.click(bananaButton); + + expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false }); + expectFacetOptionSet(actions, { value: 'food > fruit', selected: true }); + }); + it('Clicking a non-selected option selects it and deselects its siblings', () => { + const actions = spyOnActions(); + render(mockHierarchicalFacet()); + + const appleButton = screen.getByRole('button', { name: /apple/i }); + userEvent.click(appleButton); + + expectFacetOptionSet(actions, { value: 'food > fruit > apple', selected: true }); + expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false }); + }); + it('Clicking the current category with a selected child selects the category and deselects the ' + + 'child', () => { + const actions = spyOnActions(); + render(mockHierarchicalFacet()); + + const currentCategoryButton = screen.getAllByRole('button', { name: /fruit/i })[1]; + userEvent.click(currentCategoryButton); + + expectFacetOptionSet(actions, { value: 'food > fruit', selected: true }); + expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false }); + }); + it('Clicking a selected current category deselects it and selects its parent', () => { + const facets = [ + createHierarchicalFacet([ + 'food', + { value: 'food > fruit', selected: true }, + 'food > fruit > banana', + 'food > fruit > apple', + ]) + ]; + + mockAnswersState({ + ...mockedState, + filters: { facets: facets } + }); + + const actions = spyOnActions(); + render(mockHierarchicalFacet()); + + const currentCategoryButton = screen.getAllByRole('button', { name: /fruit/i })[1]; + userEvent.click(currentCategoryButton); + + expectFacetOptionSet(actions, { value: 'food > fruit', selected: false }); + expectFacetOptionSet(actions, { value: 'food', selected: true }); + }); + it('Clicking a parent category selects it and deselects its children', () => { + const actions = spyOnActions(); + render(mockHierarchicalFacet()); + + const parentCategoryButton = screen.getByRole('button', { name: /food/i }); + userEvent.click(parentCategoryButton); + + expectFacetOptionSet(actions, { value: 'food', selected: true }); + expectFacetOptionSet(actions, { value: 'food > fruit', selected: false }); + expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false }); + }); +}); + +function expectFacetOptionSet(actions, facet: { value: string, selected: boolean }) { + expect(actions.setFacetOption).toHaveBeenCalledWith( + 'hier', + { matcher: Matcher.Equals, value: facet.value }, + facet.selected + ); +} diff --git a/tests/components/LocationBias.stories.tsx b/tests/components/LocationBias.stories.tsx index ff5ce6c8b..ef61c2be3 100644 --- a/tests/components/LocationBias.stories.tsx +++ b/tests/components/LocationBias.stories.tsx @@ -15,7 +15,12 @@ const meta: ComponentMeta = { geolocationOptions: { control: false } - } + }, + parameters: { + status: { + type: 'deprecated', + } + }, }; export default meta; diff --git a/tests/components/NumericalFacet.stories.tsx b/tests/components/NumericalFacet.stories.tsx new file mode 100644 index 000000000..ef47eff76 --- /dev/null +++ b/tests/components/NumericalFacet.stories.tsx @@ -0,0 +1,37 @@ +import { ComponentMeta, Story } from '@storybook/react'; +import { Facets, NumericalFacetProps, NumericalFacet } from '../../src'; +import { SearchHeadlessContext, State } from '@yext/search-headless-react'; +import { generateMockedHeadless } from '../__fixtures__/search-headless'; +import { RecursivePartial } from '../__utils__/mocks'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; +import { createHierarchicalFacet } from '../__utils__/hierarchicalfacets'; + +const meta: ComponentMeta = { + title: 'Facets', + component: Facets +}; +export default meta; + +const mockedHeadlessState: RecursivePartial = { + filters: { + facets: [ + ...DisplayableFacets, + createHierarchicalFacet([ + 'food', + 'food > fruit', + { value: 'food > fruit > banana', selected: true }, + 'food > fruit > apple', + ]) + ] + } +}; + +export const Primary: Story = (args) => { + return ( + + + + + + ); +}; \ No newline at end of file diff --git a/tests/components/NumericalFacetContent.test.tsx b/tests/components/NumericalFacetContent.test.tsx new file mode 100644 index 000000000..1b3c99ff3 --- /dev/null +++ b/tests/components/NumericalFacetContent.test.tsx @@ -0,0 +1,176 @@ +import { render, screen } from '@testing-library/react'; +import { SearchActions, FacetOption, Matcher, NumberRangeValue, SelectableStaticFilter, Source, State } from '@yext/search-headless-react'; +import { mockAnswersHooks, mockAnswersState, spyOnActions } from '../__utils__/mocks'; +import userEvent from '@testing-library/user-event'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; +import { NumericalFacetProps } from '../../src'; +import { NumericalFacetContent } from '../../src/components/NumericalFacetContent'; +import { FacetsProvider } from '../../src/components/Filters'; +import { getOptionLabelTextWithCount } from '../__utils__/facets'; + +const numericalFacet = DisplayableFacets[1]; + +const mockedState: Partial = { + filters: { + static: [], + facets: [numericalFacet] + }, + vertical: { + verticalKey: 'vertical', + results: [{ + source: Source.KnowledgeManager, + rawData: {} + }], + appliedQueryFilters: [] + }, + searchStatus: { + isLoading: false + }, + meta: { + searchType: 'vertical' + } +}; + +const mockedActions = { + state: mockedState, + setOffset: jest.fn(), + setFacetOption: jest.fn(), + setFilterOption: jest.fn(), + executeVerticalQuery: jest.fn() +}; + +const mockedUtils = { + isCloseMatch: () => true +}; + +jest.mock('@yext/search-headless-react'); + +const mockNumericalFacet = (props?: NumericalFacetProps) => { + return ( + + {facets => facets.map(facet => ( + ))} + ); +}; + +describe('NumericalFacetContent', () => { + beforeEach(() => { + mockAnswersHooks({ mockedState, mockedActions, mockedUtils }); + }); + + it('Properly renders number range facets', () => { + render(mockNumericalFacet()); + + expect(screen.getByText(numericalFacet.displayName)).toBeDefined(); + numericalFacet.options.forEach(o => { + expect(screen.getByText(o.displayName)).toBeDefined(); + }); + }); + + it('Clicking a selected number range facet option checkbox unselects it', () => { + const actions = spyOnActions(); + render(mockNumericalFacet()); + + const expensiveCheckbox: HTMLInputElement = + screen.getByLabelText(numericalFacet.options[0].displayName); + expect(expensiveCheckbox.checked).toBeTruthy(); + + userEvent.click(expensiveCheckbox); + expectFacetOptionSet(actions, numericalFacet.fieldId, numericalFacet.options[0], false); + }); + + it('Clicking an unselected number range facet option checkbox selects it', () => { + const actions = spyOnActions(); + render(mockNumericalFacet()); + + const cheapCheckbox: HTMLInputElement = + screen.getByLabelText(numericalFacet.options[1].displayName); + expect(cheapCheckbox.checked).toBeFalsy(); + + userEvent.click(cheapCheckbox); + expectFacetOptionSet(actions, numericalFacet.fieldId, numericalFacet.options[1], true); + }); + + it('getFilterDisplayName field works as expected', () => { + const facets = [{ + ...numericalFacet, + options: numericalFacet.options.map(o => ({ ...o, selected: false })) + }]; + mockAnswersState({ + ...mockedState, + filters: { facets: facets } + }); + const getFilterDisplayName = (value: NumberRangeValue) => { + return 'start-' + value.start?.value + ' end-' + value.end?.value; + }; + const actions = spyOnActions(); + render(mockNumericalFacet( + { fieldId: numericalFacet.fieldId, getFilterDisplayName: getFilterDisplayName })); + + userEvent.type(screen.getByPlaceholderText('Min'), '1'); + userEvent.type(screen.getByPlaceholderText('Max'), '5'); + userEvent.click(screen.getByText('Apply')); + + const expectedSelectableFilter: SelectableStaticFilter = { + displayName: 'start-1 end-5', + selected: true, + filter: { + kind: 'fieldValue', + fieldId: 'price', + value: { + start: { + matcher: Matcher.GreaterThanOrEqualTo, + value: 1 + }, + end: { + matcher: Matcher.LessThanOrEqualTo, + value: 5 + } + }, + matcher: Matcher.Between + } + }; + expect(actions.setFilterOption).toHaveBeenCalledWith(expectedSelectableFilter); + }); + + it('inputPrefix field works as expected', () => { + render(mockNumericalFacet({ fieldId: numericalFacet.fieldId, inputPrefix: <>some prefix })); + + expect(screen.getAllByText('some prefix').length).toEqual(2); + }); + + it('Does not display option counts by default', () => { + render(mockNumericalFacet()); + + const label = screen.queryByLabelText(numericalFacet.options[0].displayName); + const labelAndCount = + screen.queryByLabelText(getOptionLabelTextWithCount(numericalFacet.options[0])); + + expect(label).toBeDefined(); + expect(labelAndCount).toBeNull(); + }); + + it('Display option counts if showOptionCounts is set to true', () => { + render(mockNumericalFacet({ fieldId: numericalFacet.fieldId, showOptionCounts: true })); + + const label = screen.queryByLabelText(numericalFacet.options[0].displayName); + const labelAndCount = + screen.queryByLabelText(getOptionLabelTextWithCount(numericalFacet.options[0])); + + expect(label).toBeNull(); + expect(labelAndCount).toBeDefined(); + }); +}); + +function expectFacetOptionSet( + actions: SearchActions, + fieldId: string, + option: FacetOption, + selected: boolean +) { + expect(actions.setFacetOption).toHaveBeenCalledWith( + fieldId, + { matcher: option.matcher, value: option.value }, + selected + ); +} diff --git a/tests/components/StandardFacet.stories.tsx b/tests/components/StandardFacet.stories.tsx new file mode 100644 index 000000000..834ffd206 --- /dev/null +++ b/tests/components/StandardFacet.stories.tsx @@ -0,0 +1,28 @@ +import { ComponentMeta, Story } from '@storybook/react'; +import { Facets, NumericalFacetProps, NumericalFacet } from '../../src'; +import { SearchHeadlessContext, State } from '@yext/search-headless-react'; +import { generateMockedHeadless } from '../__fixtures__/search-headless'; +import { RecursivePartial } from '../__utils__/mocks'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; + +const meta: ComponentMeta = { + title: 'Facets', + component: Facets +}; +export default meta; + +const mockedHeadlessState: RecursivePartial = { + filters: { + facets: DisplayableFacets + } +}; + +export const Primary: Story = (args) => { + return ( + + + + + + ); +}; \ No newline at end of file diff --git a/tests/components/StandardFacetContent.test.tsx b/tests/components/StandardFacetContent.test.tsx new file mode 100644 index 000000000..6a35a9bf5 --- /dev/null +++ b/tests/components/StandardFacetContent.test.tsx @@ -0,0 +1,120 @@ +import { render, screen } from '@testing-library/react'; +import { FacetOption, Source, State, SearchActions } from '@yext/search-headless-react'; +import { mockAnswersHooks, spyOnActions } from '../__utils__/mocks'; +import userEvent from '@testing-library/user-event'; +import { DisplayableFacets } from '../__fixtures__/data/filters'; +import { getOptionLabelTextWithCount } from '../__utils__/facets'; +import { StandardFacetContent } from '../../src/components/StandardFacetContent'; +import { StandardFacetProps } from '../../src/components'; +import { FacetsProvider } from '../../src/components/Filters'; + +const standardFacet = DisplayableFacets[0]; + +const mockedState: Partial = { + filters: { + static: [], + facets: [standardFacet] + }, + vertical: { + verticalKey: 'vertical', + results: [{ + source: Source.KnowledgeManager, + rawData: {} + }], + appliedQueryFilters: [] + }, + searchStatus: { + isLoading: false + }, + meta: { + searchType: 'vertical' + } +}; + +const mockedActions = { + state: mockedState, + setOffset: jest.fn(), + setFacetOption: jest.fn(), + executeVerticalQuery: jest.fn() +}; + +const mockedUtils = { + isCloseMatch: () => true +}; + +jest.mock('@yext/search-headless-react'); + +const mockStandardFacet = (props?: StandardFacetProps) => { + return ( + + {facets => facets.map(facet => ( + ))} + ); +}; + +describe('StandardFacetContent', () => { + beforeEach(() => { + mockAnswersHooks({ mockedState, mockedActions, mockedUtils }); + }); + + it('Display option counts by default', () => { + render(mockStandardFacet()); + + const coffeeLabel = screen.queryByLabelText(standardFacet.options[0].displayName); + const coffeeLabelAndCount = + screen.queryByLabelText(getOptionLabelTextWithCount(standardFacet.options[0])); + + expect(coffeeLabel).toBeNull(); + expect(coffeeLabelAndCount).toBeDefined(); + }); + + it('Does not display option counts if showOptionCounts is set to false', () => { + render(mockStandardFacet({ fieldId: standardFacet.fieldId, showOptionCounts: false })); + + const coffeeLabel = screen.queryByLabelText(standardFacet.options[0].displayName); + const coffeeLabelAndCount = + screen.queryByLabelText(getOptionLabelTextWithCount(standardFacet.options[0])); + + expect(coffeeLabel).toBeDefined(); + expect(coffeeLabelAndCount).toBeNull(); + }); + + it('Clicking an unselected facet option label selects it', () => { + const actions = spyOnActions(); + render(mockStandardFacet()); + + const coffeeFacetOption = standardFacet.options[0]; + const labelText = getOptionLabelTextWithCount(coffeeFacetOption); + const coffeeCheckbox: HTMLInputElement = screen.getByLabelText(labelText); + expect(coffeeCheckbox.checked).toBeFalsy(); + + const coffeeLabel = screen.getByText(labelText); + userEvent.click(coffeeLabel); + expectFacetOptionSet(actions, standardFacet.fieldId, coffeeFacetOption, true); + }); + + it('Clicking a selected facet option checkbox unselects it', () => { + const actions = spyOnActions(); + render(mockStandardFacet()); + + const teaCheckbox: HTMLInputElement = screen.getByLabelText( + getOptionLabelTextWithCount(standardFacet.options[1])); + expect(teaCheckbox.checked).toBeTruthy(); + + userEvent.click(teaCheckbox); + expectFacetOptionSet(actions, standardFacet.fieldId, standardFacet.options[1], false); + }); +}); + +function expectFacetOptionSet( + actions: SearchActions, + fieldId: string, + option: FacetOption, + selected: boolean +) { + expect(actions.setFacetOption).toHaveBeenCalledWith( + fieldId, + { matcher: option.matcher, value: option.value }, + selected + ); +} \ No newline at end of file diff --git a/tests/components/StandardFacets.stories.tsx b/tests/components/StandardFacets.stories.tsx index 976d0593c..832a4ee99 100644 --- a/tests/components/StandardFacets.stories.tsx +++ b/tests/components/StandardFacets.stories.tsx @@ -42,5 +42,5 @@ ShowMoreLimitClicked.args = { }; ShowMoreLimitClicked.play = ({ canvasElement }) => { const canvas = within(canvasElement); - userEvent.click(canvas.getByText('Show More')); + userEvent.click(canvas.queryAllByText('Show More')[0]); }; diff --git a/tests/components/StandardFacets.test.tsx b/tests/components/StandardFacets.test.tsx index 520dd9dcb..7917f03d9 100644 --- a/tests/components/StandardFacets.test.tsx +++ b/tests/components/StandardFacets.test.tsx @@ -1,9 +1,10 @@ import { render, screen } from '@testing-library/react'; -import { DisplayableFacetOption, FacetOption, Source, State, SearchActions } from '@yext/search-headless-react'; +import { Source, State } from '@yext/search-headless-react'; import { mockAnswersHooks, spyOnActions } from '../__utils__/mocks'; import userEvent from '@testing-library/user-event'; import { DisplayableFacets } from '../__fixtures__/data/filters'; import { StandardFacets } from '../../src/components'; +import { expectFacetOptionSet, getOptionLabelTextWithCount } from '../__utils__/facets'; const mockedState: Partial = { filters: { @@ -52,7 +53,7 @@ describe('StandardFacets', () => { expect(screen.getByText(regularFilter.displayName)).toBeDefined(); regularFilter.options.forEach(o => { - expect(screen.getByText(getOptionLabelText(o))).toBeDefined(); + expect(screen.getByText(getOptionLabelTextWithCount(o))).toBeDefined(); }); expect(screen.queryByText(numericalFilter.displayName)).toBeNull(); @@ -67,7 +68,7 @@ describe('StandardFacets', () => { const productFacet = DisplayableFacets[0]; const coffeeCheckbox: HTMLInputElement = screen.getByLabelText( - getOptionLabelText(productFacet.options[0]) + getOptionLabelTextWithCount(productFacet.options[0]) ); expect(coffeeCheckbox.checked).toBeFalsy(); @@ -81,7 +82,7 @@ describe('StandardFacets', () => { const facets = DisplayableFacets[0]; const coffeeLabel = screen.queryByLabelText(facets.options[0].displayName); const coffeeLabelAndCount = screen.queryByLabelText( - getOptionLabelText(facets.options[0]) + getOptionLabelTextWithCount(facets.options[0]) ); expect(coffeeLabel).toBeDefined(); @@ -94,7 +95,7 @@ describe('StandardFacets', () => { const productFacet = DisplayableFacets[0]; const coffeeFacetOption = productFacet.options[0]; - const labelText = getOptionLabelText(coffeeFacetOption); + const labelText = getOptionLabelTextWithCount(coffeeFacetOption); const coffeeCheckbox: HTMLInputElement = screen.getByLabelText(labelText); expect(coffeeCheckbox.checked).toBeFalsy(); @@ -108,7 +109,8 @@ describe('StandardFacets', () => { render(); const productFacet = DisplayableFacets[0]; - const teaCheckbox: HTMLInputElement = screen.getByLabelText(getOptionLabelText(productFacet.options[1])); + const teaCheckbox: HTMLInputElement = screen.getByLabelText( + getOptionLabelTextWithCount(productFacet.options[1])); expect(teaCheckbox.checked).toBeTruthy(); userEvent.click(teaCheckbox); @@ -121,7 +123,7 @@ describe('StandardFacets', () => { const productFacet = DisplayableFacets[0]; const coffeeCheckbox: HTMLInputElement = screen.getByLabelText( - getOptionLabelText(productFacet.options[0]) + getOptionLabelTextWithCount(productFacet.options[0]) ); expect(coffeeCheckbox.checked).toBeFalsy(); @@ -136,7 +138,7 @@ describe('StandardFacets', () => { const productFacet = DisplayableFacets[0]; const coffeeCheckbox: HTMLInputElement = screen.getByLabelText( - getOptionLabelText(productFacet.options[0]) + getOptionLabelTextWithCount(productFacet.options[0]) ); expect(coffeeCheckbox.checked).toBeFalsy(); @@ -145,20 +147,3 @@ describe('StandardFacets', () => { expect(actions.executeVerticalQuery).not.toBeCalled(); }); }); - -function expectFacetOptionSet( - actions: SearchActions, - fieldId: string, - option: FacetOption, - selected: boolean -) { - expect(actions.setFacetOption).toHaveBeenCalledWith( - fieldId, - { matcher: option.matcher, value: option.value }, - selected - ); -} - -function getOptionLabelText(option: DisplayableFacetOption) { - return `${option.displayName} (${option.count})`; -} diff --git a/tests/scripts/combined-coverage.sh b/tests/scripts/combined-coverage.sh index 58bdfa34d..e35e3b236 100755 --- a/tests/scripts/combined-coverage.sh +++ b/tests/scripts/combined-coverage.sh @@ -1,7 +1,7 @@ #!/bin/bash -npm run test:unit -npm run test:visual +npm run test:unit || exit 1 +npm run test:visual || exit 1 # merge mkdir -p coverage/merge