@yext/search-ui-react
Version:
A library of React Components for powering Yext Search integrations
1 lines • 401 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/SearchBar.tsx","../src/hooks/useEntityPreviews.tsx","../src/hooks/useComponentMountStatus.tsx","../src/hooks/useDebouncedFunction.ts","../src/hooks/useRecentSearches.ts","../src/hooks/useSearchWithNearMeHandling.ts","../src/utils/search-operations.ts","../src/utils/location-operations.ts","../src/hooks/useSynchronizedRequest.tsx","../src/icons/VerticalDividerIcon.tsx","../src/icons/HistoryIcon.tsx","../src/icons/CloseIcon.tsx","../src/icons/MagnifyingGlassIcon.tsx","../src/components/Dropdown/Dropdown.tsx","../src/components/Dropdown/DropdownContext.ts","../src/components/Dropdown/InputContext.ts","../src/components/Dropdown/FocusContext.ts","../src/components/ScreenReader.tsx","../src/components/utils/recursivelyMapChildren.ts","../src/components/Dropdown/DropdownItem.tsx","../src/components/Dropdown/generateDropdownId.ts","../src/hooks/useLayoutEffect.ts","../src/hooks/useId.ts","../src/components/Dropdown/DropdownInput.tsx","../src/components/Dropdown/DropdownMenu.tsx","../src/hooks/useComposedCssClasses.tsx","../src/components/SearchButton.tsx","../src/components/utils/processTranslation.ts","../src/components/utils/renderHighlightedValue.tsx","../src/components/utils/renderAutocompleteResult.tsx","../src/hooks/useAnalytics.ts","../src/hooks/useSearchBarAnalytics.ts","../src/models/verticalLink.ts","../src/utils/filterutils.tsx","../src/models/NumberRangeFilter.ts","../src/components/SpellCheck.tsx","../src/hooks/useCardAnalytics.ts","../src/hooks/useCardAnalyticsCallback.tsx","../src/hooks/useCardFeedbackCallback.tsx","../src/components/DirectAnswer.tsx","../src/components/ThumbsFeedback.tsx","../src/icons/ThumbIcon.tsx","../src/components/FieldValueDirectAnswer.tsx","../src/components/FeaturedSnippetDirectAnswer.tsx","../src/components/FilterSearch.tsx","../src/icons/LoadingIndicator.tsx","../src/icons/YextIcon.tsx","../src/hooks/useGeolocationHandler.ts","../src/components/Geolocation.tsx","../src/icons/CurrentLocationIcon.tsx","../src/components/LocationBias.tsx","../src/components/AppliedFilters.tsx","../src/hooks/useClearFiltersCallback.ts","../src/components/AppliedFiltersDisplay.tsx","../src/components/Filters/HierarchicalFacetDisplay.tsx","../src/hooks/useHierarchicalFacetTree.ts","../src/components/Filters/FiltersContext.ts","../src/hooks/useNlpFilterDisplayNames.ts","../src/hooks/useRemovableFilters.ts","../src/utils/isDescendantHierarchicalFacet.tsx","../src/hooks/useStateUpdatedOnSearch.ts","../src/hooks/useRemovableStaticFilters.ts","../src/components/UniversalResults.tsx","../src/components/VerticalResultsDisplay.tsx","../src/components/cards/standard/StandardCardDisplay.tsx","../src/components/cards/standard/StandardCard.tsx","../src/components/sections/StandardSection.tsx","../src/icons/CollectionIcon.tsx","../src/components/sections/SectionHeader.tsx","../src/components/VerticalResults.tsx","../src/components/Pagination.tsx","../src/icons/ChevronIcon.tsx","../src/hooks/usePaginationAnalytics.ts","../src/icons/StarIcon.tsx","../src/components/AlternativeVerticals.tsx","../src/components/ResultsCount.tsx","../src/components/Filters/CheckboxOption.tsx","../src/components/Filters/FilterGroupContext.ts","../src/components/Filters/CollapsibleLabel.tsx","../src/components/Filters/CollapsibleSection.tsx","../src/components/Filters/FacetsProvider.tsx","../src/components/Filters/FilterGroupProvider.tsx","../src/components/Filters/SearchInput.tsx","../src/components/Filters/StaticFiltersProvider.tsx","../src/components/Filters/RangeInput.tsx","../src/icons/InvalidIcon.tsx","../src/components/FilterGroup.tsx","../src/components/FacetTiltle.tsx","../src/components/StaticFilters.tsx","../src/components/StandardFacets.tsx","../src/components/FilterDivider.tsx","../src/components/HierarchicalFacets.tsx","../src/components/NumericalFacets.tsx","../src/components/StandardFacetContent.tsx","../src/components/Facets.tsx","../src/components/NumericalFacetContent.tsx","../src/components/HierarchicalFacetContent.tsx","../src/components/ApplyFiltersButton.tsx","../src/components/MapboxMap.tsx","../src/components/AnalyticsProvider.tsx","../src/components/GenerativeDirectAnswer.tsx","../src/components/Markdown.tsx","../src/models/StandardCardData.ts","../src/index.ts"],"sourcesContent":["import {\n SearchHeadless,\n QuerySource,\n SearchTypeEnum,\n UniversalLimit,\n useSearchActions,\n useSearchState,\n useSearchUtilities,\n VerticalResults as VerticalResultsData\n} from '@yext/search-headless-react';\nimport classNames from 'classnames';\nimport React, { Fragment, isValidElement, PropsWithChildren, ReactNode, useCallback, useEffect, useMemo } from 'react';\nimport { useEntityPreviews } from '../hooks/useEntityPreviews';\nimport { useRecentSearches } from '../hooks/useRecentSearches';\nimport { useSearchWithNearMeHandling } from '../hooks/useSearchWithNearMeHandling';\nimport { useSynchronizedRequest } from '../hooks/useSynchronizedRequest';\nimport { useDebouncedFunction } from '../hooks/useDebouncedFunction';\nimport { VerticalDividerIcon } from '../icons/VerticalDividerIcon';\nimport { HistoryIcon as RecentSearchIcon } from '../icons/HistoryIcon';\nimport { CloseIcon } from '../icons/CloseIcon';\nimport { MagnifyingGlassIcon } from '../icons/MagnifyingGlassIcon';\nimport { Dropdown } from './Dropdown/Dropdown';\nimport { useDropdownContext } from './Dropdown/DropdownContext';\nimport { DropdownInput } from './Dropdown/DropdownInput';\nimport { DropdownItem } from './Dropdown/DropdownItem';\nimport { DropdownMenu } from './Dropdown/DropdownMenu';\nimport { FocusedItemData } from './Dropdown/FocusContext';\nimport { useComposedCssClasses, twMerge } from '../hooks/useComposedCssClasses';\nimport { SearchButton } from './SearchButton';\nimport { processTranslation } from './utils/processTranslation';\nimport {\n renderAutocompleteResult,\n AutocompleteResultCssClasses,\n builtInCssClasses as AutocompleteResultBuiltInCssClasses\n} from './utils/renderAutocompleteResult';\nimport { useSearchBarAnalytics } from '../hooks/useSearchBarAnalytics';\nimport { isVerticalLink, VerticalLink } from '../models/verticalLink';\nimport { executeAutocomplete as executeAutocompleteSearch } from '../utils/search-operations';\nimport { clearStaticRangeFilters } from '../utils/filterutils';\nimport { recursivelyMapChildren } from './utils/recursivelyMapChildren';\n\nconst builtInCssClasses: Readonly<SearchBarCssClasses> = {\n searchBarContainer: 'h-12 mb-6',\n inputDivider: 'border-t border-gray-200 mx-2.5',\n inputElement: 'outline-none flex-grow border-none h-11 pl-5 pr-2 text-neutral-dark text-base placeholder:text-neutral-light',\n searchButtonContainer: ' w-8 h-full mx-2 flex flex-col justify-center items-center',\n searchButton: 'h-7 w-7',\n focusedOption: 'bg-gray-100',\n clearButton: 'h-3 w-3 mr-3.5',\n verticalDivider: 'mr-0.5',\n recentSearchesIcon: 'w-5 mr-1 flex-shrink-0 h-full text-gray-400',\n recentSearchesOption: 'whitespace-no-wrap max-w-full px-3 text-neutral-dark truncate',\n recentSearchesNonHighlighted: 'font-normal', // Swap this to semibold once we apply highlighting to recent searches\n verticalLink: 'ml-12 pl-1 text-neutral italic',\n entityPreviewsDivider: 'h-px bg-gray-200 mt-1 mb-4 mx-3.5',\n ...AutocompleteResultBuiltInCssClasses\n};\n\n/**\n * The CSS class interface for the {@link SearchBar}.\n *\n * @public\n */\nexport interface SearchBarCssClasses extends AutocompleteResultCssClasses {\n searchBarContainer?: string,\n inputElement?: string,\n inputDivider?: string,\n clearButton?: string,\n searchButton?: string,\n searchButtonContainer?: string,\n focusedOption?: string,\n recentSearchesIcon?: string,\n recentSearchesOption?: string,\n recentSearchesNonHighlighted?: string,\n verticalLink?: string,\n verticalDivider?: string,\n entityPreviewsDivider?: string\n}\n\n/**\n * The type of a functional React component which renders entity previews using\n * a map of vertical key to the corresponding VerticalResults data.\n *\n * @remarks\n * The autocomplete loading state is passed in as an optional param.\n *\n * Default props for rendering corresponding DropdownItems are passed in:\n * an onClick function to allow an entity preview to be submitted, and\n * an ariaLabel function that returns text for the screenreader\n *\n * For the entity previews to be navigable in the search bar's dropdown section,\n * wrap each entity preview in a {@link DropdownItem} component.\n *\n * @public\n */\nexport type RenderEntityPreviews = (\n autocompleteLoading: boolean,\n verticalKeyToResults: Record<string, VerticalResultsData>,\n dropdownItemProps: {\n onClick: (value: string, _index: number, itemData?: FocusedItemData) => void,\n ariaLabel: (value: string) => string\n }\n) => JSX.Element | null;\n\n/**\n * The configuration options for Visual Autocomplete.\n *\n * @public\n */\nexport interface VisualAutocompleteConfig {\n /** The Search Headless instance used to perform visual autocomplete searches. */\n entityPreviewSearcher: SearchHeadless,\n /** Renders entity previews based on the autocomplete loading state and results. */\n renderEntityPreviews: RenderEntityPreviews,\n /** Specify which verticals to include for VisualAutocomplete. */\n includedVerticals: string[],\n /** Specify the number of entities to return per vertical. **/\n universalLimit?: UniversalLimit,\n /** The debouncing time, in milliseconds, for making API requests for entity previews. */\n entityPreviewsDebouncingTime?: number\n}\n\n/**\n * The interface of a function which is called on a search.\n *\n * @public\n */\nexport type onSearchFunc = (searchEventData: { verticalKey?: string, query?: string }) => void;\n\n/**\n * The props for the {@link SearchBar} component.\n *\n * @public\n */\nexport interface SearchBarProps {\n /** The search bar's placeholder text. */\n placeholder?: string,\n /** {@inheritDoc LocationBiasProps.geolocationOptions} */\n geolocationOptions?: PositionOptions,\n /** CSS classes for customizing the component styling. */\n customCssClasses?: SearchBarCssClasses,\n /** {@inheritDoc VisualAutocompleteConfig} */\n visualAutocompleteConfig?: VisualAutocompleteConfig,\n /** Shows vertical links if true, set to false on default. */\n showVerticalLinks?: boolean,\n /** A function which is called when a vertical link is selected. */\n onSelectVerticalLink?: (data: { verticalLink: VerticalLink, querySource: QuerySource }) => void,\n /** A function which returns a display label for the given verticalKey. */\n verticalKeyToLabel?: (verticalKey: string) => string,\n /** Hides recent searches if true. */\n hideRecentSearches?: boolean,\n /** Limits the number of recent searches shown. */\n recentSearchesLimit?: number,\n /** A callback which is called when a search is ran. */\n onSearch?: onSearchFunc\n}\n\n/**\n * Renders a SearchBar that is hooked up with an InputDropdown component.\n *\n * @public\n */\nexport function SearchBar({\n placeholder,\n geolocationOptions,\n hideRecentSearches,\n visualAutocompleteConfig,\n showVerticalLinks = false,\n onSelectVerticalLink,\n verticalKeyToLabel,\n recentSearchesLimit = 5,\n customCssClasses,\n onSearch\n}: SearchBarProps): JSX.Element {\n const {\n entityPreviewSearcher,\n renderEntityPreviews,\n includedVerticals,\n universalLimit,\n entityPreviewsDebouncingTime = 500\n } = visualAutocompleteConfig ?? {};\n const searchActions = useSearchActions();\n const searchUtilities = useSearchUtilities();\n const reportAnalyticsEvent = useSearchBarAnalytics();\n\n const query = useSearchState(state => state.query.input) ?? '';\n const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);\n const isVertical = useSearchState(state => state.meta.searchType) === SearchTypeEnum.Vertical;\n const verticalKey = useSearchState(state => state.vertical.verticalKey);\n const debouncedExecuteAutocompleteSearch = useDebouncedFunction( () => executeAutocompleteSearch(searchActions), 200);\n const [autocompleteResponse, executeAutocomplete, clearAutocompleteData] = useSynchronizedRequest(\n async () => {\n return debouncedExecuteAutocompleteSearch ?\n debouncedExecuteAutocompleteSearch() :\n undefined;\n }\n );\n const [\n executeQueryWithNearMeHandling,\n autocompletePromiseRef,\n ] = useSearchWithNearMeHandling(geolocationOptions, onSearch);\n const [\n recentSearches,\n setRecentSearch,\n clearRecentSearches,\n ] = useRecentSearches(recentSearchesLimit, verticalKey);\n const filteredRecentSearches = recentSearches?.filter(search =>\n searchUtilities.isCloseMatch(search.query, query)\n );\n\n useEffect(() => {\n if (hideRecentSearches) {\n clearRecentSearches();\n }\n }, [clearRecentSearches, hideRecentSearches]);\n\n const clearAutocomplete = useCallback(() => {\n clearAutocompleteData();\n autocompletePromiseRef.current = undefined;\n }, [autocompletePromiseRef, clearAutocompleteData]);\n\n const executeQuery = useCallback(() => {\n if (!hideRecentSearches) {\n const input = searchActions.state.query.input;\n input && setRecentSearch(input);\n }\n executeQueryWithNearMeHandling();\n }, [\n searchActions.state.query.input,\n executeQueryWithNearMeHandling,\n hideRecentSearches,\n setRecentSearch\n ]);\n\n const handleSubmit = useCallback((value?: string, index?: number, itemData?: FocusedItemData) => {\n value !== undefined && searchActions.setQuery(value);\n searchActions.setOffset(0);\n searchActions.setFacets([]);\n clearStaticRangeFilters(searchActions);\n if (itemData && isVerticalLink(itemData.verticalLink) && onSelectVerticalLink) {\n onSelectVerticalLink({ verticalLink: itemData.verticalLink, querySource: QuerySource.Autocomplete });\n } else {\n executeQuery();\n }\n if (typeof index === 'number' && index >= 0 && !itemData?.isEntityPreview) {\n reportAnalyticsEvent('AUTO_COMPLETE_SELECTION', value);\n }\n }, [searchActions, executeQuery, onSelectVerticalLink, reportAnalyticsEvent]);\n\n const [\n entityPreviewsState,\n executeEntityPreviewsQuery\n ] = useEntityPreviews(entityPreviewSearcher, entityPreviewsDebouncingTime);\n const { verticalKeyToResults, isLoading: entityPreviewsLoading } = entityPreviewsState;\n const entityPreviews = renderEntityPreviews?.(\n entityPreviewsLoading,\n verticalKeyToResults,\n { onClick: handleSubmit, ariaLabel: getAriaLabel }\n );\n const updateEntityPreviews = useCallback((query: string) => {\n if (!renderEntityPreviews || !includedVerticals) {\n return;\n }\n executeEntityPreviewsQuery(query, universalLimit ?? {}, includedVerticals);\n }, [executeEntityPreviewsQuery, renderEntityPreviews, includedVerticals, universalLimit]);\n\n const handleInputFocus = useCallback((value = '') => {\n searchActions.setQuery(value);\n updateEntityPreviews(value);\n autocompletePromiseRef.current = executeAutocomplete();\n }, [searchActions, autocompletePromiseRef, executeAutocomplete, updateEntityPreviews]);\n\n const handleInputChange = useCallback((value = '') => {\n searchActions.setQuery(value);\n updateEntityPreviews(value);\n autocompletePromiseRef.current = executeAutocomplete();\n }, [searchActions, autocompletePromiseRef, executeAutocomplete, updateEntityPreviews]);\n\n const handleClickClearButton = useCallback(() => {\n updateEntityPreviews('');\n searchActions.setQuery('');\n reportAnalyticsEvent('SEARCH_CLEAR_BUTTON');\n }, [handleSubmit, reportAnalyticsEvent, updateEntityPreviews]);\n\n function renderInput() {\n return (\n <DropdownInput\n className={cssClasses.inputElement}\n placeholder={placeholder}\n onSubmit={handleSubmit}\n onFocus={handleInputFocus}\n onChange={handleInputChange}\n ariaLabel='Conduct a search'\n />\n );\n }\n\n function renderRecentSearches() {\n const recentSearchesCssClasses = {\n icon: cssClasses.recentSearchesIcon,\n option: cssClasses.recentSearchesOption,\n nonHighlighted: cssClasses.recentSearchesNonHighlighted\n };\n\n return filteredRecentSearches?.map((result, i) => (\n <DropdownItem\n className='flex items-center h-6.5 px-3.5 py-1.5 cursor-pointer hover:bg-gray-100'\n focusedClassName={twMerge('flex items-center h-6.5 px-3.5 py-1.5 cursor-pointer hover:bg-gray-100', cssClasses.focusedOption)}\n key={i}\n value={result.query}\n onClick={handleSubmit}\n >\n {renderAutocompleteResult(\n { value: result.query, inputIntents: [] },\n recentSearchesCssClasses,\n RecentSearchIcon,\n `recent search: ${result.query}`\n )}\n </DropdownItem>\n ));\n }\n\n const itemDataMatrix = useMemo(() => {\n return autocompleteResponse?.results.map(result => {\n return result.verticalKeys?.map(verticalKey => ({\n verticalLink: { verticalKey, query: result.value }\n })) ?? [];\n }) ?? [];\n }, [autocompleteResponse?.results]);\n\n function renderQuerySuggestions() {\n return autocompleteResponse?.results.map((result, i) => (\n <Fragment key={i}>\n <DropdownItem\n className='flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100'\n focusedClassName={twMerge('flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100', cssClasses.focusedOption)}\n value={result.value}\n onClick={handleSubmit}\n >\n {renderAutocompleteResult(\n result,\n cssClasses,\n MagnifyingGlassIcon,\n `autocomplete suggestion: ${result.value}`\n )}\n </DropdownItem>\n {showVerticalLinks && !isVertical && result.verticalKeys?.map((verticalKey, j) => (\n <DropdownItem\n key={j}\n className='flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100'\n focusedClassName={twMerge('flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100', cssClasses.focusedOption)}\n value={result.value}\n itemData={itemDataMatrix[i][j]}\n onClick={handleSubmit}\n >\n {renderAutocompleteResult(\n {\n value: `in ${verticalKeyToLabel ? verticalKeyToLabel(verticalKey) : verticalKey}`,\n inputIntents: []\n },\n { ...cssClasses, option: cssClasses.verticalLink }\n )}\n </DropdownItem>\n ))}\n </Fragment>\n ));\n }\n\n function renderClearButton() {\n return (\n <>\n <button\n aria-label='Clear the search bar'\n className={cssClasses.clearButton}\n onClick={handleClickClearButton}\n >\n <CloseIcon />\n </button>\n <VerticalDividerIcon className={cssClasses.verticalDivider} />\n </>\n );\n }\n\n const entityPreviewsCount = calculateEntityPreviewsCount(entityPreviews);\n const showEntityPreviewsDivider = entityPreviews\n && !!(autocompleteResponse?.results.length || filteredRecentSearches?.length);\n const hasItems = !!(autocompleteResponse?.results.length\n || filteredRecentSearches?.length || entityPreviews);\n const screenReaderText = getScreenReaderText(\n autocompleteResponse?.results.length,\n filteredRecentSearches?.length,\n entityPreviewsCount\n );\n const activeClassName = classNames('relative z-10 bg-white border rounded-3xl border-gray-200 w-full overflow-hidden', {\n ['shadow-lg']: hasItems\n });\n\n const handleToggleDropdown = useCallback((isActive) => {\n if (!isActive) {\n clearAutocomplete();\n }\n }, [clearAutocomplete]);\n\n return (\n <div className={cssClasses.searchBarContainer}>\n <Dropdown\n className='relative bg-white border rounded-3xl border-gray-200 w-full overflow-hidden'\n activeClassName={activeClassName}\n screenReaderText={screenReaderText}\n parentQuery={query}\n onToggle={handleToggleDropdown}\n >\n <div className='inline-flex items-center justify-between w-full'>\n {renderInput()}\n {query && renderClearButton()}\n <DropdownSearchButton\n handleSubmit={handleSubmit}\n cssClasses={cssClasses}\n />\n </div>\n {hasItems &&\n <StyledDropdownMenu cssClasses={cssClasses}>\n {renderRecentSearches()}\n {renderQuerySuggestions()}\n {showEntityPreviewsDivider && <div className={cssClasses.entityPreviewsDivider}></div>}\n {entityPreviews}\n </StyledDropdownMenu>\n }\n </Dropdown>\n </div>\n );\n}\n\nfunction StyledDropdownMenu({ cssClasses, children }: PropsWithChildren<{\n cssClasses: {\n inputDivider?: string\n }\n}>) {\n return (\n <DropdownMenu>\n <div className={cssClasses.inputDivider} />\n <div className='bg-white py-4'>\n {children}\n </div>\n </DropdownMenu>\n );\n}\n\nfunction getScreenReaderText(\n autocompleteOptions = 0,\n recentSearchesOptions = 0,\n entityPreviewsCount = 0\n): string {\n const recentSearchesText = recentSearchesOptions > 0\n ? processTranslation({\n phrase: `${recentSearchesOptions} recent search found.`,\n pluralForm: `${recentSearchesOptions} recent searches found.`,\n count: recentSearchesOptions\n })\n : '';\n const entityPreviewsText = entityPreviewsCount > 0\n ? ' ' + processTranslation({\n phrase: `${entityPreviewsCount} result preview found.`,\n pluralForm: `${entityPreviewsCount} result previews found.`,\n count: entityPreviewsCount\n })\n : '';\n const autocompleteText = autocompleteOptions > 0\n ? ' ' + processTranslation({\n phrase: `${autocompleteOptions} autocomplete suggestion found.`,\n pluralForm: `${autocompleteOptions} autocomplete suggestions found.`,\n count: autocompleteOptions\n })\n : '';\n\n const text = recentSearchesText + autocompleteText + entityPreviewsText;\n if (text === '') {\n return processTranslation({\n phrase: '0 autocomplete suggestion found.',\n pluralForm: '0 autocomplete suggestions found.',\n count: 0\n });\n }\n return text.trim();\n}\n\nfunction DropdownSearchButton({ handleSubmit, cssClasses }: {\n handleSubmit: () => void,\n cssClasses: {\n searchButtonContainer?: string,\n searchButton?: string\n }\n}) {\n const { toggleDropdown } = useDropdownContext();\n const handleClick = useCallback(() => {\n handleSubmit();\n toggleDropdown(false);\n }, [handleSubmit, toggleDropdown]);\n return (\n <div className={cssClasses.searchButtonContainer}>\n <SearchButton\n className={cssClasses.searchButton}\n handleClick={handleClick}\n />\n </div>\n );\n}\n\nfunction getAriaLabel(value: string): string {\n return 'result preview: ' + value;\n}\n\n/**\n * Calculates the number of navigable entity previews from a ReactNode containing DropdownItems.\n */\nexport function calculateEntityPreviewsCount(children: ReactNode): number {\n let count = 0;\n recursivelyMapChildren(children, c => {\n if (isValidElement(c) && c.type === DropdownItem) {\n count++;\n }\n return c;\n });\n return count;\n}","import { SearchHeadless, UniversalLimit, VerticalResults as VerticalResultsData } from '@yext/search-headless-react';\nimport { useState } from 'react';\nimport { useComponentMountStatus } from './useComponentMountStatus';\nimport { useDebouncedFunction } from './useDebouncedFunction';\n\ninterface EntityPreviewsState {\n verticalKeyToResults: Record<string, VerticalResultsData>,\n isLoading: boolean\n}\n\ntype ExecuteEntityPreviewsQuery = (\n query: string,\n universalLimit: UniversalLimit,\n restrictVerticals: string[]\n) => void;\n\n/**\n * useEntityPreviews provides state surrounding the visual entities portion of visual autocomplete,\n * which performs debounced universal searches.\n *\n * @param entityPreviewSearcher - the headless instance use as searcher for entity preview related queries\n * @param debounceTime - the time in milliseconds to debounce the universal search request\n */\nexport function useEntityPreviews(\n entityPreviewSearcher: SearchHeadless | undefined,\n debounceTime: number\n): [EntityPreviewsState, ExecuteEntityPreviewsQuery] {\n const isMountedRef = useComponentMountStatus();\n const [\n verticalKeyToResults,\n setVerticalKeyToResults\n ] = useState<Record<string, VerticalResultsData>>({});\n const debouncedUniversalSearch = useDebouncedFunction(async () => {\n if (!entityPreviewSearcher) {\n return;\n }\n await entityPreviewSearcher.executeUniversalQuery();\n /**\n * Avoid performing a React state update on an unmounted component\n * (e.g unmounted during async await)\n */\n if (!isMountedRef.current) {\n return;\n }\n const results = entityPreviewSearcher.state.universal.verticals || [];\n setVerticalKeyToResults(getVerticalKeyToResults(results));\n setLoadingState(false);\n }, debounceTime);\n const [isLoading, setLoadingState] = useState<boolean>(false);\n\n function executeEntityPreviewsQuery(\n query: string,\n universalLimit: UniversalLimit,\n restrictVerticals: string[]\n ) {\n if (!entityPreviewSearcher) {\n return;\n }\n if (query === entityPreviewSearcher.state.query.input) {\n return;\n }\n setLoadingState(true);\n entityPreviewSearcher.setQuery(query);\n entityPreviewSearcher.setRestrictVerticals(restrictVerticals);\n entityPreviewSearcher.setUniversalLimit(universalLimit);\n debouncedUniversalSearch?.();\n }\n return [{ verticalKeyToResults, isLoading }, executeEntityPreviewsQuery];\n}\n\n/**\n * @returns a mapping of vertical key to VerticalResults\n */\nfunction getVerticalKeyToResults(\n verticalResultsArray: VerticalResultsData[]\n): Record<string, VerticalResultsData> {\n return verticalResultsArray.reduce<Record<string, VerticalResultsData>>((prev, current) => {\n prev[current.verticalKey] = current;\n return prev;\n }, {});\n}","import { useEffect, useRef } from 'react';\n\n/**\n * This is use to prevent react errors due to performing state update on unmounted component.\n * This error does not need to be suppressed as it will be remove in the next version of React.\n *\n * React PR regarding the issue: https://github.com/facebook/react/pull/22114\n */\nexport function useComponentMountStatus(): React.MutableRefObject<boolean> {\n const isMountedRef = useRef<boolean>(false);\n useEffect(() => {\n isMountedRef.current = true;\n return () => { isMountedRef.current = false; };\n }, []);\n\n return isMountedRef;\n}","import { useRef } from 'react';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Func = (...args: any[]) => any;\ntype DebouncedFunction<F extends Func> = (...args: Parameters<F>) => Promise<undefined | ReturnType<F>>;\n\nexport function useDebouncedFunction<F extends Func>(\n func: F | undefined,\n milliseconds: number\n): DebouncedFunction<F> | undefined {\n const timeoutIdRef = useRef<number | undefined>();\n if (!func) {\n return undefined;\n }\n const debounced: DebouncedFunction<F> = (...args: Parameters<F>) => {\n return new Promise(resolve => {\n if (timeoutIdRef.current !== undefined) {\n clearTimeout(timeoutIdRef.current);\n }\n timeoutIdRef.current = window.setTimeout(() => {\n resolve(func(...args));\n timeoutIdRef.current = undefined;\n }, milliseconds);\n });\n };\n return debounced;\n}","import { useCallback, useEffect, useState } from 'react';\nimport { RecentSearches, ISearch } from 'recent-searches';\n\nexport function useRecentSearches(\n recentSearchesLimit: number,\n verticalKey: string | undefined\n): [ISearch[] | undefined, (input: string) => void, () => void] {\n const recentSearchesKey = getRecentSearchesKey(verticalKey);\n const [recentSearches, setRecentSeaches] = useState<RecentSearches>(\n new RecentSearches({\n limit: recentSearchesLimit,\n namespace: recentSearchesKey\n })\n );\n\n const clearRecentSearches = useCallback(() => {\n localStorage.removeItem(recentSearchesKey);\n setRecentSeaches(new RecentSearches({\n limit: recentSearchesLimit,\n namespace: recentSearchesKey\n }));\n localStorage.removeItem(recentSearchesKey);\n }, [recentSearchesKey, recentSearchesLimit]);\n\n const setRecentSearch = useCallback((input: string) => {\n recentSearches.setRecentSearch(input);\n }, [recentSearches]);\n\n useEffect(() => {\n setRecentSeaches(new RecentSearches({\n limit: recentSearchesLimit,\n namespace: recentSearchesKey\n }));\n }, [recentSearchesKey, recentSearchesLimit]);\n\n return [recentSearches?.getRecentSearches(), setRecentSearch, clearRecentSearches];\n}\n\nfunction getRecentSearchesKey(verticalKey: string | undefined): string {\n if (verticalKey) {\n return `__yxt_recent_searches_${verticalKey}__`;\n } else {\n return '__yxt_recent_searches_universal__';\n }\n}","import { useSearchActions, AutocompleteResponse, SearchIntent } from '@yext/search-headless-react';\nimport { executeSearch, executeAutocomplete } from '../utils/search-operations';\nimport { updateLocationIfNeeded } from '../utils/location-operations';\nimport { MutableRefObject, useRef } from 'react';\nimport { onSearchFunc } from '../components/SearchBar';\n\n/** The type of a function for executing a query and returning a promise. @public */\nexport type QueryFunc = () => Promise<void>;\n/**\n * A ref which contains a promise of the latest autocomplete response in order get the\n * latest search intents.\n */\nexport type AutocompleteRef = MutableRefObject<Promise<AutocompleteResponse | undefined> | undefined>;\n\n/**\n * Returns a search action that will handle near me searches, by first checking\n * for near me intents using an autocomplete request.\n *\n * @remarks\n * You can optionally use the provided ref to store autocomplete responses, to avoid\n * making unnecessary autocomplete requests.\n */\nexport function useSearchWithNearMeHandling(\n geolocationOptions?: PositionOptions,\n onSearch?: onSearchFunc\n): [QueryFunc, AutocompleteRef] {\n /**\n * Allow a query search to wait on the response to the autocomplete request right\n * before the search execution in order to retrieve the search intents.\n */\n const autocompletePromiseRef = useRef<Promise<AutocompleteResponse | undefined>>();\n const searchActions = useSearchActions();\n\n async function executeQuery() {\n try {\n let intents: SearchIntent[] = [];\n if (!searchActions.state.location.userLocation) {\n if (!autocompletePromiseRef.current) {\n autocompletePromiseRef.current = executeAutocomplete(searchActions);\n }\n const autocompleteResponseBeforeSearch = await autocompletePromiseRef.current;\n intents = autocompleteResponseBeforeSearch?.inputIntents || [];\n await updateLocationIfNeeded(searchActions, intents, geolocationOptions);\n }\n } catch (e) {\n console.error('Error executing autocomplete before search:', e);\n await updateLocationIfNeeded(searchActions, [], geolocationOptions);\n }\n const verticalKey = searchActions.state.vertical.verticalKey ?? '';\n const query = searchActions.state.query.input ?? '';\n onSearch\n ? onSearch({ verticalKey, query })\n : executeSearch(searchActions);\n }\n return [executeQuery, autocompletePromiseRef];\n}","import {\n SearchActions,\n AutocompleteResponse,\n GenerativeDirectAnswerResponse,\n SearchIntent,\n SearchTypeEnum\n} from '@yext/search-headless-react';\n\n/**\n * Executes a universal/vertical search.\n *\n * @public\n */\nexport async function executeSearch(searchActions: SearchActions): Promise<void> {\n const isVertical = searchActions.state.meta.searchType === SearchTypeEnum.Vertical;\n try {\n isVertical\n ? searchActions.executeVerticalQuery()\n : searchActions.executeUniversalQuery();\n } catch (e) {\n console.error(`Error occured executing a ${isVertical ? 'vertical' : 'universal'} search.\\n`, e);\n }\n}\n\n/**\n * Executes a universal/vertical autocomplete search and return the corresponding response.\n *\n * @public\n */\nexport async function executeAutocomplete(\n searchActions: SearchActions\n): Promise<AutocompleteResponse | undefined> {\n const isVertical = searchActions.state.meta.searchType === SearchTypeEnum.Vertical;\n try {\n return isVertical\n ? searchActions.executeVerticalAutocomplete()\n : searchActions.executeUniversalAutocomplete();\n } catch (e) {\n console.error(`Error occured executing a ${isVertical ? 'vertical' : 'universal'} autocomplete search.\\n`, e);\n }\n}\n\n/**\n * Get search intents of the current query stored in headless using autocomplete request.\n *\n * @public\n */\nexport async function getSearchIntents(\n searchActions: SearchActions\n): Promise<SearchIntent[] | undefined> {\n const results = await executeAutocomplete(searchActions);\n return results?.inputIntents;\n}\n\n/**\n * Executes a generative direct answer and return the corresponding response.\n *\n * @public\n */\nexport async function executeGenerativeDirectAnswer(\n searchActions: SearchActions\n): Promise<GenerativeDirectAnswerResponse | undefined> {\n try {\n return await searchActions.executeGenerativeDirectAnswer();\n } catch (e) {\n console.error(`Error occured executing generative direct answer.\\n`, e);\n }\n}\n","import {\n SearchActions,\n SearchIntent,\n} from '@yext/search-headless-react';\n\nconst defaultGeolocationOptions: PositionOptions = {\n enableHighAccuracy: false,\n timeout: 6000,\n maximumAge: 300000,\n};\n\n/**\n * If the provided search intents include a 'NEAR_ME' intent and there's no existing\n * user's location in state, retrieve and store user's location in headless state.\n *\n * @public\n */\nexport async function updateLocationIfNeeded(\n searchActions: SearchActions,\n intents: SearchIntent[],\n geolocationOptions?: PositionOptions\n): Promise<void> {\n if (intents.includes(SearchIntent.NearMe) && !searchActions.state.location.userLocation) {\n try {\n const position = await getUserLocation(geolocationOptions);\n searchActions.setUserLocation({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n });\n } catch (e) {\n console.error(e);\n }\n }\n}\n\n/**\n * Retrieves user's location using navigator.geolocation API.\n *\n * @public\n */\nexport async function getUserLocation(geolocationOptions?: PositionOptions): Promise<GeolocationPosition> {\n return new Promise((resolve, reject) => {\n if ('geolocation' in navigator) {\n navigator.geolocation.getCurrentPosition(\n position => resolve(position),\n err => {\n console.error('Error occured using geolocation API. Unable to determine user\\'s location.');\n reject(err);\n },\n { ...defaultGeolocationOptions, ...geolocationOptions }\n );\n } else {\n reject('No access to geolocation API. Unable to determine user\\'s location.');\n }\n });\n}\n","import { useRef, useState, useCallback, useEffect } from 'react';\nimport { useComponentMountStatus } from './useComponentMountStatus';\n\n/**\n * Handles the network request race condition by synchronizing requests with their responses. If multiple\n * requests are sent before getting a response, only the response corresponding to the latest request will\n * be returned.\n *\n * @param executeRequest - Function that executes the network request\n * @param handleRejectedPromise - Function that executes when a rejected promise is received from the request\n *\n * @returns Reponse to the latest request and a function to execute the request in a synchronized manner\n */\nexport function useSynchronizedRequest<RequestDataType, ResponseType>(\n executeRequest: (data?: RequestDataType) => Promise<ResponseType | undefined>,\n handleRejectedPromise?: (error: unknown) => void\n): [\n ResponseType | undefined,\n (data?: RequestDataType) => Promise<ResponseType | undefined>,\n () => void\n ]\n{\n const executeRequestRef = useRef(executeRequest);\n const handleRejectedPromiseRef = useRef(handleRejectedPromise);\n const isMountedRef = useComponentMountStatus();\n const networkIds = useRef({ latestRequest: 0, responseInState: 0 });\n const [synchronizedResponse, setSynchronizedResponse] = useState<ResponseType>();\n\n const executeSynchronizedRequest = useCallback(async (data?: RequestDataType):\n Promise<ResponseType | undefined> => {\n const requestId = ++networkIds.current.latestRequest;\n return new Promise(async (resolve) => {\n let response: ResponseType | undefined = undefined;\n try {\n response = await executeRequestRef.current(data);\n } catch (e) {\n handleRejectedPromiseRef.current ? handleRejectedPromiseRef.current(e) : console.error(e);\n }\n if (requestId >= networkIds.current.responseInState) {\n /**\n * Avoid performing a React state update on an unmounted component\n * (e.g unmounted during async await)\n */\n if (!isMountedRef.current) {\n return;\n }\n setSynchronizedResponse(response);\n networkIds.current.responseInState = requestId;\n }\n resolve(response);\n });\n }, [isMountedRef]);\n\n const clearResponseData = useCallback(() => {\n setSynchronizedResponse(undefined);\n }, [setSynchronizedResponse]);\n\n useEffect(() => {\n executeRequestRef.current = executeRequest;\n handleRejectedPromiseRef.current = handleRejectedPromise;\n });\n\n return [synchronizedResponse, executeSynchronizedRequest, clearResponseData];\n}","import React from 'react';\n\nexport function VerticalDividerIcon({ className }: { className?: string }): JSX.Element {\n return (\n <svg className={className}\n width=\"1\" height=\"24\" viewBox=\"0 0 1 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <rect width=\"1\" height=\"24\" rx=\"0.5\" fill=\"#E1E5E8\" />\n </svg>\n );\n}","import React from 'react';\n\nexport function HistoryIcon(): JSX.Element {\n return (\n <svg viewBox=\"0 0 14 15\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M13.7813 7.75C13.7539 4.00391 10.7188 0.96875 7 0.96875C5.11328 0.96875 3.39063 1.76172 2.16016 2.99219L0.929688 1.76172C0.738281 1.57031 0.382813 1.70703 0.382813 2.00781L0.382813 5.45312C0.382813 5.64453 0.519531 5.78125 0.710938 5.78125L4.21094 5.78125C4.51172 5.78125 4.64844 5.42578 4.45703 5.23437L3.11719 3.92188C4.10156 2.91016 5.46875 2.28125 7 2.28125C10.0078 2.28125 12.4688 4.74219 12.4688 7.75C12.4688 10.7852 10.0078 13.2187 7 13.2188C5.57813 13.2188 4.32031 12.6992 3.33594 11.8516C3.22656 11.7422 3.00781 11.7422 2.89844 11.8516L2.43359 12.3164C2.29688 12.4531 2.29688 12.6719 2.43359 12.8086C3.63672 13.875 5.25 14.5586 7 14.5312C10.7188 14.5312 13.7813 11.4961 13.7813 7.75ZM9.1875 10.2109L9.59766 9.69141C9.67969 9.52734 9.65234 9.33594 9.51563 9.22656L7.65625 7.85937V3.92187C7.65625 3.75781 7.49219 3.59375 7.32813 3.59375H6.67188C6.48047 3.59375 6.34375 3.75781 6.34375 3.92187V8.54297L8.75 10.293C8.88672 10.4023 9.10547 10.375 9.1875 10.2109Z\"/>\n </svg>\n );\n}","import React from 'react';\n\nexport function CloseIcon(): JSX.Element {\n return (\n <svg viewBox=\"0 0 18 18\" xmlns=\"http://www.w3.org/2000/svg\" fill='currentColor' aria-hidden=\"true\">\n <path d=\"M10.9095 9.00028L16.6786 3.2311L17.8684 2.04138C18.0439 1.86587 18.0439 1.58067 17.8684 1.40517L16.5954 0.132192C16.4199 -0.0433137 16.1347 -0.0433137 15.9592 0.132192L9.00028 7.0911L2.04138 0.131629C1.86587 -0.0438764 1.58067 -0.0438764 1.40517 0.131629L0.131629 1.40461C-0.0438764 1.58011 -0.0438764 1.86531 0.131629 2.04081L7.0911 9.00028L0.131629 15.9592C-0.0438764 16.1347 -0.0438764 16.4199 0.131629 16.5954L1.40461 17.8684C1.58011 18.0439 1.86531 18.0439 2.04081 17.8684L9.00028 10.9095L14.7695 16.6786L15.9592 17.8684C16.1347 18.0439 16.4199 18.0439 16.5954 17.8684L17.8684 16.5954C18.0439 16.4199 18.0439 16.1347 17.8684 15.9592L10.9095 9.00028Z\" fill=\"#6b7280\"/>\n </svg>\n );\n}","import React from 'react';\n\nexport function MagnifyingGlassIcon(): JSX.Element {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M0 0h24v24H0V0z\" fill=\"none\" />\n <path\n d=\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z\" />\n </svg>\n );\n}","import React, {\n createElement,\n isValidElement,\n KeyboardEvent,\n PropsWithChildren,\n ReactNode,\n useEffect,\n useMemo,\n useRef,\n useState\n} from 'react';\nimport { DropdownContext, DropdownContextType } from './DropdownContext';\nimport { InputContext, InputContextType } from './InputContext';\nimport useRootClosePkg from '@restart/ui/useRootClose';\nimport { FocusContext, FocusContextType } from './FocusContext';\nimport { ScreenReader } from '../ScreenReader';\nimport { recursivelyMapChildren } from '../utils/recursivelyMapChildren';\nimport { DropdownItem, DropdownItemProps, DropdownItemWithIndex } from './DropdownItem';\nimport { useLayoutEffect } from '../../hooks/useLayoutEffect';\nimport { useId } from '../../hooks/useId';\n\nconst useRootClose = typeof useRootClosePkg === 'function' ? useRootClosePkg : useRootClosePkg['default'];\n\ninterface DropdownItemData {\n value: string,\n itemData?: Record<string, unknown>\n}\n\nexport interface DropdownProps {\n screenReaderText: string,\n screenReaderInstructions?: string,\n parentQuery?: string,\n onSelect?: (value: string, index: number, focusedItemData: Record<string, unknown> | undefined) => void,\n onToggle?: (\n isActive: boolean,\n prevValue: string,\n value: string,\n index: number,\n focusedItemData: Record<string, unknown> | undefined\n ) => void,\n className?: string,\n activeClassName?: string,\n alwaysSelectOption?: boolean\n}\n\n/**\n * Dropdown is the parent component for a set of Dropdown-related components.\n *\n * @remarks\n * It provides multiple shared contexts, which are consumed by its child components,\n * and also registers some global event listeners.\n */\nexport function Dropdown(props: PropsWithChildren<DropdownProps>): JSX.Element {\n const {\n children,\n screenReaderText,\n screenReaderInstructions = 'When autocomplete results are available, use up and down arrows to review and enter to select.',\n onSelect,\n onToggle,\n className,\n activeClassName,\n parentQuery,\n alwaysSelectOption = false\n } = props;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const screenReaderUUID = useId('dropdown');\n const [screenReaderKey, setScreenReaderKey] = useState<number>(0);\n const [hasTyped, setHasTyped] = useState<boolean>(false);\n const [childrenWithDropdownItemsTransformed, items] = useMemo(() => {\n return getTransformedChildrenAndItemData(children);\n }, [children]);\n\n const inputContext = useInputContextInstance();\n const { value, setValue, lastTypedOrSubmittedValue, setLastTypedOrSubmittedValue } = inputContext;\n\n const focusContext = useFocusContextInstance(\n items,\n lastTypedOrSubmittedValue,\n setValue,\n screenReaderKey,\n setScreenReaderKey,\n alwaysSelectOption\n );\n const { focusedIndex, focusedItemData, updateFocusedItem } = focusContext;\n\n const dropdownContext = useDropdownContextInstance(\n lastTypedOrSubmittedValue,\n value,\n focusedIndex,\n focusedItemData,\n screenReaderUUID,\n setHasTyped,\n onToggle,\n onSelect\n );\n const { toggleDropdown, isActive } = dropdownContext;\n\n useLayoutEffect(() => {\n if (parentQuery !== undefined && parentQuery !== lastTypedOrSubmittedValue) {\n setLastTypedOrSubmittedValue(parentQuery);\n updateFocusedItem(-1, parentQuery);\n }\n }, [\n parentQuery,\n lastTypedOrSubmittedValue,\n updateFocusedItem,\n setLastTypedOrSubmittedValue\n ]);\n\n useRootClose(containerRef, () => {\n toggleDropdown(false);\n }, { disabled: !isActive });\n\n function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {\n if (!isActive) {\n return;\n }\n\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n e.preventDefault();\n }\n\n if (e.key === 'ArrowDown') {\n if (alwaysSelectOption && focusedIndex === items.length - 1) {\n updateFocusedItem(0);\n } else {\n updateFocusedItem(focusedIndex + 1);\n }\n } else if (e.key === 'ArrowUp') {\n if (alwaysSelectOption && focusedIndex === 0) {\n updateFocusedItem(items.length - 1);\n } else {\n updateFocusedItem(focusedIndex - 1);\n }\n } else if (e.key === 'Tab' && !e.shiftKey) {\n if (items.length !== 0) {\n if (focusedIndex >= items.length - 1) {\n updateFocusedItem(-1);\n toggleDropdown(false);\n } else {\n updateFocusedItem(focusedIndex + 1);\n e.preventDefault();\n }\n }\n } else if (e.key === 'Tab' && e.shiftKey) {\n if (focusedIndex > 0 || (!alwaysSelectOption && focusedIndex === 0)) {\n updateFocusedItem(focusedIndex - 1);\n e.preventDefault();\n } else {\n updateFocusedItem(-1);\n toggleDropdown(false);\n }\n } else if (!hasTyped) {\n setHasTyped(true);\n }\n }\n\n return (\n <div ref={containerRef} className={isActive ? activeClassName : className} onKeyDown={handleKeyDown}>\n <DropdownContext.Provider value={dropdownContext}>\n <InputContext.Provider value={inputContext}>\n <FocusContext.Provider value={focusContext}>\n {childrenWithDropdownItemsTransformed}\n </FocusContext.Provider>\n </InputContext.Provider>\n </DropdownContext.Provider>\n\n <ScreenReader\n announcementKey={screenReaderKey}\n announcementText={isActive && (hasTyped || items.length || value) ? screenReaderText : ''}\n instructionsId={screenReaderUUID}\n instructions={screenReaderInstructions}\n />\n </div>\n );\n}\n\nfunction useInputContextInstance(): InputContextType {\n const [value, setValue] = useState('');\n const [lastTypedOrSubmittedValue, setLastTypedOrSubmittedValue] = useState('');\n return {\n value,\n setValue,\n lastTypedOrSubmittedValue,\n setLastTypedOrSubmittedValue\n };\n}\n\nfunction useFocusContextInstance(\n items: DropdownItemData[],\n lastTypedOrSubmittedValue: string,\n setValue: (newValue: string) => void,\n screenReaderKey: number,\n setScreenReaderKey: (newKey: number) => void,\n alwaysSelectOption: boolean\n): FocusContextType {\n const [focusedIndex, setFocusedIndex] = useState(-1);\n const [focusedValue, setFocusedValue] = useState<string | null>(null);\n const [focusedItemData, setFocusedItemData] = useState<Record<string, unknown> | undefined>(undefined);\n useEffect(() => {\n if (alwaysSelectOption) {\n if (items.length > 0) {\n const index = focusedIndex === -1 || focusedIndex >= items.length ? 0 : focusedIndex;\n setFocusedIndex(index);\n setFocusedValue(items[index].value);\n setFocusedItemData(items[index].itemData);\n } else {\n setFocusedIndex(-1);\n setFocusedValue(null);\n setFocusedItemData(undefined);\n }\n }\n }, [alwaysSelectOption, focusedIndex, items]);\n\n function updateFocusedItem(updatedFocusedIndex: number, value?: string) {\n const numItems = items.length;\n let updatedValue;\n if (updatedFocusedIndex === -1 || updatedFocusedIndex >= numItems || numItems === 0) {\n updatedValue = value ?? lastTypedOrSubmittedValue;\n if (alwaysSelectOption && numItems !== 0) {\n setFocusedIndex(0);\n setFocusedItemData(items[0].itemData);\n setScreenReaderKey(screenReaderKey + 1);\n } else {\n setFocusedIndex(-1);\n setFocusedItemData(undefined);\n setScreenReaderKey(screenReaderKey + 1);\n }\n } else if (updatedFocusedIndex < -1) {\n const loopedAroundIndex = (numItems + updatedFocusedIndex + 1) % numItems;\n updatedValue = value ?? items[loopedAroundIndex].value;\n setFocusedIndex(loopedAroundIndex);\n setFocusedItemData(items[loopedAroundIndex].itemData);\n } else {\n updatedValue = value ?? items[updatedFocusedIndex].value;\n setFocusedIndex(updatedFocusedIndex);\n setFocusedItemData(items[updatedFocusedIndex].itemData);\n }\n setFocusedValue(updatedValue);\n setValue(alwaysSelectOption ? (value ?? lastTypedOrSubmittedValue) : updatedValue);\n }\n\n return {\n focusedIndex,\n focusedValue,\n focusedItemData,\n updateFocusedItem\n };\n}\n\nfunction useDropdownContextInstance(\n prevValue: string,\n value: string,\n index: number,\n focusedItemData: Record<string, unknown> | undefined,\n screenReaderUUID: string | undefined,\n setHasTyped: (hasTyped: boolean) => void,\n onToggle?: (\n isActive: boolean,\n prevValue: string,\n value: string,\n index: number,\n focusedItemData: Record<string, unknown> | undefined\n ) => void,\n onSelect?: (value: string, index: number, focusedItemDa