diff --git a/src/theme/SearchBar/index.js b/src/theme/SearchBar/index.js index 7e9ba1b1aea..595140b1c5e 100644 --- a/src/theme/SearchBar/index.js +++ b/src/theme/SearchBar/index.js @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; import { DocSearchButton, useDocSearchKeyboardEvents } from '@docsearch/react'; import Head from '@docusaurus/Head'; import { useHistory } from '@docusaurus/router'; @@ -9,7 +9,6 @@ import { import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { createPortal } from 'react-dom'; import translations from '@theme/SearchTranslations'; -import { useEffect } from 'react'; import {useAskAI} from '@site/src/hooks/useAskAI' import { shouldPreventSearchAction, handleSearchKeyboardConflict } from './utils/aiConflictHandler'; import { initializeSearchAnalytics, createEnhancedSearchClient } from './utils/searchAnalytics'; @@ -25,17 +24,17 @@ import { DocTypeSelector } from './docTypeSelector'; function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { const queryIDRef = useRef(null); + const lastQueryRef = useRef(''); const { siteMetadata, i18n: { currentLocale } } = useDocusaurusContext(); const processSearchResultUrl = useSearchResultUrlProcessor(); const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters(); - const { isAskAIOpen, currentMode } = useAskAI(); + const { isAskAIOpen } = useAskAI(); const history = useHistory(); const searchButtonRef = useRef(null); - // Doc type filtering state const [selectedDocTypes, setSelectedDocTypes] = useState(null); + const searchParametersRef = useRef(null); - // Use the modal management hook const { isOpen, initialQuery, @@ -47,26 +46,63 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { importDocSearchModalIfNeeded } = useDocSearchModal(); - // Configure search parameters with doc_type filter - const searchParameters = createSearchParameters( - props, - contextualSearch, - contextualSearchFacetFilters, - selectedDocTypes - ); + // Update searchParameters ref instead of creating new object + useEffect(() => { + const newParams = createSearchParameters( + props, + contextualSearch, + contextualSearchFacetFilters, + selectedDocTypes + ); + + if (!searchParametersRef.current) { + searchParametersRef.current = newParams; + } else { + Object.keys(newParams).forEach(key => { + searchParametersRef.current[key] = newParams[key]; + }); + } + }, [props, contextualSearch, contextualSearchFacetFilters, selectedDocTypes]); + + // Initialize on mount + if (!searchParametersRef.current) { + searchParametersRef.current = createSearchParameters( + props, + contextualSearch, + contextualSearchFacetFilters, + selectedDocTypes + ); + } + + // Track input changes to capture the query + useEffect(() => { + if (!isOpen) return; + + const handleInput = (e) => { + const input = e.target; + if (input.classList.contains('DocSearch-Input')) { + lastQueryRef.current = input.value; + } + }; + + document.addEventListener('input', handleInput, true); + return () => document.removeEventListener('input', handleInput, true); + }, [isOpen]); useEffect(() => { initializeSearchAnalytics(props.appId, props.apiKey); }, [props.appId, props.apiKey]); - // Create navigator for handling result clicks const navigator = useMemo( () => createSearchNavigator(history, externalUrlRegex), [history, externalUrlRegex] ); - // Transform search items with metadata const transformItems = useCallback((items, state) => { + if (state?.query) { + lastQueryRef.current = state.query; + } + return transformSearchItems(items, { transformItems: props.transformItems, processSearchResultUrl, @@ -75,36 +111,74 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { }); }, [props.transformItems, processSearchResultUrl, currentLocale]); - const handleDocTypeChange = useCallback((docTypes) => { - setSelectedDocTypes(docTypes); - }, []); +const handleDocTypeChange = useCallback((docTypes) => { + setSelectedDocTypes(docTypes); + + // Re-trigger search with updated filters after state update completes + setTimeout(() => { + const input = document.querySelector('.DocSearch-Input'); + const query = lastQueryRef.current; + + if (input && query) { + // Access React's internal value setter to bypass readonly property + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + ).set; + + // Clear input to trigger change detection + nativeInputValueSetter.call(input, ''); + input.dispatchEvent(new Event('input', { bubbles: true })); + + // Restore original query to execute search with new filters + setTimeout(() => { + nativeInputValueSetter.call(input, query); + input.dispatchEvent(new Event('input', { bubbles: true })); + input.focus(); + }, 0); + } + }, 100); +}, []); const resultsFooterComponent = useMemo( - () => - // eslint-disable-next-line react/no-unstable-nested-components - (footerProps) => - , - [onClose], + () => (footerProps) => , + [onClose] ); const transformSearchClient = useCallback((searchClient) => { - return createEnhancedSearchClient(searchClient, siteMetadata.docusaurusVersion, queryIDRef); + const enhancedClient = createEnhancedSearchClient( + searchClient, + siteMetadata.docusaurusVersion, + queryIDRef + ); + + const originalSearch = enhancedClient.search.bind(enhancedClient); + + let debounceTimeout; + enhancedClient.search = (...args) => { + return new Promise((resolve, reject) => { + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(() => { + originalSearch(...args) + .then(resolve) + .catch(reject); + }, 200); + }); + }; + + return enhancedClient; }, [siteMetadata.docusaurusVersion]); const handleOnOpen = useCallback(() => { - console.log('handleOnOpen called', { isAskAIOpen }); - if (shouldPreventSearchAction(isAskAIOpen)) { - console.log('handleOnOpen - preventing search modal'); return; } - onOpen(); }, [isAskAIOpen, onOpen]); const handleOnInput = useCallback((event) => { if (shouldPreventSearchAction(isAskAIOpen)) { - return; // Prevent search input handling + return; } onInput(event); }, [isAskAIOpen, onInput]); @@ -118,68 +192,64 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { }); return ( - <> - - {/* This hints the browser that the website will load data from Algolia, - and allows it to preconnect to the DocSearch cluster. It makes the first - query faster, especially on mobile. */} - - - - + + + - {isOpen && - DocSearchModal && - searchContainer && - createPortal( - <> - - - {/* Selector positioned as overlay */} -
- -
- , - searchContainer, - )} - + + + {isOpen && + DocSearchModal && + searchContainer && + createPortal( + <> + + +
+ +
+ , + searchContainer + )} + ); } @@ -192,12 +262,8 @@ export default function SearchBar() { handleSearchKeyboardConflict(event, isAskAIOpen); }; - // Add listener with capture phase to intercept before DocSearch document.addEventListener('keydown', handleKeyDown, true); - - return () => { - document.removeEventListener('keydown', handleKeyDown, true); - }; + return () => document.removeEventListener('keydown', handleKeyDown, true); }, [isAskAIOpen]); return ;