diff --git a/docs/src/theme/SearchBar/index.js b/docs/src/theme/SearchBar/index.js
new file mode 100644
index 00000000000..923a9cfac86
--- /dev/null
+++ b/docs/src/theme/SearchBar/index.js
@@ -0,0 +1,202 @@
+import React, {useCallback, useMemo, useRef, useState} from 'react';
+import {createPortal} from 'react-dom';
+import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
+import Head from '@docusaurus/Head';
+import Link from '@docusaurus/Link';
+import {useHistory} from '@docusaurus/router';
+import {
+ isRegexpStringMatch,
+ useSearchLinkCreator,
+} from '@docusaurus/theme-common';
+import {
+ useAlgoliaContextualFacetFilters,
+ useSearchResultUrlProcessor,
+} from '@docusaurus/theme-search-algolia/client';
+import Translate from '@docusaurus/Translate';
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import translations from '@theme/SearchTranslations';
+let DocSearchModal = null;
+function Hit({hit, children}) {
+ if (hit.url.includes('/api_reference/')) {
+ return (
+
+ {children}
+
+ )
+ }
+ return {children};
+}
+function ResultsFooter({state, onClose}) {
+ const createSearchLink = useSearchLinkCreator();
+ return (
+
+
+ {'See all {count} results'}
+
+
+ );
+}
+function mergeFacetFilters(f1, f2) {
+ const normalize = (f) => (typeof f === 'string' ? [f] : f);
+ return [...normalize(f1), ...normalize(f2)];
+}
+function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
+ const {siteMetadata} = useDocusaurusContext();
+ const processSearchResultUrl = useSearchResultUrlProcessor();
+ const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
+ const configFacetFilters = props.searchParameters?.facetFilters ?? [];
+ const facetFilters = contextualSearch
+ ? // Merge contextual search filters with config filters
+ mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters)
+ : // ... or use config facetFilters
+ configFacetFilters;
+ // We let user override default searchParameters if she wants to
+ const searchParameters = {
+ ...props.searchParameters,
+ facetFilters,
+ };
+ const history = useHistory();
+ const searchContainer = useRef(null);
+ const searchButtonRef = useRef(null);
+ const [isOpen, setIsOpen] = useState(false);
+ const [initialQuery, setInitialQuery] = useState(undefined);
+ const importDocSearchModalIfNeeded = useCallback(() => {
+ if (DocSearchModal) {
+ return Promise.resolve();
+ }
+ return Promise.all([
+ import('@docsearch/react/modal'),
+ import('@docsearch/react/style'),
+ import('./styles.css'),
+ ]).then(([{DocSearchModal: Modal}]) => {
+ DocSearchModal = Modal;
+ });
+ }, []);
+ const prepareSearchContainer = useCallback(() => {
+ if (!searchContainer.current) {
+ const divElement = document.createElement('div');
+ searchContainer.current = divElement;
+ document.body.insertBefore(divElement, document.body.firstChild);
+ }
+ }, []);
+ const openModal = useCallback(() => {
+ prepareSearchContainer();
+ importDocSearchModalIfNeeded().then(() => setIsOpen(true));
+ }, [importDocSearchModalIfNeeded, prepareSearchContainer]);
+ const closeModal = useCallback(() => {
+ setIsOpen(false);
+ searchButtonRef.current?.focus();
+ }, []);
+ const handleInput = useCallback(
+ (event) => {
+ if (event.key === 'f' && (event.metaKey || event.ctrlKey)) {
+ // ignore browser's ctrl+f
+ return;
+ }
+ // prevents duplicate key insertion in the modal input
+ event.preventDefault();
+ setInitialQuery(event.key);
+ openModal();
+ },
+ [openModal],
+ );
+ const navigator = useRef({
+ navigate({itemUrl}) {
+ // Algolia results could contain URL's from other domains which cannot
+ // be served through history and should navigate with window.location
+ if (isRegexpStringMatch(externalUrlRegex, itemUrl) || itemUrl.includes('/api_reference/')) {
+ window.location.href = itemUrl;
+ } else {
+ history.push(itemUrl);
+ }
+ },
+ }).current;
+ const transformItems = useRef((items) =>
+ props.transformItems
+ ? // Custom transformItems
+ props.transformItems(items)
+ : // Default transformItems
+ items.map((item) => ({
+ ...item,
+ url: processSearchResultUrl(item.url),
+ })),
+ ).current;
+ const resultsFooterComponent = useMemo(
+ () =>
+ // eslint-disable-next-line react/no-unstable-nested-components
+ (footerProps) =>
+ ,
+ [closeModal],
+ );
+ const transformSearchClient = useCallback(
+ (searchClient) => {
+ searchClient.addAlgoliaAgent(
+ 'docusaurus',
+ siteMetadata.docusaurusVersion,
+ );
+ return searchClient;
+ },
+ [siteMetadata.docusaurusVersion],
+ );
+ useDocSearchKeyboardEvents({
+ isOpen,
+ onOpen: openModal,
+ onClose: closeModal,
+ onInput: handleInput,
+ searchButtonRef,
+ });
+ 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.current &&
+ createPortal(
+ ,
+ searchContainer.current,
+ )}
+ >
+ );
+}
+export default function SearchBar() {
+ const {siteConfig} = useDocusaurusContext();
+ return ;
+}
diff --git a/docs/src/theme/SearchBar/styles.css b/docs/src/theme/SearchBar/styles.css
new file mode 100644
index 00000000000..fdf8dff9a4a
--- /dev/null
+++ b/docs/src/theme/SearchBar/styles.css
@@ -0,0 +1,14 @@
+:root {
+ --docsearch-primary-color: var(--ifm-color-primary);
+ --docsearch-text-color: var(--ifm-font-color-base);
+}
+
+.DocSearch-Button {
+ margin: 0;
+ transition: all var(--ifm-transition-fast)
+ var(--ifm-transition-timing-default);
+}
+
+.DocSearch-Container {
+ z-index: calc(var(--ifm-z-index-fixed) + 1);
+}
diff --git a/docs/src/theme/SearchPage/index.js b/docs/src/theme/SearchPage/index.js
new file mode 100644
index 00000000000..7a5da716737
--- /dev/null
+++ b/docs/src/theme/SearchPage/index.js
@@ -0,0 +1,464 @@
+/* eslint-disable jsx-a11y/no-autofocus */
+import React, {useEffect, useReducer, useRef, useState} from 'react';
+import clsx from 'clsx';
+import algoliaSearchHelper from 'algoliasearch-helper';
+import algoliaSearch from 'algoliasearch/lite';
+import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
+import Head from '@docusaurus/Head';
+import Link from '@docusaurus/Link';
+import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
+import {
+ HtmlClassNameProvider,
+ useEvent,
+ usePluralForm,
+ useSearchQueryString,
+} from '@docusaurus/theme-common';
+import {useTitleFormatter} from '@docusaurus/theme-common/internal';
+import Translate, {translate} from '@docusaurus/Translate';
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import {
+ useAlgoliaThemeConfig,
+ useSearchResultUrlProcessor,
+} from '@docusaurus/theme-search-algolia/client';
+import Layout from '@theme/Layout';
+import Heading from '@theme/Heading';
+import styles from './styles.module.css';
+// Very simple pluralization: probably good enough for now
+function useDocumentsFoundPlural() {
+ const {selectMessage} = usePluralForm();
+ return (count) =>
+ selectMessage(
+ count,
+ translate(
+ {
+ id: 'theme.SearchPage.documentsFound.plurals',
+ description:
+ 'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
+ message: 'One document found|{count} documents found',
+ },
+ {count},
+ ),
+ );
+}
+function useDocsSearchVersionsHelpers() {
+ const allDocsData = useAllDocsData();
+ // State of the version select menus / algolia facet filters
+ // docsPluginId -> versionName map
+ const [searchVersions, setSearchVersions] = useState(() =>
+ Object.entries(allDocsData).reduce(
+ (acc, [pluginId, pluginData]) => ({
+ ...acc,
+ [pluginId]: pluginData.versions[0].name,
+ }),
+ {},
+ ),
+ );
+ // Set the value of a single select menu
+ const setSearchVersion = (pluginId, searchVersion) =>
+ setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
+ const versioningEnabled = Object.values(allDocsData).some(
+ (docsData) => docsData.versions.length > 1,
+ );
+ return {
+ allDocsData,
+ versioningEnabled,
+ searchVersions,
+ setSearchVersion,
+ };
+}
+// We want to display one select per versioned docs plugin instance
+function SearchVersionSelectList({docsSearchVersionsHelpers}) {
+ const versionedPluginEntries = Object.entries(
+ docsSearchVersionsHelpers.allDocsData,
+ )
+ // Do not show a version select for unversioned docs plugin instances
+ .filter(([, docsData]) => docsData.versions.length > 1);
+ return (
+
+ {versionedPluginEntries.map(([pluginId, docsData]) => {
+ const labelPrefix =
+ versionedPluginEntries.length > 1 ? `${pluginId}: ` : '';
+ return (
+
+ );
+ })}
+
+ );
+}
+function SearchPageContent() {
+ const {
+ i18n: {currentLocale},
+ } = useDocusaurusContext();
+ const {
+ algolia: {appId, apiKey, indexName, contextualSearch},
+ } = useAlgoliaThemeConfig();
+ const processSearchResultUrl = useSearchResultUrlProcessor();
+ const documentsFoundPlural = useDocumentsFoundPlural();
+ const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
+ const [searchQuery, setSearchQuery] = useSearchQueryString();
+ const initialSearchResultState = {
+ items: [],
+ query: null,
+ totalResults: null,
+ totalPages: null,
+ lastPage: null,
+ hasMore: null,
+ loading: null,
+ };
+ const [searchResultState, searchResultStateDispatcher] = useReducer(
+ (prevState, data) => {
+ switch (data.type) {
+ case 'reset': {
+ return initialSearchResultState;
+ }
+ case 'loading': {
+ return {...prevState, loading: true};
+ }
+ case 'update': {
+ if (searchQuery !== data.value.query) {
+ return prevState;
+ }
+ return {
+ ...data.value,
+ items:
+ data.value.lastPage === 0
+ ? data.value.items
+ : prevState.items.concat(data.value.items),
+ };
+ }
+ case 'advance': {
+ const hasMore = prevState.totalPages > prevState.lastPage + 1;
+ return {
+ ...prevState,
+ lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage,
+ hasMore,
+ };
+ }
+ default:
+ return prevState;
+ }
+ },
+ initialSearchResultState,
+ );
+ // respect settings from the theme config for facets
+ const disjunctiveFacets = contextualSearch
+ ? ['language', 'docusaurus_tag']
+ : [];
+ const algoliaClient = algoliaSearch(appId, apiKey);
+ const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: why errors happens after upgrading to TS 5.5 ?
+ hitsPerPage: 15,
+ advancedSyntax: true,
+ disjunctiveFacets,
+ });
+ algoliaHelper.on(
+ 'result',
+ ({results: {query, hits, page, nbHits, nbPages}}) => {
+ if (query === '' || !Array.isArray(hits)) {
+ searchResultStateDispatcher({type: 'reset'});
+ return;
+ }
+ const sanitizeValue = (value) =>
+ value.replace(
+ /algolia-docsearch-suggestion--highlight/g,
+ 'search-result-match',
+ );
+ const items = hits.map(
+ ({
+ url,
+ _highlightResult: {hierarchy},
+ _snippetResult: snippet = {},
+ }) => {
+ const titles = Object.keys(hierarchy).map((key) =>
+ sanitizeValue(hierarchy[key].value),
+ );
+ return {
+ title: titles.pop(),
+ url: processSearchResultUrl(url),
+ summary: snippet.content
+ ? `${sanitizeValue(snippet.content.value)}...`
+ : '',
+ breadcrumbs: titles,
+ };
+ },
+ );
+ searchResultStateDispatcher({
+ type: 'update',
+ value: {
+ items,
+ query,
+ totalResults: nbHits,
+ totalPages: nbPages,
+ lastPage: page,
+ hasMore: nbPages > page + 1,
+ loading: false,
+ },
+ });
+ },
+ );
+ const [loaderRef, setLoaderRef] = useState(null);
+ const prevY = useRef(0);
+ const observer = useRef(
+ ExecutionEnvironment.canUseIntersectionObserver &&
+ new IntersectionObserver(
+ (entries) => {
+ const {
+ isIntersecting,
+ boundingClientRect: {y: currentY},
+ } = entries[0];
+ if (isIntersecting && prevY.current > currentY) {
+ searchResultStateDispatcher({type: 'advance'});
+ }
+ prevY.current = currentY;
+ },
+ {threshold: 1},
+ ),
+ );
+ const getTitle = () =>
+ searchQuery
+ ? translate(
+ {
+ id: 'theme.SearchPage.existingResultsTitle',
+ message: 'Search results for "{query}"',
+ description: 'The search page title for non-empty query',
+ },
+ {
+ query: searchQuery,
+ },
+ )
+ : translate({
+ id: 'theme.SearchPage.emptyResultsTitle',
+ message: 'Search the documentation',
+ description: 'The search page title for empty query',
+ });
+ const makeSearch = useEvent((page = 0) => {
+ if (contextualSearch) {
+ algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
+ algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
+ Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(
+ ([pluginId, searchVersion]) => {
+ algoliaHelper.addDisjunctiveFacetRefinement(
+ 'docusaurus_tag',
+ `docs-${pluginId}-${searchVersion}`,
+ );
+ },
+ );
+ }
+ algoliaHelper.setQuery(searchQuery).setPage(page).search();
+ });
+ useEffect(() => {
+ if (!loaderRef) {
+ return undefined;
+ }
+ const currentObserver = observer.current;
+ if (currentObserver) {
+ currentObserver.observe(loaderRef);
+ return () => currentObserver.unobserve(loaderRef);
+ }
+ return () => true;
+ }, [loaderRef]);
+ useEffect(() => {
+ searchResultStateDispatcher({type: 'reset'});
+ if (searchQuery) {
+ searchResultStateDispatcher({type: 'loading'});
+ setTimeout(() => {
+ makeSearch();
+ }, 300);
+ }
+ }, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]);
+ useEffect(() => {
+ if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
+ return;
+ }
+ makeSearch(searchResultState.lastPage);
+ }, [makeSearch, searchResultState.lastPage]);
+ return (
+
+
+ {useTitleFormatter(getTitle())}
+ {/*
+ We should not index search pages
+ See https://github.com/facebook/docusaurus/pull/3233
+ */}
+
+
+
+
+
{getTitle()}
+
+
+
+
+
+ {!!searchResultState.totalResults &&
+ documentsFoundPlural(searchResultState.totalResults)}
+
+
+
+
+
+ {searchResultState.items.length > 0 ? (
+
+ {searchResultState.items.map(
+ ({title, url, summary, breadcrumbs}, i) => (
+
+
+ {url.includes('/api_reference/') ? : }
+
+
+ {breadcrumbs.length > 0 && (
+
+ )}
+
+ {summary && (
+
+ )}
+
+ ),
+ )}
+
+ ) : (
+ [
+ searchQuery && !searchResultState.loading && (
+
+
+ No results were found
+
+
+ ),
+ !!searchResultState.loading && (
+
+ ),
+ ]
+ )}
+
+ {searchResultState.hasMore && (
+
+
+ Fetching new results...
+
+
+ )}
+
+
+ );
+}
+export default function SearchPage() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/theme/SearchPage/styles.module.css b/docs/src/theme/SearchPage/styles.module.css
new file mode 100644
index 00000000000..57de7498db6
--- /dev/null
+++ b/docs/src/theme/SearchPage/styles.module.css
@@ -0,0 +1,112 @@
+.searchQueryInput,
+.searchVersionInput {
+ border-radius: var(--ifm-global-radius);
+ border: 2px solid var(--ifm-toc-border-color);
+ font: var(--ifm-font-size-base) var(--ifm-font-family-base);
+ padding: 0.8rem;
+ width: 100%;
+ background: var(--docsearch-searchbox-focus-background);
+ color: var(--docsearch-text-color);
+ margin-bottom: 0.5rem;
+ transition: border var(--ifm-transition-fast) ease;
+}
+
+.searchQueryInput:focus,
+.searchVersionInput:focus {
+ border-color: var(--docsearch-primary-color);
+ outline: none;
+}
+
+.searchQueryInput::placeholder {
+ color: var(--docsearch-muted-color);
+}
+
+.searchResultsColumn {
+ font-size: 0.9rem;
+ font-weight: bold;
+}
+
+.algoliaLogo {
+ max-width: 150px;
+}
+
+.algoliaLogoPathFill {
+ fill: var(--ifm-font-color-base);
+}
+
+.searchResultItem {
+ padding: 1rem 0;
+ border-bottom: 1px solid var(--ifm-toc-border-color);
+}
+
+.searchResultItemHeading {
+ font-weight: 400;
+ margin-bottom: 0;
+}
+
+.searchResultItemPath {
+ font-size: 0.8rem;
+ color: var(--ifm-color-content-secondary);
+ --ifm-breadcrumb-separator-size-multiplier: 1;
+}
+
+.searchResultItemSummary {
+ margin: 0.5rem 0 0;
+ font-style: italic;
+}
+
+@media only screen and (max-width: 996px) {
+ .searchQueryColumn {
+ max-width: 60% !important;
+ }
+
+ .searchVersionColumn {
+ max-width: 40% !important;
+ }
+
+ .searchResultsColumn {
+ max-width: 60% !important;
+ }
+
+ .searchLogoColumn {
+ max-width: 40% !important;
+ padding-left: 0 !important;
+ }
+}
+
+@media screen and (max-width: 576px) {
+ .searchQueryColumn {
+ max-width: 100% !important;
+ }
+
+ .searchVersionColumn {
+ max-width: 100% !important;
+ padding-left: var(--ifm-spacing-horizontal) !important;
+ }
+}
+
+.loadingSpinner {
+ width: 3rem;
+ height: 3rem;
+ border: 0.4em solid #eee;
+ border-top-color: var(--ifm-color-primary);
+ border-radius: 50%;
+ animation: loading-spin 1s linear infinite;
+ margin: 0 auto;
+}
+
+@keyframes loading-spin {
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.loader {
+ margin-top: 2rem;
+}
+
+:global(.search-result-match) {
+ color: var(--docsearch-hit-color);
+ background: rgb(255 215 142 / 25%);
+ padding: 0.09em 0;
+}