@docusaurus/theme-search-algolia 2.0.0-beta.1 → 2.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/copyUntypedFiles.js +20 -0
  2. package/lib/index.d.ts +11 -0
  3. package/lib/index.js +85 -0
  4. package/lib/templates/opensearch.d.ts +8 -0
  5. package/lib/templates/opensearch.js +23 -0
  6. package/lib/theme/SearchBar/index.d.ts +9 -0
  7. package/lib/theme/SearchBar/index.js +153 -0
  8. package/lib/theme/SearchBar/styles.css +21 -0
  9. package/lib/theme/SearchBar/styles.module.css +20 -0
  10. package/lib/theme/SearchMetadata/index.d.ts +10 -0
  11. package/lib/theme/SearchMetadata/index.js +20 -0
  12. package/lib/theme/SearchPage/index.d.ts +9 -0
  13. package/lib/theme/SearchPage/index.js +299 -0
  14. package/lib/theme/SearchPage/styles.module.css +119 -0
  15. package/lib/theme/hooks/useAlgoliaContextualFacetFilters.d.ts +8 -0
  16. package/lib/theme/hooks/useAlgoliaContextualFacetFilters.js +15 -0
  17. package/lib/theme/hooks/useSearchQuery.d.ts +9 -0
  18. package/lib/theme/hooks/useSearchQuery.js +43 -0
  19. package/lib/validateThemeConfig.d.ts +13 -0
  20. package/lib/validateThemeConfig.js +39 -0
  21. package/package.json +24 -12
  22. package/src/__tests__/validateThemeConfig.test.js +14 -0
  23. package/src/{index.js → index.ts} +37 -19
  24. package/src/templates/{opensearch.js → opensearch.ts} +4 -2
  25. package/src/theme/SearchBar/{index.js → index.tsx} +78 -26
  26. package/src/theme/{SearchMetadatas/index.js → SearchMetadata/index.tsx} +9 -2
  27. package/src/theme/SearchPage/{index.js → index.tsx} +81 -51
  28. package/src/theme/hooks/{useAlgoliaContextualFacetFilters.js → useAlgoliaContextualFacetFilters.ts} +4 -3
  29. package/src/theme/hooks/useSearchQuery.ts +63 -0
  30. package/src/theme-search-algolia.d.ts +47 -0
  31. package/src/types.d.ts +10 -0
  32. package/src/{validateThemeConfig.js → validateThemeConfig.ts} +10 -7
  33. package/tsconfig.browser.json +8 -0
  34. package/tsconfig.json +9 -0
  35. package/tsconfig.server.json +4 -0
  36. package/src/theme/hooks/useSearchQuery.js +0 -44
@@ -5,29 +5,38 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- const path = require('path');
9
- const fs = require('fs');
10
- const eta = require('eta');
11
- const {normalizeUrl, getSwizzledComponent} = require('@docusaurus/utils');
12
- const openSearchTemplate = require('./templates/opensearch');
13
- const {validateThemeConfig} = require('./validateThemeConfig');
14
- const {memoize} = require('lodash');
8
+ import path from 'path';
9
+ import fs from 'fs';
10
+ import {defaultConfig, compile} from 'eta';
11
+ import {normalizeUrl, getSwizzledComponent} from '@docusaurus/utils';
12
+ import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
13
+ import openSearchTemplate from './templates/opensearch';
14
+ import {memoize} from 'lodash';
15
15
 
16
- const getCompiledOpenSearchTemplate = memoize(() => {
17
- return eta.compile(openSearchTemplate.trim());
18
- });
16
+ import type {DocusaurusContext, Plugin} from '@docusaurus/types';
19
17
 
