UNPKG

@yext/search-ui-react

Version:

A library of React Components for powering Yext Search integrations

1,351 lines (1,309 loc) 185 kB
// src/components/SearchBar.tsx import { QuerySource, SearchTypeEnum as SearchTypeEnum2, useSearchActions as useSearchActions2, useSearchState as useSearchState2, useSearchUtilities } from "@yext/search-headless-react"; import classNames from "classnames"; import React14, { Fragment, isValidElement as isValidElement3, useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo3 } from "react"; // src/hooks/useEntityPreviews.tsx import { useState } from "react"; // src/hooks/useComponentMountStatus.tsx import { useEffect, useRef } from "react"; function useComponentMountStatus() { const isMountedRef = useRef(false); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); return isMountedRef; } // src/hooks/useDebouncedFunction.ts import { useRef as useRef2 } from "react"; function useDebouncedFunction(func, milliseconds) { const timeoutIdRef = useRef2(); if (!func) { return void 0; } const debounced = (...args) => { return new Promise((resolve) => { if (timeoutIdRef.current !== void 0) { clearTimeout(timeoutIdRef.current); } timeoutIdRef.current = window.setTimeout(() => { resolve(func(...args)); timeoutIdRef.current = void 0; }, milliseconds); }); }; return debounced; } // src/hooks/useEntityPreviews.tsx function useEntityPreviews(entityPreviewSearcher, debounceTime) { const isMountedRef = useComponentMountStatus(); const [ verticalKeyToResults, setVerticalKeyToResults ] = useState({}); const debouncedUniversalSearch = useDebouncedFunction(async () => { if (!entityPreviewSearcher) { return; } await entityPreviewSearcher.executeUniversalQuery(); if (!isMountedRef.current) { return; } const results = entityPreviewSearcher.state.universal.verticals || []; setVerticalKeyToResults(getVerticalKeyToResults(results)); setLoadingState(false); }, debounceTime); const [isLoading, setLoadingState] = useState(false); function executeEntityPreviewsQuery(query, universalLimit, restrictVerticals) { if (!entityPreviewSearcher) { return; } if (query === entityPreviewSearcher.state.query.input) { return; } setLoadingState(true); entityPreviewSearcher.setQuery(query); entityPreviewSearcher.setRestrictVerticals(restrictVerticals); entityPreviewSearcher.setUniversalLimit(universalLimit); debouncedUniversalSearch?.(); } return [{ verticalKeyToResults, isLoading }, executeEntityPreviewsQuery]; } function getVerticalKeyToResults(verticalResultsArray) { return verticalResultsArray.reduce((prev, current) => { prev[current.verticalKey] = current; return prev; }, {}); } // src/hooks/useRecentSearches.ts import { useCallback, useEffect as useEffect2, useState as useState2 } from "react"; import { RecentSearches } from "recent-searches"; function useRecentSearches(recentSearchesLimit, verticalKey) { const recentSearchesKey = getRecentSearchesKey(verticalKey); const [recentSearches, setRecentSeaches] = useState2( new RecentSearches({ limit: recentSearchesLimit, namespace: recentSearchesKey }) ); const clearRecentSearches = useCallback(() => { localStorage.removeItem(recentSearchesKey); setRecentSeaches(new RecentSearches({ limit: recentSearchesLimit, namespace: recentSearchesKey })); localStorage.removeItem(recentSearchesKey); }, [recentSearchesKey, recentSearchesLimit]); const setRecentSearch = useCallback((input) => { recentSearches.setRecentSearch(input); }, [recentSearches]); useEffect2(() => { setRecentSeaches(new RecentSearches({ limit: recentSearchesLimit, namespace: recentSearchesKey })); }, [recentSearchesKey, recentSearchesLimit]); return [recentSearches?.getRecentSearches(), setRecentSearch, clearRecentSearches]; } function getRecentSearchesKey(verticalKey) { if (verticalKey) { return `__yxt_recent_searches_${verticalKey}__`; } else { return "__yxt_recent_searches_universal__"; } } // src/hooks/useSearchWithNearMeHandling.ts import { useSearchActions } from "@yext/search-headless-react"; // src/utils/search-operations.ts import { SearchTypeEnum } from "@yext/search-headless-react"; async function executeSearch(searchActions) { const isVertical = searchActions.state.meta.searchType === SearchTypeEnum.Vertical; try { isVertical ? searchActions.executeVerticalQuery() : searchActions.executeUniversalQuery(); } catch (e) { console.error(`Error occured executing a ${isVertical ? "vertical" : "universal"} search. `, e); } } async function executeAutocomplete(searchActions) { const isVertical = searchActions.state.meta.searchType === SearchTypeEnum.Vertical; try { return isVertical ? searchActions.executeVerticalAutocomplete() : searchActions.executeUniversalAutocomplete(); } catch (e) { console.error(`Error occured executing a ${isVertical ? "vertical" : "universal"} autocomplete search. `, e); } } async function getSearchIntents(searchActions) { const results = await executeAutocomplete(searchActions); return results?.inputIntents; } async function executeGenerativeDirectAnswer(searchActions) { try { return await searchActions.executeGenerativeDirectAnswer(); } catch (e) { console.error(`Error occured executing generative direct answer. `, e); } } // src/utils/location-operations.ts import { SearchIntent as SearchIntent2 } from "@yext/search-headless-react"; var defaultGeolocationOptions = { enableHighAccuracy: false, timeout: 6e3, maximumAge: 3e5 }; async function updateLocationIfNeeded(searchActions, intents, geolocationOptions) { if (intents.includes(SearchIntent2.NearMe) && !searchActions.state.location.userLocation) { try { const position = await getUserLocation(geolocationOptions); searchActions.setUserLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude }); } catch (e) { console.error(e); } } } async function getUserLocation(geolocationOptions) { return new Promise((resolve, reject) => { if ("geolocation" in navigator) { navigator.geolocation.getCurrentPosition( (position) => resolve(position), (err) => { console.error("Error occured using geolocation API. Unable to determine user's location."); reject(err); }, { ...defaultGeolocationOptions, ...geolocationOptions } ); } else { reject("No access to geolocation API. Unable to determine user's location."); } }); } // src/hooks/useSearchWithNearMeHandling.ts import { useRef as useRef3 } from "react"; function useSearchWithNearMeHandling(geolocationOptions, onSearch) { const autocompletePromiseRef = useRef3(); const searchActions = useSearchActions(); async function executeQuery() { try { let intents = []; if (!searchActions.state.location.userLocation) { if (!autocompletePromiseRef.current) { autocompletePromiseRef.current = executeAutocomplete(searchActions); } const autocompleteResponseBeforeSearch = await autocompletePromiseRef.current; intents = autocompleteResponseBeforeSearch?.inputIntents || []; await updateLocationIfNeeded(searchActions, intents, geolocationOptions); } } catch (e) { console.error("Error executing autocomplete before search:", e); await updateLocationIfNeeded(searchActions, [], geolocationOptions); } const verticalKey = searchActions.state.vertical.verticalKey ?? ""; const query = searchActions.state.query.input ?? ""; onSearch ? onSearch({ verticalKey, query }) : executeSearch(searchActions); } return [executeQuery, autocompletePromiseRef]; } // src/hooks/useSynchronizedRequest.tsx import { useRef as useRef4, useState as useState3, useCallback as useCallback2, useEffect as useEffect3 } from "react"; function useSynchronizedRequest(executeRequest, handleRejectedPromise) { const executeRequestRef = useRef4(executeRequest); const handleRejectedPromiseRef = useRef4(handleRejectedPromise); const isMountedRef = useComponentMountStatus(); const networkIds = useRef4({ latestRequest: 0, responseInState: 0 }); const [synchronizedResponse, setSynchronizedResponse] = useState3(); const executeSynchronizedRequest = useCallback2(async (data) => { const requestId = ++networkIds.current.latestRequest; return new Promise(async (resolve) => { let response = void 0; try { response = await executeRequestRef.current(data); } catch (e) { handleRejectedPromiseRef.current ? handleRejectedPromiseRef.current(e) : console.error(e); } if (requestId >= networkIds.current.responseInState) { if (!isMountedRef.current) { return; } setSynchronizedResponse(response); networkIds.current.responseInState = requestId; } resolve(response); }); }, [isMountedRef]); const clearResponseData = useCallback2(() => { setSynchronizedResponse(void 0); }, [setSynchronizedResponse]); useEffect3(() => { executeRequestRef.current = executeRequest; handleRejectedPromiseRef.current = handleRejectedPromise; }); return [synchronizedResponse, executeSynchronizedRequest, clearResponseData]; } // src/icons/VerticalDividerIcon.tsx import React from "react"; function VerticalDividerIcon({ className }) { return /* @__PURE__ */ React.createElement( "svg", { className, width: "1", height: "24", viewBox: "0 0 1 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("rect", { width: "1", height: "24", rx: "0.5", fill: "#E1E5E8" }) ); } // src/icons/HistoryIcon.tsx import React2 from "react"; function HistoryIcon() { return /* @__PURE__ */ React2.createElement("svg", { viewBox: "0 0 14 15", fill: "currentColor", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" }, /* @__PURE__ */ React2.createElement("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" })); } // src/icons/CloseIcon.tsx import React3 from "react"; function CloseIcon() { return /* @__PURE__ */ React3.createElement("svg", { viewBox: "0 0 18 18", xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", "aria-hidden": "true" }, /* @__PURE__ */ React3.createElement("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" })); } // src/icons/MagnifyingGlassIcon.tsx import React4 from "react"; function MagnifyingGlassIcon() { return /* @__PURE__ */ React4.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true" }, /* @__PURE__ */ React4.createElement("path", { d: "M0 0h24v24H0V0z", fill: "none" }), /* @__PURE__ */ React4.createElement( "path", { 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" } )); } // src/components/Dropdown/Dropdown.tsx import React8, { createElement, isValidElement as isValidElement2, useEffect as useEffect5, useMemo, useRef as useRef5, useState as useState5 } from "react"; // src/components/Dropdown/DropdownContext.ts import { createContext, useContext } from "react"; var DropdownContext = createContext(null); function useDropdownContext() { const dropdownContextInstance = useContext(DropdownContext); if (dropdownContextInstance === null) { throw new Error("Tried to use DropdownContext when none exists."); } return dropdownContextInstance; } // src/components/Dropdown/InputContext.ts import { useContext as useContext2, createContext as createContext2 } from "react"; var InputContext = createContext2(null); function useInputContext() { const inputContextInstance = useContext2(InputContext); if (inputContextInstance === null) { throw new Error("Tried to use InputContext when none exists."); } return inputContextInstance; } // src/components/Dropdown/Dropdown.tsx import useRootClosePkg from "@restart/ui/useRootClose"; // src/components/Dropdown/FocusContext.ts import { createContext as createContext3, useContext as useContext3 } from "react"; var FocusContext = createContext3(null); function useFocusContext() { const focusContextInstance = useContext3(FocusContext); if (focusContextInstance === null) { throw new Error("Tried to use FocusContext when none exists."); } return focusContextInstance; } // src/components/ScreenReader.tsx import React5 from "react"; function ScreenReader({ instructionsId, instructions, announcementKey, announcementText }) { return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement( "div", { id: instructionsId, className: "hidden" }, instructions ), /* @__PURE__ */ React5.createElement( "div", { className: "sr-only", key: announcementKey, "aria-live": "assertive" }, announcementText )); } // src/components/utils/recursivelyMapChildren.ts import { Children, cloneElement, isValidElement } from "react"; function recursivelyMapChildren(children, elementReplacer) { return Children.map(children, (c, index) => { if (!isValidElement(c)) { return c; } const replacedElement = elementReplacer(c, index); if (!replacedElement || !isValidElement(replacedElement)) { return replacedElement; } const grandchildren = replacedElement.props.children; if (!grandchildren) { return replacedElement; } const replacedGrandchildren = recursivelyMapChildren(grandchildren, elementReplacer); return cloneElement(replacedElement, {}, [replacedGrandchildren]); }); } // src/components/Dropdown/DropdownItem.tsx import React6, { useCallback as useCallback3 } from "react"; // src/components/Dropdown/generateDropdownId.ts function generateDropdownId(screenReaderUUID, index) { if (!screenReaderUUID) return ""; return screenReaderUUID + "_" + index; } // src/components/Dropdown/DropdownItem.tsx function DropdownItem(_props) { return null; } function DropdownItemWithIndex(props) { const { children, value, index, className, focusedClassName, itemData, onClick, ariaLabel } = props; const { toggleDropdown, onSelect, screenReaderUUID } = useDropdownContext(); const { focusedIndex, updateFocusedItem } = useFocusContext(); const { setValue, setLastTypedOrSubmittedValue } = useInputContext(); const isFocused = focusedIndex === index; const handleClick = useCallback3(() => { toggleDropdown(false); updateFocusedItem(-1); setLastTypedOrSubmittedValue(value); setValue(value); onSelect?.(value, index, itemData); onClick?.(value, index, itemData); }, [ index, itemData, onClick, onSelect, setLastTypedOrSubmittedValue, setValue, toggleDropdown, updateFocusedItem, value ]); return /* @__PURE__ */ React6.createElement( "div", { id: generateDropdownId(screenReaderUUID, index), tabIndex: 0, className: isFocused ? focusedClassName : className, onClick: handleClick, "aria-label": typeof ariaLabel === "function" ? ariaLabel(value) : ariaLabel }, children ); } // src/hooks/useLayoutEffect.ts import useIsomorphicLayoutEffect from "use-isomorphic-layout-effect"; var useLayoutEffect = typeof useIsomorphicLayoutEffect === "function" ? useIsomorphicLayoutEffect : useIsomorphicLayoutEffect["default"]; // src/hooks/useId.ts import React7, { useEffect as useEffect4, useState as useState4 } from "react"; var serverHandoffComplete = false; var id = 0; function genId(baseName) { ++id; return baseName + "-" + id.toString(); } var maybeReactUseId = React7["useId".toString()]; function useId(baseName) { if (maybeReactUseId !== void 0) { return maybeReactUseId(); } const initialId = serverHandoffComplete ? genId(baseName) : ""; const [id2, setId] = useState4(initialId); useLayoutEffect(() => { if (id2 === "") { setId(genId(baseName)); } }, [id2]); useEffect4(() => { if (serverHandoffComplete === false) { serverHandoffComplete = true; } }, []); return id2; } // src/components/Dropdown/Dropdown.tsx var useRootClose = typeof useRootClosePkg === "function" ? useRootClosePkg : useRootClosePkg["default"]; function Dropdown(props) { const { children, screenReaderText, screenReaderInstructions = "When autocomplete results are available, use up and down arrows to review and enter to select.", onSelect, onToggle, className, activeClassName, parentQuery, alwaysSelectOption = false } = props; const containerRef = useRef5(null); const screenReaderUUID = useId("dropdown"); const [screenReaderKey, setScreenReaderKey] = useState5(0); const [hasTyped, setHasTyped] = useState5(false); const [childrenWithDropdownItemsTransformed, items] = useMemo(() => { return getTransformedChildrenAndItemData(children); }, [children]); const inputContext = useInputContextInstance(); const { value, setValue, lastTypedOrSubmittedValue, setLastTypedOrSubmittedValue } = inputContext; const focusContext = useFocusContextInstance( items, lastTypedOrSubmittedValue, setValue, screenReaderKey, setScreenReaderKey, alwaysSelectOption ); const { focusedIndex, focusedItemData, updateFocusedItem } = focusContext; const dropdownContext = useDropdownContextInstance( lastTypedOrSubmittedValue, value, focusedIndex, focusedItemData, screenReaderUUID, setHasTyped, onToggle, onSelect ); const { toggleDropdown, isActive } = dropdownContext; useLayoutEffect(() => { if (parentQuery !== void 0 && parentQuery !== lastTypedOrSubmittedValue) { setLastTypedOrSubmittedValue(parentQuery); updateFocusedItem(-1, parentQuery); } }, [ parentQuery, lastTypedOrSubmittedValue, updateFocusedItem, setLastTypedOrSubmittedValue ]); useRootClose(containerRef, () => { toggleDropdown(false); }, { disabled: !isActive }); function handleKeyDown(e) { if (!isActive) { return; } if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); } if (e.key === "ArrowDown") { if (alwaysSelectOption && focusedIndex === items.length - 1) { updateFocusedItem(0); } else { updateFocusedItem(focusedIndex + 1); } } else if (e.key === "ArrowUp") { if (alwaysSelectOption && focusedIndex === 0) { updateFocusedItem(items.length - 1); } else { updateFocusedItem(focusedIndex - 1); } } else if (e.key === "Tab" && !e.shiftKey) { if (items.length !== 0) { if (focusedIndex >= items.length - 1) { updateFocusedItem(-1); toggleDropdown(false); } else { updateFocusedItem(focusedIndex + 1); e.preventDefault(); } } } else if (e.key === "Tab" && e.shiftKey) { if (focusedIndex > 0 || !alwaysSelectOption && focusedIndex === 0) { updateFocusedItem(focusedIndex - 1); e.preventDefault(); } else { updateFocusedItem(-1); toggleDropdown(false); } } else if (!hasTyped) { setHasTyped(true); } } return /* @__PURE__ */ React8.createElement("div", { ref: containerRef, className: isActive ? activeClassName : className, onKeyDown: handleKeyDown }, /* @__PURE__ */ React8.createElement(DropdownContext.Provider, { value: dropdownContext }, /* @__PURE__ */ React8.createElement(InputContext.Provider, { value: inputContext }, /* @__PURE__ */ React8.createElement(FocusContext.Provider, { value: focusContext }, childrenWithDropdownItemsTransformed))), /* @__PURE__ */ React8.createElement( ScreenReader, { announcementKey: screenReaderKey, announcementText: isActive && (hasTyped || items.length || value) ? screenReaderText : "", instructionsId: screenReaderUUID, instructions: screenReaderInstructions } )); } function useInputContextInstance() { const [value, setValue] = useState5(""); const [lastTypedOrSubmittedValue, setLastTypedOrSubmittedValue] = useState5(""); return { value, setValue, lastTypedOrSubmittedValue, setLastTypedOrSubmittedValue }; } function useFocusContextInstance(items, lastTypedOrSubmittedValue, setValue, screenReaderKey, setScreenReaderKey, alwaysSelectOption) { const [focusedIndex, setFocusedIndex] = useState5(-1); const [focusedValue, setFocusedValue] = useState5(null); const [focusedItemData, setFocusedItemData] = useState5(void 0); useEffect5(() => { if (alwaysSelectOption) { if (items.length > 0) { const index = focusedIndex === -1 || focusedIndex >= items.length ? 0 : focusedIndex; setFocusedIndex(index); setFocusedValue(items[index].value); setFocusedItemData(items[index].itemData); } else { setFocusedIndex(-1); setFocusedValue(null); setFocusedItemData(void 0); } } }, [alwaysSelectOption, focusedIndex, items]); function updateFocusedItem(updatedFocusedIndex, value) { const numItems = items.length; let updatedValue; if (updatedFocusedIndex === -1 || updatedFocusedIndex >= numItems || numItems === 0) { updatedValue = value ?? lastTypedOrSubmittedValue; if (alwaysSelectOption && numItems !== 0) { setFocusedIndex(0); setFocusedItemData(items[0].itemData); setScreenReaderKey(screenReaderKey + 1); } else { setFocusedIndex(-1); setFocusedItemData(void 0); setScreenReaderKey(screenReaderKey + 1); } } else if (updatedFocusedIndex < -1) { const loopedAroundIndex = (numItems + updatedFocusedIndex + 1) % numItems; updatedValue = value ?? items[loopedAroundIndex].value; setFocusedIndex(loopedAroundIndex); setFocusedItemData(items[loopedAroundIndex].itemData); } else { updatedValue = value ?? items[updatedFocusedIndex].value; setFocusedIndex(updatedFocusedIndex); setFocusedItemData(items[updatedFocusedIndex].itemData); } setFocusedValue(updatedValue); setValue(alwaysSelectOption ? value ?? lastTypedOrSubmittedValue : updatedValue); } return { focusedIndex, focusedValue, focusedItemData, updateFocusedItem }; } function useDropdownContextInstance(prevValue, value, index, focusedItemData, screenReaderUUID, setHasTyped, onToggle, onSelect) { const [isActive, _toggleDropdown] = useState5(false); const toggleDropdown = (willBeOpen) => { if (!willBeOpen) { setHasTyped(false); } _toggleDropdown(willBeOpen); onToggle?.(willBeOpen, prevValue, value, index, focusedItemData); }; return { isActive, toggleDropdown, onSelect, screenReaderUUID }; } function getTransformedChildrenAndItemData(children) { const items = []; const childrenWithDropdownItemsTransformed = recursivelyMapChildren(children, (child) => { if (!(isValidElement2(child) && child.type === DropdownItem)) { return child; } const props = child.props; items.push({ value: props.value, itemData: props.itemData }); return createElement(DropdownItemWithIndex, { ...props, index: items.length - 1 }); }); return [childrenWithDropdownItemsTransformed, items]; } // src/components/Dropdown/DropdownInput.tsx import React9, { useCallback as useCallback4, useRef as useRef6, useState as useState6 } from "react"; function DropdownInput(props) { const { className, placeholder, ariaLabel, onSubmit, onFocus, onChange, submitCriteria } = props; const inputRef = useRef6(null); const { toggleDropdown, onSelect, screenReaderUUID } = useDropdownContext(); const { value = "", setLastTypedOrSubmittedValue } = useInputContext(); const { focusedIndex = -1, focusedItemData, focusedValue, updateFocusedItem } = useFocusContext(); const [isTyping, setIsTyping] = useState6(true); const handleChange = useCallback4((e) => { setIsTyping(true); toggleDropdown(true); onChange?.(e.target.value); updateFocusedItem(-1, e.target.value); setLastTypedOrSubmittedValue(e.target.value); }, [onChange, setLastTypedOrSubmittedValue, toggleDropdown, updateFocusedItem]); const handleKeyDown = useCallback4((e) => { if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Tab") { setIsTyping(false); } if (e.key === "Enter" && (!submitCriteria || submitCriteria(focusedIndex))) { updateFocusedItem(focusedIndex); toggleDropdown(false); inputRef.current?.blur(); onSubmit?.(value, focusedIndex, focusedItemData); if (focusedIndex >= 0) { onSelect?.(value, focusedIndex, focusedItemData); } updateFocusedItem(-1, focusedValue ?? void 0); } }, [ focusedIndex, focusedValue, focusedItemData, onSelect, onSubmit, submitCriteria, toggleDropdown, updateFocusedItem, value ]); const handleFocus = useCallback4(() => { toggleDropdown(true); updateFocusedItem(-1); onFocus?.(value); }, [onFocus, toggleDropdown, updateFocusedItem, value]); return /* @__PURE__ */ React9.createElement( "input", { ref: inputRef, className, placeholder, value, onChange: handleChange, onKeyDown: handleKeyDown, onFocus: handleFocus, id: generateDropdownId(screenReaderUUID, -1), autoComplete: "off", "aria-describedby": screenReaderUUID, "aria-activedescendant": isTyping ? "" : generateDropdownId(screenReaderUUID, focusedIndex), "aria-label": ariaLabel } ); } // src/components/Dropdown/DropdownMenu.tsx import React10 from "react"; function DropdownMenu({ children }) { const { isActive } = useDropdownContext(); if (!isActive) { return null; } return /* @__PURE__ */ React10.createElement(React10.Fragment, null, children); } // src/hooks/useComposedCssClasses.tsx import { useMemo as useMemo2 } from "react"; import { extendTailwindMerge } from "tailwind-merge"; var twMerge = extendTailwindMerge({ classGroups: { form: ["input", "checkbox", "textarea", "select", "multiselect", "radio"].map((v) => "form-" + v) } }); function useComposedCssClasses(builtInClasses, customClasses, disableBuiltInClasses = false) { return useMemo2(() => { if (disableBuiltInClasses && customClasses) { return customClasses; } const mergedCssClasses = { ...builtInClasses }; if (!customClasses) { return mergedCssClasses; } Object.keys(customClasses).forEach((key) => { const builtIn = builtInClasses[key]; const custom = customClasses[key]; if (!builtIn || !custom) { mergedCssClasses[key] = custom || builtIn; } else { mergedCssClasses[key] = twMerge(builtIn, custom); } }); return mergedCssClasses; }, [builtInClasses, customClasses, disableBuiltInClasses]); } // src/components/SearchButton.tsx import React11 from "react"; function SearchButton({ handleClick, className }) { return /* @__PURE__ */ React11.createElement( "button", { className, onClick: handleClick, "aria-label": "Submit Search" }, /* @__PURE__ */ React11.createElement(MagnifyingGlassIcon, null) ); } // src/components/utils/processTranslation.ts function processTranslation(args) { if (args.count != null && args.pluralForm && args.count !== 1) { return args.pluralForm; } else { return args.phrase; } } // src/components/utils/renderHighlightedValue.tsx import React12 from "react"; var defaultCssClasses = { highlighted: "font-normal", nonHighlighted: "font-semibold" }; function renderHighlightedValue(highlightedValueOrString, customCssClasses) { const { value = "", matchedSubstrings } = typeof highlightedValueOrString === "string" ? { value: highlightedValueOrString, matchedSubstrings: [] } : highlightedValueOrString; const cssClasses = { ...defaultCssClasses, ...customCssClasses }; if (!matchedSubstrings || matchedSubstrings.length === 0) { return /* @__PURE__ */ React12.createElement("span", null, value); } const substrings = [...matchedSubstrings]; substrings.sort((a, b) => a.offset - b.offset); const highlightedJSX = []; let curr = 0; for (const { offset, length } of substrings) { if (offset > curr) { highlightedJSX.push( /* @__PURE__ */ React12.createElement("span", { key: curr, className: cssClasses.nonHighlighted }, value.substring(curr, offset)) ); } highlightedJSX.push( /* @__PURE__ */ React12.createElement("span", { key: offset, className: cssClasses.highlighted }, value.substring(offset, offset + length)) ); curr = offset + length; } if (curr < value.length) { highlightedJSX.push( /* @__PURE__ */ React12.createElement("span", { key: curr, className: cssClasses.nonHighlighted }, value.substring(curr)) ); } return /* @__PURE__ */ React12.createElement(React12.Fragment, null, highlightedJSX); } // src/components/utils/renderAutocompleteResult.tsx import React13 from "react"; var builtInCssClasses = { option: "whitespace-no-wrap max-w-full px-3 text-neutral-dark truncate", icon: "w-6 h-full flex-shrink-0 text-gray-400" }; function renderAutocompleteResult(result, cssClasses = {}, Icon, ariaLabel) { return /* @__PURE__ */ React13.createElement(React13.Fragment, null, Icon && /* @__PURE__ */ React13.createElement("div", { className: cssClasses.icon }, /* @__PURE__ */ React13.createElement(Icon, null)), /* @__PURE__ */ React13.createElement("div", { "aria-label": ariaLabel || "", className: cssClasses.option }, renderHighlightedValue(result, cssClasses))); } // src/hooks/useAnalytics.ts import { createContext as createContext4, useContext as useContext4 } from "react"; var AnalyticsContext = createContext4(null); function useAnalytics() { return useContext4(AnalyticsContext); } // src/hooks/useSearchBarAnalytics.ts import { useSearchState } from "@yext/search-headless-react"; function useSearchBarAnalytics() { const analytics = useAnalytics(); const verticalKey = useSearchState((state) => state.vertical.verticalKey); const queryId = useSearchState((state) => state.query.queryId); const reportAutocompleteEvent = (suggestedSearchText) => { analytics?.report({ type: "AUTO_COMPLETE_SELECTION", ...queryId && { queryId }, suggestedSearchText }); }; const reportSearchClearEvent = () => { if (!queryId) { console.error("Unable to report a search clear event. Missing field: queryId."); return; } analytics?.report({ type: "SEARCH_CLEAR_BUTTON", queryId, verticalKey }); }; const reportAnalyticsEvent = (analyticsEventType, suggestedSearchText) => { if (!analytics) { return; } analyticsEventType === "AUTO_COMPLETE_SELECTION" ? reportAutocompleteEvent(suggestedSearchText || "") : reportSearchClearEvent(); }; return reportAnalyticsEvent; } // src/models/verticalLink.ts var isVerticalLink = (obj) => { return typeof obj === "object" && !!obj && "verticalKey" in obj; }; // src/utils/filterutils.tsx import { Matcher as Matcher2 } from "@yext/search-headless-react"; import isEqual from "lodash/isEqual.js"; // src/models/NumberRangeFilter.ts import { Matcher } from "@yext/search-headless-react"; function isNumberRangeFilter(unknownFilter = {}) { const filter = unknownFilter; return filter.matcher === Matcher.Between && isNumberRangeValue(filter.value); } // src/utils/filterutils.tsx function isNearFilterValue(obj) { return typeof obj === "object" && !!obj && "radius" in obj && "lat" in obj && "long" in obj; } function isNumberRangeValue(obj) { return typeof obj === "object" && !!obj && ("start" in obj || "end" in obj); } function isStringFacet(facet) { return facet.options.length > 0 && typeof facet.options[0].value === "string"; } function isNumericalFacet(facet) { return facet.options.length > 0 && facet.options.some((option) => isNumberRangeFilter(option)); } function isDuplicateFieldValueFilter(thisFilter, otherFilter) { if (thisFilter.fieldId !== otherFilter.fieldId) { return false; } if (thisFilter.matcher !== otherFilter.matcher) { return false; } return isEqual(thisFilter.value, otherFilter.value); } function isDuplicateStaticFilter(thisFilter, otherFilter) { if (thisFilter.kind === "fieldValue") { return otherFilter.kind === "fieldValue" ? isDuplicateFieldValueFilter(thisFilter, otherFilter) : false; } if (otherFilter.kind === "fieldValue") { return false; } return thisFilter.combinator === otherFilter.combinator && thisFilter.filters.length === otherFilter.filters.length && thisFilter.filters.every((t) => otherFilter.filters.some((o) => isDuplicateStaticFilter(t, o))) && otherFilter.filters.every((o) => thisFilter.filters.some((t) => isDuplicateStaticFilter(o, t))); } function findSelectableFieldValueFilter(filter, selectableFilters) { return selectableFilters.find((selectableFilter) => { const { displayName: _2, ...storedFilter } = selectableFilter; return isDuplicateFieldValueFilter(storedFilter, filter); }); } function parseNumberRangeInput(minRangeInput, maxRangeInput) { const minRange = parseNumber(minRangeInput); const maxRange = parseNumber(maxRangeInput); return { ...minRange !== void 0 && { start: { matcher: Matcher2.GreaterThanOrEqualTo, value: minRange } }, ...maxRange !== void 0 && { end: { matcher: Matcher2.LessThanOrEqualTo, value: maxRange } } }; } function parseNumber(num) { const parsedNum = parseFloat(num); if (isNaN(parsedNum)) { return void 0; } return parsedNum; } function clearStaticRangeFilters(searchActions, fieldIds) { const selectedStaticRangeFilters = searchActions.state?.filters?.static?.filter( (filter) => isNumberRangeFilter(filter) && filter.selected === true && (!fieldIds || fieldIds.has(filter.fieldId)) ); selectedStaticRangeFilters?.forEach((filter) => { searchActions.setFilterOption({ ...filter, selected: false }); }); } function getSelectedNumericalFacetFields(searchActions) { const selectedNumericalFacets = searchActions.state.filters.facets?.filter( (f) => isNumericalFacet(f) && f.options.some((o) => o.selected) ) ?? []; return new Set(selectedNumericalFacets.map((f) => f.fieldId)); } function getSelectableFieldValueFilters(staticFilters) { return staticFilters.map((s) => { const { filter: { kind, ...filterFields }, ...displayFields } = s; if (kind === "fieldValue") { return { ...displayFields, ...filterFields }; } return void 0; }).filter((s) => !!s); } function getDefaultFilterDisplayName(numberRange) { const start = numberRange.start; const end = numberRange.end; if (start && end) { return `${start.value} - ${end.value}`; } else if (start && !end) { return `Over ${start.value}`; } else if (end && !start) { return `Up to ${end.value}`; } return ""; } // src/components/SearchBar.tsx var builtInCssClasses2 = { searchBarContainer: "h-12 mb-6", inputDivider: "border-t border-gray-200 mx-2.5", 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 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", ...builtInCssClasses }; function SearchBar({ placeholder, geolocationOptions, hideRecentSearches, visualAutocompleteConfig, showVerticalLinks = false, onSelectVerticalLink, verticalKeyToLabel, recentSearchesLimit = 5, customCssClasses, onSearch }) { const { entityPreviewSearcher, renderEntityPreviews, includedVerticals, universalLimit, entityPreviewsDebouncingTime = 500 } = visualAutocompleteConfig ?? {}; const searchActions = useSearchActions2(); const searchUtilities = useSearchUtilities(); const reportAnalyticsEvent = useSearchBarAnalytics(); const query = useSearchState2((state) => state.query.input) ?? ""; const cssClasses = useComposedCssClasses(builtInCssClasses2, customCssClasses); const isVertical = useSearchState2((state) => state.meta.searchType) === SearchTypeEnum2.Vertical; const verticalKey = useSearchState2((state) => state.vertical.verticalKey); const debouncedExecuteAutocompleteSearch = useDebouncedFunction(() => executeAutocomplete(searchActions), 200); const [autocompleteResponse, executeAutocomplete2, clearAutocompleteData] = useSynchronizedRequest( async () => { return debouncedExecuteAutocompleteSearch ? debouncedExecuteAutocompleteSearch() : void 0; } ); const [ executeQueryWithNearMeHandling, autocompletePromiseRef ] = useSearchWithNearMeHandling(geolocationOptions, onSearch); const [ recentSearches, setRecentSearch, clearRecentSearches ] = useRecentSearches(recentSearchesLimit, verticalKey); const filteredRecentSearches = recentSearches?.filter( (search) => searchUtilities.isCloseMatch(search.query, query) ); useEffect6(() => { if (hideRecentSearches) { clearRecentSearches(); } }, [clearRecentSearches, hideRecentSearches]); const clearAutocomplete = useCallback5(() => { clearAutocompleteData(); autocompletePromiseRef.current = void 0; }, [autocompletePromiseRef, clearAutocompleteData]); const executeQuery = useCallback5(() => { if (!hideRecentSearches) { const input = searchActions.state.query.input; input && setRecentSearch(input); } executeQueryWithNearMeHandling(); }, [ searchActions.state.query.input, executeQueryWithNearMeHandling, hideRecentSearches, setRecentSearch ]); const handleSubmit = useCallback5((value, index, itemData) => { value !== void 0 && searchActions.setQuery(value); searchActions.setOffset(0); searchActions.setFacets([]); clearStaticRangeFilters(searchActions); if (itemData && isVerticalLink(itemData.verticalLink) && onSelectVerticalLink) { onSelectVerticalLink({ verticalLink: itemData.verticalLink, querySource: QuerySource.Autocomplete }); } else { executeQuery(); } if (typeof index === "number" && index >= 0 && !itemData?.isEntityPreview) { reportAnalyticsEvent("AUTO_COMPLETE_SELECTION", value); } }, [searchActions, executeQuery, onSelectVerticalLink, reportAnalyticsEvent]); const [ entityPreviewsState, executeEntityPreviewsQuery ] = useEntityPreviews(entityPreviewSearcher, entityPreviewsDebouncingTime); const { verticalKeyToResults, isLoading: entityPreviewsLoading } = entityPreviewsState; const entityPreviews = renderEntityPreviews?.( entityPreviewsLoading, verticalKeyToResults, { onClick: handleSubmit, ariaLabel: getAriaLabel } ); const updateEntityPreviews = useCallback5((query2) => { if (!renderEntityPreviews || !includedVerticals) { return; } executeEntityPreviewsQuery(query2, universalLimit ?? {}, includedVerticals); }, [executeEntityPreviewsQuery, renderEntityPreviews, includedVerticals, universalLimit]); const handleInputFocus = useCallback5((value = "") => { searchActions.setQuery(value); updateEntityPreviews(value); autocompletePromiseRef.current = executeAutocomplete2(); }, [searchActions, autocompletePromiseRef, executeAutocomplete2, updateEntityPreviews]); const handleInputChange = useCallback5((value = "") => { searchActions.setQuery(value); updateEntityPreviews(value); autocompletePromiseRef.current = executeAutocomplete2(); }, [searchActions, autocompletePromiseRef, executeAutocomplete2, updateEntityPreviews]); const handleClickClearButton = useCallback5(() => { updateEntityPreviews(""); searchActions.setQuery(""); reportAnalyticsEvent("SEARCH_CLEAR_BUTTON"); }, [handleSubmit, reportAnalyticsEvent, updateEntityPreviews]); function renderInput() { return /* @__PURE__ */ React14.createElement( DropdownInput, { className: cssClasses.inputElement, placeholder, onSubmit: handleSubmit, onFocus: handleInputFocus, onChange: handleInputChange, ariaLabel: "Conduct a search" } ); } function renderRecentSearches() { const recentSearchesCssClasses = { icon: cssClasses.recentSearchesIcon, option: cssClasses.recentSearchesOption, nonHighlighted: cssClasses.recentSearchesNonHighlighted }; return filteredRecentSearches?.map((result, i) => /* @__PURE__ */ React14.createElement( DropdownItem, { className: "flex items-center h-6.5 px-3.5 py-1.5 cursor-pointer hover:bg-gray-100", focusedClassName: twMerge("flex items-center h-6.5 px-3.5 py-1.5 cursor-pointer hover:bg-gray-100", cssClasses.focusedOption), key: i, value: result.query, onClick: handleSubmit }, renderAutocompleteResult( { value: result.query, inputIntents: [] }, recentSearchesCssClasses, HistoryIcon, `recent search: ${result.query}` ) )); } const itemDataMatrix = useMemo3(() => { return autocompleteResponse?.results.map((result) => { return result.verticalKeys?.map((verticalKey2) => ({ verticalLink: { verticalKey: verticalKey2, query: result.value } })) ?? []; }) ?? []; }, [autocompleteResponse?.results]); function renderQuerySuggestions() { return autocompleteResponse?.results.map((result, i) => /* @__PURE__ */ React14.createElement(Fragment, { key: i }, /* @__PURE__ */ React14.createElement( DropdownItem, { className: "flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100", focusedClassName: twMerge("flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100", cssClasses.focusedOption), value: result.value, onClick: handleSubmit }, renderAutocompleteResult( result, cssClasses, MagnifyingGlassIcon, `autocomplete suggestion: ${result.value}` ) ), showVerticalLinks && !isVertical && result.verticalKeys?.map((verticalKey2, j) => /* @__PURE__ */ React14.createElement( DropdownItem, { key: j, className: "flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100", focusedClassName: twMerge("flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100", cssClasses.focusedOption), value: result.value, itemData: itemDataMatrix[i][j], onClick: handleSubmit }, renderAutocompleteResult( { value: `in ${verticalKeyToLabel ? verticalKeyToLabel(verticalKey2) : verticalKey2}`, inputIntents: [] }, { ...cssClasses, option: cssClasses.verticalLink } ) )))); } function renderClearButton() { return /* @__PURE__ */ React14.createElement(React14.Fragment, null, /* @__PURE__ */ React14.createElement( "button", { "aria-label": "Clear the search bar", className: cssClasses.clearButton, onClick: handleClickClearButton }, /* @__PURE__ */ React14.createElement(CloseIcon, null) ), /* @__PURE__ */ React14.createElement(VerticalDividerIcon, { className: cssClasses.verticalDivider })); } const entityPreviewsCount = calculateEntityPreviewsCount(entityPreviews); const showEntityPreviewsDivider = entityPreviews && !!(autocompleteResponse?.results.length || filteredRecentSearches?.length); const hasItems = !!(autocompleteResponse?.results.length || filteredRecentSearches?.length || entityPreviews); const screenReaderText = getScreenReaderText( autocompleteResponse?.results.length, filteredRecentSearches?.length, entityPreviewsCount ); const activeClassName = classNames("relative z-10 bg-white border rounded-3xl border-gray-200 w-full overflow-hidden", { ["shadow-lg"]: hasItems }); const handleToggleDropdown = useCallback5((isActive) => { if (!isActive) { clearAutocomplete(); } }, [clearAutocomplete]); return /* @__PURE__ */ React14.createElement("div", { className: cssClasses.searchBarContainer }, /* @__PURE__ */ React14.createElement( Dropdown, { className: "relative bg-white border rounded-3xl border-gray-200 w-full overflow-hidden", activeClassName, screenReaderText, parentQuery: query, onToggle: handleToggleDropdown }, /* @__PURE__ */ React14.createElement("div", { className: "inline-flex items-center justify-between w-full" }, renderInput(), query && renderClearButton(), /* @__PURE__ */ React14.createElement( DropdownSearchButton, { handleSubmit, cssClasses } )), hasItems && /* @__PURE__ */ React14.createElement(StyledDropdownMenu, { cssClasses }, renderRecentSearches(), renderQuerySuggestions(), showEntityPreviewsDivider && /* @__PURE__ */ React14.createElement("div", { className: cssClasses.entityPreviewsDivider }), entityPreviews) )); } function StyledDropdownMenu({ cssClasses, children }) { return /* @__PURE__ */ React14.createElement(DropdownMenu, null, /* @__PURE__ */ React14.createElement("div", { className: cssClasses.inputDivider }), /* @__PURE__ */ React14.createElement("div", { className: "bg-white py-4" }, children)); } function getScreenReaderText(autocompleteOptions = 0, recentSearchesOptions = 0, entityPreviewsCount = 0) { const recentSearchesText = recentSearchesOptions > 0 ? processTranslation({ phrase: `${recentSearchesOptions} recent search found.`, pluralForm: `${recentSearchesOptions} recent searches found.`, count: recentSearchesOptions }) : ""; const entityPreviewsText = entityPreviewsCount > 0 ? " " + processTranslation({ phrase: `${entityPreviewsCount} result preview found.`, pluralForm: `${entityPreviewsCount} result previews found.`, count: entityPreviewsCount }) : ""; const autocompleteText = autocompleteOptions > 0 ? " " + processTranslation({ phrase: `${autocompleteOptions} autocomplete suggestion found.`, pluralForm: `${autocompleteOptions} autocomplete suggestions found.`, count: autocompleteOptions }) : ""; const text = recentSearchesText + autocompleteText + entityPreviewsText; if (text === "") { return processTranslation({ phrase: "0 autocomplete suggestion found.", pluralForm: "0 autocomplete suggestions found.", count: 0 }); } return text.trim(); } function DropdownSearchButton({ handleSubmit, cssClasses }) { const { toggleDropdown } = useDropdownContext(); const handleClick = useCallback5(() => { handleSubmit(); toggleDropdown(false); }, [handleSubmit, toggleDropdown]); return /* @__PURE__ */ React14.createElement("div", { className: cssClasses.searchButtonContainer }, /* @__PURE__ */ React14.createElement( SearchButton, { className: cssClasses.searchButton, handleClick } )); } function getAriaLabel(value) { return "result preview: " + value; } function calculateEntityPreviewsCount(children) { let count = 0; recursivelyMapChildren(children, (c) => { if (isValidElement3(c) && c.type === DropdownItem) { count++; } return c; }); return count; } // src/components/SpellCheck.tsx import { useSearchState as useSearchState4, useSearchAct