diff --git a/src/components/AutocompleteSearch/AutocompleteSearch.tsx b/src/components/AutocompleteSearch/AutocompleteSearch.tsx index 1cbbe734cb..61bb9e5569 100644 --- a/src/components/AutocompleteSearch/AutocompleteSearch.tsx +++ b/src/components/AutocompleteSearch/AutocompleteSearch.tsx @@ -27,7 +27,10 @@ import { InstantSearch, useInstantSearch, useSearchBox } from 'react-instantsear import { ClearableAutoComplete } from '~/components/ClearableAutoComplete/ClearableAutoComplete'; import { slugit } from '~/utils/string-helpers'; import { autocompleteSearchClient } from '~/components/Search/autocomplete.client'; -import { useCarriedSearchText } from '~/components/Search/useCarriedSearchText'; +import { + shouldRefineSearchQuery, + useCarriedSearchText, +} from '~/components/Search/useCarriedSearchText'; import { quoteMeiliValue } from '~/components/Search/meili-filter'; import { useAutocompleteAvailabilityStore } from '~/components/Search/search-availability.store'; import { ModelSearchItem } from '~/components/AutocompleteSearch/renderItems/models'; @@ -96,6 +99,18 @@ export const AutocompleteSearch = forwardRef<{ focus: () => void }, Props>(({ .. // causes. const carriedSearchText = useRef(''); + // Follow the section the user navigates to. This has to live ABOVE the keyed provider for the + // same reason the carrier does: inside it, the effect would run again on the mount that a + // target switch causes, read a section the user has not navigated to, and immediately revert + // their pick — so the category selector would only ever "work" when it picked what the URL + // already said. + const pathname = usePathname(); + const currentSection = pathname.split('/')[1] || 'models'; + const searchTarget = targetData.find((t) => t.value === currentSection)?.value ?? 'models'; + useEffect(() => { + setTargetIndex(searchTarget); + }, [searchTarget]); + const isModels = targetIndex === 'models'; const isImages = targetIndex === 'images'; const supportsPoi = ['models', 'images'].includes(targetIndex); @@ -174,9 +189,6 @@ function AutocompleteSearchContentInner( const isMobile = useIsMobile(); const features = useFeatureFlags(); const inputRef = useRef(null); - const pathname = usePathname(); - const currentSection = pathname.split('/')[1] || 'models'; - const searchTarget = targetData.find((t) => t.value === currentSection)?.value ?? 'models'; const domainColor = useDomainColor(); const { status } = useInstantSearch({ @@ -410,7 +422,8 @@ function AutocompleteSearchContentInner( useEffect(() => { // Only set the query when the debounced search changes // and user didn't select from the list - if (debouncedSearch === query || selectedItem || searchErrorState) return; + if (!shouldRefineSearchQuery(debouncedSearch, query, !!selectedItem || searchErrorState)) + return; // Check if the query is an AIR const air = checkAIR(indexName, debouncedSearch); @@ -424,22 +437,18 @@ function AutocompleteSearchContentInner( setQuery(cleanedSearch); setQueryFilters(filters); + // `searchErrorState` is a module-level store, so it is the one input here that SURVIVES the + // remount an index switch causes. Without it in the deps, a tree that remounted while search + // was unavailable restores the typed text, returns early, and then never refines when the + // flag clears — the box reads as populated while the fresh helper's query is still empty. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearch, query, indexName]); + }, [debouncedSearch, query, indexName, searchErrorState]); // Clear selected item after search changes useEffect(() => { setSelectedItem(null); }, [debouncedSearch]); - // Change index target when search target changes - useEffect(() => { - if (indexNameProp !== searchTarget) { - onTargetChange(searchTarget as TKey); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchTarget]); - const processHitUrl = (hit: Hit) => { switch (indexName) { case 'articles': @@ -468,7 +477,10 @@ function AutocompleteSearchContentInner( />