20
- function renderOpenSearchTemplate(data) {
18
+ const getCompiledOpenSearchTemplate = memoize(() =>
19
+ compile(openSearchTemplate.trim()),
20
+ );
21
+
22
+ function renderOpenSearchTemplate(data: {
23
+ title: string;
24
+ url: string;
25
+ favicon: string | null;
26
+ }) {
21
27
  const compiled = getCompiledOpenSearchTemplate();
22
- return compiled(data, eta.defaultConfig);
28
+ return compiled(data, defaultConfig);
23
29
  }
24
30
 
25
31
  const OPEN_SEARCH_FILENAME = 'opensearch.xml';
26
32
 
27
- function theme(context) {
33
+ export default function theme(
34
+ context: DocusaurusContext & {baseUrl: string},
35
+ ): Plugin<void> {
28
36
  const {
29
37
  baseUrl,
30
38
  siteConfig: {title, url, favicon},
39
+ i18n: {currentLocale},
31
40
  } = context;
32
41
  const pageComponent = './theme/SearchPage/index.js';
33
42
  const pagePath =
@@ -37,12 +46,23 @@ function theme(context) {
37
46
  return {
38
47
  name: 'docusaurus-theme-search-algolia',
39
48
 
49
+ getPathsToWatch() {
50
+ return [pagePath];
51
+ },
52
+
40
53
  getThemePath() {
41
54
  return path.resolve(__dirname, './theme');
42
55
  },
43
56
 
44
- getPathsToWatch() {
45
- return [pagePath];
57
+ getTypeScriptThemePath() {
58
+ return path.resolve(__dirname, '..', 'src', 'theme');
59
+ },
60
+
61
+ getDefaultCodeTranslationMessages() {
62
+ return readDefaultCodeTranslationMessages({
63
+ locale: currentLocale,
64
+ name: 'theme-search-algolia',
65
+ });
46
66
  },
47
67
 
48
68
  async contentLoaded({actions: {addRoute}}) {
@@ -60,7 +80,7 @@ function theme(context) {
60
80
  renderOpenSearchTemplate({
61
81
  title,
62
82
  url: url + baseUrl,
63
- favicon: normalizeUrl([url, baseUrl, favicon]),
83
+ favicon: favicon ? normalizeUrl([url, baseUrl, favicon]) : null,
64
84
  }),
65
85
  );
66
86
  } catch (err) {
@@ -87,6 +107,4 @@ function theme(context) {
87
107
  };
88
108
  }
89
109
 
90
- module.exports = theme;
91
-
92
- theme.validateThemeConfig = validateThemeConfig;
110
+ export {validateThemeConfig} from './validateThemeConfig';
@@ -5,14 +5,16 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- module.exports = `
8
+ export default `
9
9
  <?xml version="1.0" encoding="UTF-8"?>
10
10
  <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
11
11
  xmlns:moz="http://www.mozilla.org/2006/browser/search/">
12
12
  <ShortName><%= it.title %></ShortName>
13
13
  <Description>Search <%= it.title %></Description>
14
14
  <InputEncoding>UTF-8</InputEncoding>
15
- <Image width="16" height="16" type="image/x-icon"><%= it.favicon %></Image>
15
+ <% if (it.favicon) { _%>
16
+ <Image width="16" height="16" type="image/x-icon"><%= it.favicon %></Image>
17
+ <% } _%>
16
18
  <Url type="text/html" method="get" template="<%= it.url %>search?q={searchTerms}"/>
17
19
  <Url type="application/opensearchdescription+xml" rel="self" template="<%= it.url %>opensearch.xml" />
18
20
  <moz:SearchForm><%= it.url %></moz:SearchForm>
@@ -4,6 +4,7 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
7
8
 
8
9
  import React, {useState, useRef, useCallback, useMemo} from 'react';
9
10
  import {createPortal} from 'react-dom';
@@ -13,18 +14,48 @@ import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
13
14
  import Link from '@docusaurus/Link';
14
15
  import Head from '@docusaurus/Head';
15
16
  import useSearchQuery from '@theme/hooks/useSearchQuery';
17
+ import {isRegexpStringMatch} from '@docusaurus/theme-common';
16
18
  import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
17
19
  import useAlgoliaContextualFacetFilters from '@theme/hooks/useAlgoliaContextualFacetFilters';
18
20
  import {translate} from '@docusaurus/Translate';
19
21
  import styles from './styles.module.css';
20
22
 
21
- let DocSearchModal = null;
23
+ import type {
24
+ DocSearchModal as DocSearchModalType,
25
+ DocSearchModalProps,
26
+ } from '@docsearch/react';
27
+ import type {
28
+ InternalDocSearchHit,
29
+ StoredDocSearchHit,
30
+ } from '@docsearch/react/dist/esm/types';
31
+ import type {AutocompleteState} from '@algolia/autocomplete-core';
22
32
 
23
- function Hit({hit, children}) {
33
+ type DocSearchProps = Omit<
34
+ DocSearchModalProps,
35
+ 'onClose' | 'initialScrollY'
36
+ > & {
37
+ contextualSearch?: string;
38
+ externalUrlRegex?: string;
39
+ };
40
+
41
+ let DocSearchModal: typeof DocSearchModalType | null = null;
42
+
43
+ function Hit({
44
+ hit,
45
+ children,
46
+ }: {
47
+ hit: InternalDocSearchHit | StoredDocSearchHit;
48
+ children: React.ReactNode;
49
+ }) {
24
50
  return <Link to={hit.url}>{children}</Link>;
25
51
  }
26
52
 
27
- function ResultsFooter({state, onClose}) {
53
+ type ResultsFooterProps = {
54
+ state: AutocompleteState<InternalDocSearchHit>;
55
+ onClose: () => void;
56
+ };
57
+
58
+ function ResultsFooter({state, onClose}: ResultsFooterProps) {
28
59
  const {generateSearchPageLink} = useSearchQuery();
29
60
 
30
61
  return (
@@ -34,7 +65,11 @@ function ResultsFooter({state, onClose}) {
34
65
  );
35
66
  }
36
67
 
37
- function DocSearch({contextualSearch, ...props}) {
68
+ function DocSearch({
69
+ contextualSearch,
70
+ externalUrlRegex,
71
+ ...props
72
+ }: DocSearchProps) {
38
73
  const {siteMetadata} = useDocusaurusContext();
39
74
 
40
75
  const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
@@ -55,10 +90,12 @@ function DocSearch({contextualSearch, ...props}) {
55
90
 
56
91
  const {withBaseUrl} = useBaseUrlUtils();
57
92
  const history = useHistory();
58
- const searchContainer = useRef(null);
59
- const searchButtonRef = useRef(null);
93
+ const searchContainer = useRef<HTMLDivElement | null>(null);
94
+ const searchButtonRef = useRef<HTMLButtonElement>(null);
60
95
  const [isOpen, setIsOpen] = useState(false);
61
- const [initialQuery, setInitialQuery] = useState(null);
96
+ const [initialQuery, setInitialQuery] = useState<string | undefined>(
97
+ undefined,
98
+ );
62
99
 
63
100
  const importDocSearchModalIfNeeded = useCallback(() => {
64
101
  if (DocSearchModal) {
@@ -66,7 +103,9 @@ function DocSearch({contextualSearch, ...props}) {
66
103
  }
67
104
 
68
105
  return Promise.all([
106
+ // @ts-ignore
69
107
  import('@docsearch/react/modal'),
108
+ // @ts-ignore
70
109
  import('@docsearch/react/style'),
71
110
  import('./styles.css'),
72
111
  ]).then(([{DocSearchModal: Modal}]) => {
@@ -87,7 +126,7 @@ function DocSearch({contextualSearch, ...props}) {
87
126
 
88
127
  const onClose = useCallback(() => {
89
128
  setIsOpen(false);
90
- searchContainer.current.remove();
129
+ searchContainer.current?.remove();
91
130
  }, [setIsOpen]);
92
131
 
93
132
  const onInput = useCallback(
@@ -101,28 +140,38 @@ function DocSearch({contextualSearch, ...props}) {
101
140
  );
102
141
 
103
142
  const navigator = useRef({
104
- navigate({itemUrl}) {
105
- history.push(itemUrl);
143
+ navigate({itemUrl}: {itemUrl?: string}) {
144
+ // Algolia results could contain URL's from other domains which cannot
145
+ // be served through history and should navigate with window.location
146
+ if (isRegexpStringMatch(externalUrlRegex, itemUrl)) {
147
+ window.location.href = itemUrl!;
148
+ } else {
149
+ history.push(itemUrl!);
150
+ }
106
151
  },
107
152
  }).current;
108
153
 
109
- const transformItems = useRef((items) => {
110
- return items.map((item) => {
111
- // We transform the absolute URL into a relative URL.
112
- // Alternatively, we can use `new URL(item.url)` but it's not
113
- // supported in IE.
114
- const a = document.createElement('a');
115
- a.href = item.url;
116
-
117
- return {
118
- ...item,
119
- url: withBaseUrl(`${a.pathname}${a.hash}`),
120
- };
121
- });
122
- }).current;
154
+ const transformItems = useRef<DocSearchModalProps['transformItems']>(
155
+ (items) =>
156
+ items.map((item) => {
157
+ // If Algolia contains a external domain, we should navigate without relative URL
158
+ if (isRegexpStringMatch(externalUrlRegex, item.url)) {
159
+ return item;
160
+ }
161
+
162
+ // We transform the absolute URL into a relative URL.
163
+ const url = new URL(item.url);
164
+ return {
165
+ ...item,
166
+ url: withBaseUrl(`${url.pathname}${url.hash}`),
167
+ };
168
+ }),
169
+ ).current;
123
170
 
124
171
  const resultsFooterComponent = useMemo(
125
- () => (footerProps) => <ResultsFooter {...footerProps} onClose={onClose} />,
172
+ // eslint-disable-next-line react/no-unstable-nested-components
173
+ () => (footerProps: ResultsFooterProps) =>
174
+ <ResultsFooter {...footerProps} onClose={onClose} />,
126
175
  [onClose],
127
176
  );
128
177
 
@@ -180,6 +229,8 @@ function DocSearch({contextualSearch, ...props}) {
180
229
  </div>
181
230
 
182
231
  {isOpen &&
232
+ DocSearchModal &&
233
+ searchContainer.current &&
183
234
  createPortal(
184
235
  <DocSearchModal
185
236
  onClose={onClose}
@@ -199,8 +250,9 @@ function DocSearch({contextualSearch, ...props}) {
199
250
  );
200
251
  }
201
252
 
202
- function SearchBar() {
253
+ function SearchBar(): JSX.Element {
203
254
  const {siteConfig} = useDocusaurusContext();
255
+ // @ts-ignore
204
256
  return <DocSearch {...siteConfig.themeConfig.algolia} />;
205
257
  }
206
258
 
@@ -8,9 +8,14 @@
8
8
  import React from 'react';
9
9
 
10
10
  import Head from '@docusaurus/Head';
11
+ import type {SearchMetadataProps} from '@theme/SearchMetadata';
11
12
 
12
- // Override default/agnostic SearchMetas to use Algolia-specific metadatas
13
- export default function AlgoliaSearchMetadatas({locale, version, tag}) {
13
+ // Override default/agnostic SearchMetas to use Algolia-specific metadata
14
+ function SearchMetadata({
15
+ locale,
16
+ version,
17
+ tag,
18
+ }: SearchMetadataProps): JSX.Element {
14
19
  // Seems safe to consider here the locale is the language,
15
20
  // as the existing docsearch:language filter is afaik a regular string-based filter
16
21
  const language = locale;
@@ -23,3 +28,5 @@ export default function AlgoliaSearchMetadatas({locale, version, tag}) {
23
28
  </Head>
24
29
  );
25
30
  }
31
+
32
+ export default SearchMetadata;
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  /* eslint-disable jsx-a11y/no-autofocus */
9
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
9
10
 
10
11
  import React, {useEffect, useState, useReducer, useRef} from 'react';
11
12
 
@@ -16,7 +17,12 @@ import clsx from 'clsx';
16
17
  import Head from '@docusaurus/Head';
17
18
  import Link from '@docusaurus/Link';
18
19
  import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
19
- import {useTitleFormatter, usePluralForm} from '@docusaurus/theme-common';
20
+ import {
21
+ useTitleFormatter,
22
+ usePluralForm,
23
+ isRegexpStringMatch,
24
+ useDynamicCallback,
25
+ } from '@docusaurus/theme-common';
20
26
  import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
21
27
  import {useAllDocsData} from '@theme/hooks/useDocs';
22
28
  import useSearchQuery from '@theme/hooks/useSearchQuery';
@@ -27,7 +33,7 @@ import styles from './styles.module.css';
27
33
  // Very simple pluralization: probably good enough for now
28
34
  function useDocumentsFoundPlural() {
29
35
  const {selectMessage} = usePluralForm();
30
- return (count) =>
36
+ return (count: number) =>
31
37
  selectMessage(
32
38
  count,
33
39
  translate(
@@ -47,14 +53,19 @@ function useDocsSearchVersionsHelpers() {
47
53
 
48
54
  // State of the version select menus / algolia facet filters
49
55
  // docsPluginId -> versionName map
50
- const [searchVersions, setSearchVersions] = useState(() => {
51
- return Object.entries(allDocsData).reduce((acc, [pluginId, pluginData]) => {
52
- return {...acc, [pluginId]: pluginData.versions[0].name};
53
- }, {});
54
- });
56
+ const [searchVersions, setSearchVersions] = useState<Record<string, string>>(
57
+ () =>
58
+ Object.entries(allDocsData).reduce(
59
+ (acc, [pluginId, pluginData]) => ({
60
+ ...acc,
61
+ [pluginId]: pluginData.versions[0].name,
62
+ }),
63
+ {},
64
+ ),
65
+ );
55
66
 
56
67
  // Set the value of a single select menu
57
- const setSearchVersion = (pluginId, searchVersion) =>
68
+ const setSearchVersion = (pluginId: string, searchVersion: string) =>
58
69
  setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
59
70
 
60
71
  const versioningEnabled = Object.values(allDocsData).some(
@@ -70,7 +81,11 @@ function useDocsSearchVersionsHelpers() {
70
81
  }
71
82
 
72
83
  // We want to display one select per versioned docs plugin instance
73
- const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
84
+ function SearchVersionSelectList({
85
+ docsSearchVersionsHelpers,
86
+ }: {
87
+ docsSearchVersionsHelpers: ReturnType<typeof useDocsSearchVersionsHelpers>;
88
+ }) {
74
89
  const versionedPluginEntries = Object.entries(
75
90
  docsSearchVersionsHelpers.allDocsData,
76
91
  )
@@ -111,13 +126,35 @@ const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
111
126
  })}
112
127
  </div>
113
128
  );
129
+ }
130
+
131
+ type ResultDispatcherState = {
132
+ items: {
133
+ title: string;
134
+ url: string;
135
+ summary: string;
136
+ breadcrumbs: string[];
137
+ }[];
138
+ query: string | null;
139
+ totalResults: number | null;
140
+ totalPages: number | null;
141
+ lastPage: number | null;
142
+ hasMore: boolean | null;
143
+ loading: boolean | null;
114
144
  };
115
145
 
116
- function SearchPage() {
146
+ type ResultDispatcher =
147
+ | {type: 'reset'; value?: undefined}
148
+ | {type: 'loading'; value?: undefined}
149
+ | {type: 'update'; value: ResultDispatcherState}
150
+ | {type: 'advance'; value?: undefined};
151
+
152
+ function SearchPage(): JSX.Element {
117
153
  const {
118
154
  siteConfig: {
119
155
  themeConfig: {
120
- algolia: {appId, apiKey, indexName},
156
+ // @ts-ignore
157
+ algolia: {appId, apiKey, indexName, externalUrlRegex},
121
158
  },
122
159
  },
123
160
  i18n: {currentLocale},
@@ -125,9 +162,8 @@ function SearchPage() {
125
162
  const documentsFoundPlural = useDocumentsFoundPlural();
126
163
 
127
164
  const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
128
- const {searchValue, updateSearchPath} = useSearchQuery();
129
- const [searchQuery, setSearchQuery] = useState(searchValue);
130
- const initialSearchResultState = {
165
+ const {searchQuery, setSearchQuery} = useSearchQuery();
166
+ const initialSearchResultState: ResultDispatcherState = {
131
167
  items: [],
132
168
  query: null,
133
169
  totalResults: null,
@@ -137,8 +173,8 @@ function SearchPage() {
137
173
  loading: null,
138
174
  };
139
175
  const [searchResultState, searchResultStateDispatcher] = useReducer(
140
- (prevState, {type, value: state}) => {
141
- switch (type) {
176
+ (prevState: ResultDispatcherState, data: ResultDispatcher) => {
177
+ switch (data.type) {
142
178
  case 'reset': {
143
179
  return initialSearchResultState;
144
180
  }
@@ -146,24 +182,24 @@ function SearchPage() {
146
182
  return {...prevState, loading: true};
147
183
  }
148
184
  case 'update': {
149
- if (searchQuery !== state.query) {
185
+ if (searchQuery !== data.value.query) {
150
186
  return prevState;
151
187
  }
152
188
 
153
189
  return {
154
- ...state,
190
+ ...data.value,
155
191
  items:
156
- state.lastPage === 0
157
- ? state.items
158
- : prevState.items.concat(state.items),
192
+ data.value.lastPage === 0
193
+ ? data.value.items
194
+ : prevState.items.concat(data.value.items),
159
195
  };
160
196
  }
161
197
  case 'advance': {
162
- const hasMore = prevState.totalPages > prevState.lastPage + 1;
198
+ const hasMore = prevState.totalPages! > prevState.lastPage! + 1;
163
199
 
164
200
  return {
165
201
  ...prevState,
166
- lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage,
202
+ lastPage: hasMore ? prevState.lastPage! + 1 : prevState.lastPage,
167
203
  hasMore,
168
204
  };
169
205
  }
@@ -173,6 +209,7 @@ function SearchPage() {
173
209
  },
174
210
  initialSearchResultState,
175
211
  );
212
+
176
213
  const algoliaClient = algoliaSearch(appId, apiKey);
177
214
  const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
178
215
  hitsPerPage: 15,
@@ -188,12 +225,11 @@ function SearchPage() {
188
225
  return;
189
226
  }
190
227
 
191
- const sanitizeValue = (value) => {
192
- return value.replace(
228
+ const sanitizeValue = (value: string) =>
229
+ value.replace(
193
230
  /algolia-docsearch-suggestion--highlight/g,
194
231
  'search-result-match',
195
232
  );
196
- };
197
233
 
198
234
  const items = hits.map(
199
235
  ({
@@ -201,14 +237,16 @@ function SearchPage() {
201
237
  _highlightResult: {hierarchy},
202
238
  _snippetResult: snippet = {},
203
239
  }) => {
204
- const {pathname, hash} = new URL(url);
205
- const titles = Object.keys(hierarchy).map((key) => {
206
- return sanitizeValue(hierarchy[key].value);
207
- });
240
+ const parsedURL = new URL(url);
241
+ const titles = Object.keys(hierarchy).map((key) =>
242
+ sanitizeValue(hierarchy[key].value),
243
+ );
208
244
 
209
245
  return {
210
- title: titles.pop(),
211
- url: pathname + hash,
246
+ title: titles.pop()!,
247
+ url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
248
+ ? parsedURL.href
249
+ : parsedURL.pathname + parsedURL.hash,
212
250
  summary: snippet.content
213
251
  ? `${sanitizeValue(snippet.content.value)}...`
214
252
  : '',
@@ -232,7 +270,7 @@ function SearchPage() {
232
270
  },
233
271
  );
234
272
 
235
- const [loaderRef, setLoaderRef] = useState(null);
273
+ const [loaderRef, setLoaderRef] = useState<HTMLDivElement | null>(null);
236
274
  const prevY = useRef(0);
237
275
  const observer = useRef(
238
276
  ExecutionEnvironment.canUseDOM &&
@@ -271,7 +309,7 @@ function SearchPage() {
271
309
  description: 'The search page title for empty query',
272
310
  });
273
311
 
274
- const makeSearch = (page = 0) => {
312
+ const makeSearch = useDynamicCallback((page: number = 0) => {
275
313
  algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
276
314
  algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
277
315
 
@@ -285,23 +323,21 @@ function SearchPage() {
285
323
  );
286
324
 
287
325
  algoliaHelper.setQuery(searchQuery).setPage(page).search();
288
- };
326
+ });
289
327
 
290
328
  useEffect(() => {
291
329
  if (!loaderRef) {
292
330
  return undefined;
293
331
  }
294
-
295
- observer.current.observe(loaderRef);
296
-
297
- return () => {
298
- observer.current.unobserve(loaderRef);
299
- };
332
+ const currentObserver = observer.current;
333
+ if (currentObserver) {
334
+ currentObserver.observe(loaderRef);
335
+ return () => currentObserver.unobserve(loaderRef);
336
+ }
337
+ return () => true;
300
338
  }, [loaderRef]);
301
339
 
302
340
  useEffect(() => {
303
- updateSearchPath(searchQuery);
304
-
305
341
  searchResultStateDispatcher({type: 'reset'});
306
342
 
307
343
  if (searchQuery) {
@@ -311,7 +347,7 @@ function SearchPage() {
311
347
  makeSearch();
312
348
  }, 300);
313
349
  }
314
- }, [searchQuery, docsSearchVersionsHelpers.searchVersions]);
350
+ }, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]);
315
351
 
316
352
  useEffect(() => {
317
353
  if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
@@ -319,13 +355,7 @@ function SearchPage() {
319
355
  }
320
356
 
321
357
  makeSearch(searchResultState.lastPage);
322
- }, [searchResultState.lastPage]);
323
-
324
- useEffect(() => {
325
- if (searchValue && searchValue !== searchQuery) {
326
- setSearchQuery(searchValue);
327
- }
328
- }, [searchValue]);
358
+ }, [makeSearch, searchResultState.lastPage]);
329
359
 
330
360
  return (
331
361
  <Layout wrapperClassName="search-page-wrapper">
@@ -5,13 +5,14 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import useContextualSearchFilters from '@theme/hooks/useContextualSearchFilters';
8
+ import type {useAlgoliaContextualFacetFiltersReturns} from '@theme/hooks/useAlgoliaContextualFacetFilters';
9
+ import {useContextualSearchFilters} from '@docusaurus/theme-common';
9
10
 
10
11
  // Translate search-engine agnostic search filters to Algolia search filters
11
- export default function useAlgoliaContextualFacetFilters() {
12
+ export default function useAlgoliaContextualFacetFilters(): useAlgoliaContextualFacetFiltersReturns {
12
13
  const {locale, tags} = useContextualSearchFilters();
13
14
 
14
- // seems safe to convert locale->language, see AlgoliaSearchMetadatas comment
15
+ // seems safe to convert locale->language, see AlgoliaSearchMetadata comment
15
16
  const languageFilter = `language:${locale}`;
16
17
 
17
18
  const tagsFilter = tags.map((tag) => `docusaurus_tag:${tag}`);
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import {useHistory} from '@docusaurus/router';
9
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
10
+ import {useCallback, useEffect, useState} from 'react';
11
+ import type {SearchQuery} from '@theme/hooks/useSearchQuery';
12
+
13
+ const SEARCH_PARAM_QUERY = 'q';
14
+
15
+ function useSearchQuery(): SearchQuery {
16
+ const history = useHistory();
17
+ const {
18
+ siteConfig: {baseUrl},
19
+ } = useDocusaurusContext();
20
+
21
+ const [searchQuery, setSearchQueryState] = useState('');
22
+
23
+ // Init search query just after React hydration
24
+ useEffect(() => {
25
+ const searchQueryStringValue =
26
+ new URLSearchParams(window.location.search).get(SEARCH_PARAM_QUERY) ?? '';
27
+
28
+ setSearchQueryState(searchQueryStringValue);
29
+ }, []);
30
+
31
+ const setSearchQuery = useCallback(
32
+ (newSearchQuery: string) => {
33
+ const searchParams = new URLSearchParams(window.location.search);
34
+
35
+ if (newSearchQuery) {
36
+ searchParams.set(SEARCH_PARAM_QUERY, newSearchQuery);
37
+ } else {
38
+ searchParams.delete(SEARCH_PARAM_QUERY);
39
+ }
40
+
41
+ history.replace({
42
+ search: searchParams.toString(),
43
+ });
44
+ setSearchQueryState(newSearchQuery);
45
+ },
46
+ [history],
47
+ );
48
+
49
+ const generateSearchPageLink = useCallback(
50
+ (targetSearchQuery: string) =>
51
+ // Refer to https://github.com/facebook/docusaurus/pull/2838
52
+ `${baseUrl}search?q=${encodeURIComponent(targetSearchQuery)}`,
53
+ [baseUrl],
54
+ );
55
+
56
+ return {
57
+ searchQuery,
58
+ setSearchQuery,
59
+ generateSearchPageLink,
60
+ };
61
+ }
62
+
63
+ export default useSearchQuery;