diff --git a/bundlesize.config.json b/bundlesize.config.json index 54ff5d0af8..c75e673797 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -14,7 +14,7 @@ }, { "path": "./packages/instantsearch.js/dist/instantsearch.development.js", - "maxSize": "189 kB" + "maxSize": "189.25 kB" }, { "path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js", diff --git a/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts b/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts index b2024c0761..322bae269f 100644 --- a/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts +++ b/packages/instantsearch-ui-components/src/components/autocomplete/createAutocompletePropGetters.ts @@ -8,9 +8,9 @@ export type AutocompleteIndexConfig = { getURL?: (item: TItem) => string; onSelect?: (params: { item: TItem; - getQuery: () => string; - getURL: () => string; + query: string; setQuery: (query: string) => void; + url?: string; }) => void; }; @@ -48,6 +48,7 @@ type UsePropGetters = (params: { }>; indicesConfig: Array>; onRefine: (query: string) => void; + onSelect: NonNullable['onSelect']>; }) => { getInputProps: GetInputProps; getItemProps: GetItemProps; @@ -66,6 +67,7 @@ export function createAutocompletePropGetters({ indices, indicesConfig, onRefine, + onSelect: globalOnSelect, }: Parameters>[0]): ReturnType> { const getElementId = createGetElementId(useId()); const rootRef = useRef(null); @@ -117,12 +119,13 @@ export function createAutocompletePropGetters({ if (actualActiveDescendant && items.has(actualActiveDescendant)) { const { item, - config: { onSelect, getQuery, getURL }, + config: { onSelect: indexOnSelect, getQuery, getURL }, } = items.get(actualActiveDescendant)!; - onSelect?.({ + const actualOnSelect = indexOnSelect ?? globalOnSelect; + actualOnSelect({ item, - getQuery: () => getQuery?.(item) ?? '', - getURL: () => getURL?.(item) ?? '', + query: getQuery?.(item) ?? '', + url: getURL?.(item), setQuery: (query) => onRefine(query), }); setActiveDescendant(undefined); diff --git a/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx b/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx index b0e0e60017..9db596e096 100644 --- a/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx +++ b/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx @@ -38,6 +38,7 @@ import type { import type { PreparedTemplateProps } from '../../lib/templating'; import type { BaseHit, + IndexUiState, IndexWidget, Renderer, RendererOptions, @@ -87,7 +88,6 @@ type RendererParams = { instanceId: number; containerNode: HTMLElement; indicesConfig: Array>; - cssClasses: AutocompleteCSSClasses; renderState: { indexTemplateProps: Array< PreparedTemplateProps['templates']>> @@ -95,8 +95,8 @@ type RendererParams = { isolatedIndex: IndexWidget | undefined; targetIndex: IndexWidget | undefined; }; - templates: AutocompleteTemplates; -}; +} & Pick, 'getSearchPageURL' | 'onSelect'> & + Required, 'cssClasses' | 'templates'>>; const createRenderer = ( params: RendererParams @@ -133,7 +133,12 @@ const createRenderer = ( type AutocompleteWrapperProps = Pick< RendererParams, - 'indicesConfig' | 'cssClasses' | 'templates' | 'renderState' + | 'indicesConfig' + | 'getSearchPageURL' + | 'onSelect' + | 'cssClasses' + | 'templates' + | 'renderState' > & Pick & RendererOptions>>; @@ -141,12 +146,20 @@ type AutocompleteWrapperProps = Pick< function AutocompleteWrapper({ indicesConfig, indices, + getSearchPageURL, + onSelect: userOnSelect, refine, cssClasses, renderState, instantSearchInstance, }: AutocompleteWrapperProps) { const { isolatedIndex, targetIndex } = renderState; + const isSearchPage = + targetIndex + ?.getWidgets() + .some(({ $$type }) => + ['ais.hits', 'ais.infiniteHits'].includes($$type) + ) ?? false; const { getInputProps, getItemProps, getPanelProps, getRootProps } = usePropGetters({ indices, @@ -161,6 +174,23 @@ function AutocompleteWrapper({ [isolatedIndex!.getIndexId()]: { query }, })); }, + onSelect: + userOnSelect ?? + (({ query, setQuery, url }) => { + if (url) { + window.location.href = url; + return; + } + + if (!isSearchPage && typeof getSearchPageURL !== 'undefined') { + const indexUiState = + instantSearchInstance.getUiState()[targetIndex!.getIndexId()]; + window.location.href = getSearchPageURL({ ...indexUiState, query }); + return; + } + + setQuery(query); + }), }); const query = @@ -263,10 +293,14 @@ type AutocompleteWidgetParams = { showSuggestions?: Partial< Pick< IndexConfig<{ query: string }>, - 'indexName' | 'templates' | 'cssClasses' + 'indexName' | 'getURL' | 'templates' | 'cssClasses' > >; + getSearchPageURL?: (nextUiState: IndexUiState) => string; + + onSelect?: AutocompleteIndexConfig['onSelect']; + /** * Templates to use for the widget. */ @@ -292,6 +326,8 @@ export function EXPERIMENTAL_autocomplete( escapeHTML, indices = [], showSuggestions, + getSearchPageURL, + onSelect, templates = {}, cssClasses: userCssClasses = {}, } = widgetParams || {}; @@ -328,9 +364,7 @@ export function EXPERIMENTAL_autocomplete( ), }, getQuery: (item) => item.query, - onSelect({ getQuery, setQuery }) { - setQuery(getQuery()); - }, + getURL: showSuggestions.getURL as unknown as IndexConfig['getURL'], }); } @@ -339,6 +373,8 @@ export function EXPERIMENTAL_autocomplete( instanceId, containerNode, indicesConfig, + getSearchPageURL, + onSelect, cssClasses, renderState: { indexTemplateProps: [], diff --git a/packages/react-instantsearch/src/widgets/Autocomplete.tsx b/packages/react-instantsearch/src/widgets/Autocomplete.tsx index 724ce20786..1570571b68 100644 --- a/packages/react-instantsearch/src/widgets/Autocomplete.tsx +++ b/packages/react-instantsearch/src/widgets/Autocomplete.tsx @@ -15,7 +15,12 @@ import React, { useRef, useState, } from 'react'; -import { Index, useAutocomplete, useSearchBox } from 'react-instantsearch-core'; +import { + Index, + useAutocomplete, + useInstantSearch, + useSearchBox, +} from 'react-instantsearch-core'; import { AutocompleteSearch } from '../components/AutocompleteSearch'; @@ -25,7 +30,7 @@ import type { Pragma, AutocompleteClassNames, } from 'instantsearch-ui-components'; -import type { BaseHit, Hit } from 'instantsearch.js'; +import type { BaseHit, Hit, IndexUiState } from 'instantsearch.js'; import type { ComponentProps } from 'react'; const Autocomplete = createAutocompleteComponent({ @@ -71,9 +76,11 @@ export type AutocompleteProps = ComponentProps<'div'> & { showSuggestions?: Partial< Pick< IndexConfig<{ query: string }>, - 'indexName' | 'itemComponent' | 'classNames' + 'indexName' | 'getURL' | 'itemComponent' | 'classNames' > >; + getSearchPageURL?: (nextUiState: IndexUiState) => string; + onSelect?: AutocompleteIndexConfig['onSelect']; classNames?: Partial; }; @@ -83,6 +90,8 @@ type InnerAutocompleteProps = Omit< > & { indicesConfig: Array>; refineSearchBox: ReturnType['refine']; + indexUiState: IndexUiState; + isSearchPage: boolean; }; export function EXPERIMENTAL_Autocomplete({ @@ -90,6 +99,7 @@ export function EXPERIMENTAL_Autocomplete({ showSuggestions, ...props }: AutocompleteProps) { + const { indexUiState, indexRenderState } = useInstantSearch(); const { refine } = useSearchBox( {}, { $$type: 'ais.autocomplete', $$widgetType: 'ais.autocomplete' } @@ -116,12 +126,17 @@ export function EXPERIMENTAL_Autocomplete({ ), }, getQuery: (item) => item.query, - onSelect: ({ getQuery, setQuery }) => { - setQuery(getQuery()); - }, + getURL: showSuggestions.getURL as unknown as IndexConfig['getURL'], }); } + const isSearchPage = useMemo( + () => + typeof indexRenderState.hits !== 'undefined' || + typeof indexRenderState.infiniteHits !== 'undefined', + [indexRenderState] + ); + return ( @@ -132,6 +147,8 @@ export function EXPERIMENTAL_Autocomplete({ {...props} indicesConfig={indicesConfig} refineSearchBox={refine} + indexUiState={indexUiState} + isSearchPage={isSearchPage} /> @@ -141,6 +158,10 @@ export function EXPERIMENTAL_Autocomplete({ function InnerAutocomplete({ indicesConfig, refineSearchBox, + getSearchPageURL, + onSelect: userOnSelect, + indexUiState, + isSearchPage, ...props }: InnerAutocompleteProps) { const { indices, refine: refineAutocomplete } = useAutocomplete(); @@ -148,10 +169,25 @@ function InnerAutocomplete({ usePropGetters({ indices, indicesConfig, - onRefine: (query: string) => { + onRefine: (query) => { refineAutocomplete(query); refineSearchBox(query); }, + onSelect: + userOnSelect ?? + (({ query, setQuery, url }) => { + if (url) { + window.location.href = url; + return; + } + + if (!isSearchPage && typeof getSearchPageURL !== 'undefined') { + window.location.href = getSearchPageURL({ ...indexUiState, query }); + return; + } + + setQuery(query); + }), }); return ( diff --git a/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx b/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx index 3089ea6559..665b8bbffe 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/__utils__/all-widgets.tsx @@ -114,6 +114,10 @@ function Widget({ case 'LookingSimilar': { return ; } + case 'EXPERIMENTAL_Autocomplete': { + // @ts-expect-error - incorrectly expects onSelect from ComponentProps<'div'> + return ; + } default: { return ; }