@docusaurus/theme-common 2.2.0 → 2.3.1

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 (39) hide show
  1. package/lib/hooks/useSearchPage.d.ts.map +1 -1
  2. package/lib/hooks/useSearchPage.js +4 -2
  3. package/lib/hooks/useSearchPage.js.map +1 -1
  4. package/lib/index.d.ts +1 -1
  5. package/lib/index.d.ts.map +1 -1
  6. package/lib/index.js +1 -1
  7. package/lib/index.js.map +1 -1
  8. package/lib/internal.d.ts +3 -2
  9. package/lib/internal.d.ts.map +1 -1
  10. package/lib/internal.js +2 -2
  11. package/lib/internal.js.map +1 -1
  12. package/lib/utils/historyUtils.d.ts +12 -1
  13. package/lib/utils/historyUtils.d.ts.map +1 -1
  14. package/lib/utils/historyUtils.js +23 -0
  15. package/lib/utils/historyUtils.js.map +1 -1
  16. package/lib/utils/scrollUtils.d.ts.map +1 -1
  17. package/lib/utils/scrollUtils.js +4 -1
  18. package/lib/utils/scrollUtils.js.map +1 -1
  19. package/lib/utils/storageUtils.d.ts +4 -0
  20. package/lib/utils/storageUtils.d.ts.map +1 -1
  21. package/lib/utils/storageUtils.js +77 -7
  22. package/lib/utils/storageUtils.js.map +1 -1
  23. package/lib/utils/tabsUtils.d.ts +46 -0
  24. package/lib/utils/tabsUtils.d.ts.map +1 -0
  25. package/lib/utils/tabsUtils.js +153 -0
  26. package/lib/utils/tabsUtils.js.map +1 -0
  27. package/package.json +11 -10
  28. package/src/hooks/useSearchPage.ts +10 -5
  29. package/src/index.ts +5 -1
  30. package/src/internal.ts +7 -5
  31. package/src/utils/historyUtils.ts +33 -1
  32. package/src/utils/scrollUtils.tsx +4 -1
  33. package/src/utils/storageUtils.ts +115 -7
  34. package/src/utils/tabsUtils.tsx +270 -0
  35. package/lib/contexts/tabGroupChoice.d.ts +0 -21
  36. package/lib/contexts/tabGroupChoice.d.ts.map +0 -1
  37. package/lib/contexts/tabGroupChoice.js +0 -49
  38. package/lib/contexts/tabGroupChoice.js.map +0 -1
  39. package/src/contexts/tabGroupChoice.tsx +0 -85
@@ -0,0 +1,153 @@
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
+ import React, { isValidElement, useCallback, useState, useMemo, useLayoutEffect, } from 'react';
8
+ import { useHistory } from '@docusaurus/router';
9
+ import { useQueryStringValue } from '@docusaurus/theme-common/internal';
10
+ import { duplicates, useStorageSlot } from '../index';
11
+ // A very rough duck type, but good enough to guard against mistakes while
12
+ // allowing customization
13
+ function isTabItem(comp) {
14
+ return 'value' in comp.props;
15
+ }
16
+ function ensureValidChildren(children) {
17
+ return React.Children.map(children, (child) => {
18
+ if (isValidElement(child) && isTabItem(child)) {
19
+ return child;
20
+ }
21
+ // child.type.name will give non-sensical values in prod because of
22
+ // minification, but we assume it won't throw in prod.
23
+ throw new Error(`Docusaurus error: Bad <Tabs> child <${
24
+ // @ts-expect-error: guarding against unexpected cases
25
+ typeof child.type === 'string' ? child.type : child.type.name}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`);
26
+ });
27
+ }
28
+ function extractChildrenTabValues(children) {
29
+ return ensureValidChildren(children).map(({ props: { value, label, attributes, default: isDefault } }) => ({
30
+ value,
31
+ label,
32
+ attributes,
33
+ default: isDefault,
34
+ }));
35
+ }
36
+ function ensureNoDuplicateValue(values) {
37
+ const dup = duplicates(values, (a, b) => a.value === b.value);
38
+ if (dup.length > 0) {
39
+ throw new Error(`Docusaurus error: Duplicate values "${dup
40
+ .map((a) => a.value)
41
+ .join(', ')}" found in <Tabs>. Every value needs to be unique.`);
42
+ }
43
+ }
44
+ function useTabValues(props) {
45
+ const { values: valuesProp, children } = props;
46
+ return useMemo(() => {
47
+ const values = valuesProp ?? extractChildrenTabValues(children);
48
+ ensureNoDuplicateValue(values);
49
+ return values;
50
+ }, [valuesProp, children]);
51
+ }
52
+ function isValidValue({ value, tabValues, }) {
53
+ return tabValues.some((a) => a.value === value);
54
+ }
55
+ function getInitialStateValue({ defaultValue, tabValues, }) {
56
+ if (tabValues.length === 0) {
57
+ throw new Error('Docusaurus error: the <Tabs> component requires at least one <TabItem> children component');
58
+ }
59
+ if (defaultValue) {
60
+ // Warn user about passing incorrect defaultValue as prop.
61
+ if (!isValidValue({ value: defaultValue, tabValues })) {
62
+ throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${tabValues
63
+ .map((a) => a.value)
64
+ .join(', ')}. If you intend to show no default tab, use defaultValue={null} instead.`);
65
+ }
66
+ return defaultValue;
67
+ }
68
+ const defaultTabValue = tabValues.find((tabValue) => tabValue.default) ?? tabValues[0];
69
+ if (!defaultTabValue) {
70
+ throw new Error('Unexpected error: 0 tabValues');
71
+ }
72
+ return defaultTabValue.value;
73
+ }
74
+ function getStorageKey(groupId) {
75
+ if (!groupId) {
76
+ return null;
77
+ }
78
+ return `docusaurus.tab.${groupId}`;
79
+ }
80
+ function getQueryStringKey({ queryString = false, groupId, }) {
81
+ if (typeof queryString === 'string') {
82
+ return queryString;
83
+ }
84
+ if (queryString === false) {
85
+ return null;
86
+ }
87
+ if (queryString === true && !groupId) {
88
+ throw new Error(`Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".`);
89
+ }
90
+ return groupId ?? null;
91
+ }
92
+ function useTabQueryString({ queryString = false, groupId, }) {
93
+ const history = useHistory();
94
+ const key = getQueryStringKey({ queryString, groupId });
95
+ const value = useQueryStringValue(key);
96
+ const setValue = useCallback((newValue) => {
97
+ if (!key) {
98
+ return; // no-op
99
+ }
100
+ const searchParams = new URLSearchParams(history.location.search);
101
+ searchParams.set(key, newValue);
102
+ history.replace({ ...history.location, search: searchParams.toString() });
103
+ }, [key, history]);
104
+ return [value, setValue];
105
+ }
106
+ function useTabStorage({ groupId }) {
107
+ const key = getStorageKey(groupId);
108
+ const [value, storageSlot] = useStorageSlot(key);
109
+ const setValue = useCallback((newValue) => {
110
+ if (!key) {
111
+ return; // no-op
112
+ }
113
+ storageSlot.set(newValue);
114
+ }, [key, storageSlot]);
115
+ return [value, setValue];
116
+ }
117
+ export function useTabs(props) {
118
+ const { defaultValue, queryString = false, groupId } = props;
119
+ const tabValues = useTabValues(props);
120
+ const [selectedValue, setSelectedValue] = useState(() => getInitialStateValue({ defaultValue, tabValues }));
121
+ const [queryStringValue, setQueryString] = useTabQueryString({
122
+ queryString,
123
+ groupId,
124
+ });
125
+ const [storageValue, setStorageValue] = useTabStorage({
126
+ groupId,
127
+ });
128
+ // We sync valid querystring/storage value to state on change + hydration
129
+ const valueToSync = (() => {
130
+ const value = queryStringValue ?? storageValue;
131
+ if (!isValidValue({ value, tabValues })) {
132
+ return null;
133
+ }
134
+ return value;
135
+ })();
136
+ // Sync in a layout/sync effect is important, for useScrollPositionBlocker
137
+ // See https://github.com/facebook/docusaurus/issues/8625
138
+ useLayoutEffect(() => {
139
+ if (valueToSync) {
140
+ setSelectedValue(valueToSync);
141
+ }
142
+ }, [valueToSync]);
143
+ const selectValue = useCallback((newValue) => {
144
+ if (!isValidValue({ value: newValue, tabValues })) {
145
+ throw new Error(`Can't select invalid tab value=${newValue}`);
146
+ }
147
+ setSelectedValue(newValue);
148
+ setQueryString(newValue);
149
+ setStorageValue(newValue);
150
+ }, [setQueryString, setStorageValue, tabValues]);
151
+ return { selectedValue, selectValue, tabValues };
152
+ }
153
+ //# sourceMappingURL=tabsUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabsUtils.js","sourceRoot":"","sources":["../../src/utils/tabsUtils.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docusaurus/theme-common",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "description": "Common code for Docusaurus themes.",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",
@@ -30,12 +30,12 @@
30
30
  },
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@docusaurus/mdx-loader": "2.2.0",
34
- "@docusaurus/module-type-aliases": "2.2.0",
35
- "@docusaurus/plugin-content-blog": "2.2.0",
36
- "@docusaurus/plugin-content-docs": "2.2.0",
37
- "@docusaurus/plugin-content-pages": "2.2.0",
38
- "@docusaurus/utils": "2.2.0",
33
+ "@docusaurus/mdx-loader": "2.3.1",
34
+ "@docusaurus/module-type-aliases": "2.3.1",
35
+ "@docusaurus/plugin-content-blog": "2.3.1",
36
+ "@docusaurus/plugin-content-docs": "2.3.1",
37
+ "@docusaurus/plugin-content-pages": "2.3.1",
38
+ "@docusaurus/utils": "2.3.1",
39
39
  "@types/history": "^4.7.11",
40
40
  "@types/react": "*",
41
41
  "@types/react-router-config": "*",
@@ -43,11 +43,12 @@
43
43
  "parse-numeric-range": "^1.3.0",
44
44
  "prism-react-renderer": "^1.3.5",
45
45
  "tslib": "^2.4.0",
46
+ "use-sync-external-store": "^1.2.0",
46
47
  "utility-types": "^3.10.0"
47
48
  },
48
49
  "devDependencies": {
49
- "@docusaurus/core": "2.2.0",
50
- "@docusaurus/types": "2.2.0",
50
+ "@docusaurus/core": "2.3.1",
51
+ "@docusaurus/types": "2.3.1",
51
52
  "fs-extra": "^10.1.0",
52
53
  "lodash": "^4.17.21"
53
54
  },
@@ -58,5 +59,5 @@
58
59
  "engines": {
59
60
  "node": ">=16.14"
60
61
  },
61
- "gitHead": "a308fb7c81832cca354192fe2984f52749441249"
62
+ "gitHead": "cf12f21a10f6c5439ff931e61419c4bb03a5a2dc"
62
63
  }
@@ -8,6 +8,7 @@
8
8
  import {useCallback, useEffect, useState} from 'react';
9
9
  import {useHistory} from '@docusaurus/router';
10
10
  import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
11
+ import type {ThemeConfig as AlgoliaThemeConfig} from '@docusaurus/theme-search-algolia';
11
12
 
12
13
  const SEARCH_PARAM_QUERY = 'q';
13
14
 
@@ -31,8 +32,11 @@ export function useSearchPage(): {
31
32
  } {
32
33
  const history = useHistory();
33
34
  const {
34
- siteConfig: {baseUrl},
35
+ siteConfig: {baseUrl, themeConfig},
35
36
  } = useDocusaurusContext();
37
+ const {
38
+ algolia: {searchPagePath},
39
+ } = themeConfig as AlgoliaThemeConfig;
36
40
 
37
41
  const [searchQuery, setSearchQueryState] = useState('');
38
42
 
@@ -65,10 +69,11 @@ export function useSearchPage(): {
65
69
  const generateSearchPageLink = useCallback(
66
70
  (targetSearchQuery: string) =>
67
71
  // Refer to https://github.com/facebook/docusaurus/pull/2838
68
- `${baseUrl}search?${SEARCH_PARAM_QUERY}=${encodeURIComponent(
69
- targetSearchQuery,
70
- )}`,
71
- [baseUrl],
72
+ // Note: if searchPagePath is falsy, useSearchPage() will not be called
73
+ `${baseUrl}${
74
+ searchPagePath as string
75
+ }?${SEARCH_PARAM_QUERY}=${encodeURIComponent(targetSearchQuery)}`,
76
+ [baseUrl, searchPagePath],
72
77
  );
73
78
 
74
79
  return {
package/src/index.ts CHANGED
@@ -24,7 +24,11 @@ export {
24
24
  type ColorModeConfig,
25
25
  } from './utils/useThemeConfig';
26
26
 
27
- export {createStorageSlot, listStorageKeys} from './utils/storageUtils';
27
+ export {
28
+ createStorageSlot,
29
+ useStorageSlot,
30
+ listStorageKeys,
31
+ } from './utils/storageUtils';
28
32
 
29
33
  export {useContextualSearchFilters} from './utils/searchUtils';
30
34
 
package/src/internal.ts CHANGED
@@ -42,10 +42,8 @@ export {
42
42
  useAnnouncementBar,
43
43
  } from './contexts/announcementBar';
44
44
 
45
- export {
46
- useTabGroupChoice,
47
- TabGroupChoiceProvider,
48
- } from './contexts/tabGroupChoice';
45
+ export {useTabs} from './utils/tabsUtils';
46
+ export type {TabValue, TabsProps, TabItemProps} from './utils/tabsUtils';
49
47
 
50
48
  export {useNavbarMobileSidebar} from './contexts/navbarMobileSidebar';
51
49
  export {useNavbarSecondaryMenu} from './contexts/navbarSecondaryMenu/display';
@@ -82,7 +80,11 @@ export {useLocationChange} from './utils/useLocationChange';
82
80
 
83
81
  export {useLocalPathname} from './utils/useLocalPathname';
84
82
 
85
- export {useHistoryPopHandler} from './utils/historyUtils';
83
+ export {
84
+ useHistoryPopHandler,
85
+ useHistorySelector,
86
+ useQueryStringValue,
87
+ } from './utils/historyUtils';
86
88
 
87
89
  export {
88
90
  useFilteredAndTreeifiedTOC,
@@ -7,8 +7,11 @@
7
7
 
8
8
  import {useEffect} from 'react';
9
9
  import {useHistory} from '@docusaurus/router';
10
+ // @ts-expect-error: TODO temporary until React 18 upgrade
11
+ import {useSyncExternalStore} from 'use-sync-external-store/shim';
10
12
  import {useEvent} from './reactUtils';
11
- import type {Location, Action} from 'history';
13
+
14
+ import type {History, Location, Action} from 'history';
12
15
 
13
16
  type HistoryBlockHandler = (location: Location, action: Action) => void | false;
14
17
 
@@ -43,3 +46,32 @@ export function useHistoryPopHandler(handler: HistoryBlockHandler): void {
43
46
  return undefined;
44
47
  });
45
48
  }
49
+
50
+ /**
51
+ * Permits to efficiently subscribe to a slice of the history
52
+ * See https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api
53
+ * @param selector
54
+ */
55
+ export function useHistorySelector<Value>(
56
+ selector: (history: History<unknown>) => Value,
57
+ ): Value {
58
+ const history = useHistory();
59
+ return useSyncExternalStore(
60
+ history.listen,
61
+ () => selector(history),
62
+ () => selector(history),
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Permits to efficiently subscribe to a specific querystring value
68
+ * @param key
69
+ */
70
+ export function useQueryStringValue(key: string | null): string | null {
71
+ return useHistorySelector((history) => {
72
+ if (key === null) {
73
+ return null;
74
+ }
75
+ return new URLSearchParams(history.location.search).get(key);
76
+ });
77
+ }
@@ -222,7 +222,10 @@ export function useScrollPositionBlocker(): {
222
222
  );
223
223
 
224
224
  useLayoutEffect(() => {
225
- nextLayoutEffectCallbackRef.current?.();
225
+ // Queuing permits to restore scroll position after all useLayoutEffect
226
+ // have run, and yet preserve the sync nature of the scroll restoration
227
+ // See https://github.com/facebook/docusaurus/issues/8625
228
+ queueMicrotask(() => nextLayoutEffectCallbackRef.current?.());
226
229
  });
227
230
 
228
231
  return {
@@ -5,12 +5,53 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
+ import {useCallback, useRef} from 'react';
9
+ // @ts-expect-error: TODO temp error until React 18 upgrade
10
+ import {useSyncExternalStore} from 'use-sync-external-store/shim';
11
+
8
12
  const StorageTypes = ['localStorage', 'sessionStorage', 'none'] as const;
9
13
 
10
14
  export type StorageType = typeof StorageTypes[number];
11
15
 
12
16
  const DefaultStorageType: StorageType = 'localStorage';
13
17
 
18
+ // window.addEventListener('storage') only works for different windows...
19
+ // so for current window we have to dispatch the event manually
20
+ // Now we can listen for both cross-window / current-window storage changes!
21
+ // see https://stackoverflow.com/a/71177640/82609
22
+ // see https://stackoverflow.com/questions/26974084/listen-for-changes-with-localstorage-on-the-same-window
23
+ function dispatchChangeEvent({
24
+ key,
25
+ oldValue,
26
+ newValue,
27
+ storage,
28
+ }: {
29
+ key: string;
30
+ oldValue: string | null;
31
+ newValue: string | null;
32
+ storage: Storage;
33
+ }) {
34
+ // If we set multiple times the same storage value, events should not be fired
35
+ // The native events behave this way, so our manual event dispatch should
36
+ // rather behave exactly the same. Not doing so might create infinite loops.
37
+ // See https://github.com/facebook/docusaurus/issues/8594
38
+ if (oldValue === newValue) {
39
+ return;
40
+ }
41
+ const event = document.createEvent('StorageEvent');
42
+ event.initStorageEvent(
43
+ 'storage',
44
+ false,
45
+ false,
46
+ key,
47
+ oldValue,
48
+ newValue,
49
+ window.location.href,
50
+ storage,
51
+ );
52
+ window.dispatchEvent(event);
53
+ }
54
+
14
55
  /**
15
56
  * Will return `null` if browser storage is unavailable (like running Docusaurus
16
57
  * in an iframe). This should NOT be called in SSR.
@@ -58,12 +99,14 @@ export type StorageSlot = {
58
99
  get: () => string | null;
59
100
  set: (value: string) => void;
60
101
  del: () => void;
102
+ listen: (onChange: (event: StorageEvent) => void) => () => void;
61
103
  };
62
104
 
63
105
  const NoopStorageSlot: StorageSlot = {
64
106
  get: () => null,
65
107
  set: () => {},
66
108
  del: () => {},
109
+ listen: () => () => {},
67
110
  };
68
111
 
69
112
  // Fail-fast, as storage APIs should not be used during the SSR process
@@ -78,6 +121,7 @@ Please only call storage APIs in effects and event handlers.`);
78
121
  get: throwError,
79
122
  set: throwError,
80
123
  del: throwError,
124
+ listen: throwError,
81
125
  };
82
126
  }
83
127
 
@@ -98,39 +142,103 @@ export function createStorageSlot(
98
142
  if (typeof window === 'undefined') {
99
143
  return createServerStorageSlot(key);
100
144
  }
101
- const browserStorage = getBrowserStorage(options?.persistence);
102
- if (browserStorage === null) {
145
+ const storage = getBrowserStorage(options?.persistence);
146
+ if (storage === null) {
103
147
  return NoopStorageSlot;
104
148
  }
105
149
  return {
106
150
  get: () => {
107
151
  try {
108
- return browserStorage.getItem(key);
152
+ return storage.getItem(key);
109
153
  } catch (err) {
110
154
  console.error(`Docusaurus storage error, can't get key=${key}`, err);
111
155
  return null;
112
156
  }
113
157
  },
114
- set: (value) => {
158
+ set: (newValue) => {
115
159
  try {
116
- browserStorage.setItem(key, value);
160
+ const oldValue = storage.getItem(key);
161
+ storage.setItem(key, newValue);
162
+ dispatchChangeEvent({
163
+ key,
164
+ oldValue,
165
+ newValue,
166
+ storage,
167
+ });
117
168
  } catch (err) {
118
169
  console.error(
119
- `Docusaurus storage error, can't set ${key}=${value}`,
170
+ `Docusaurus storage error, can't set ${key}=${newValue}`,
120
171
  err,
121
172
  );
122
173
  }
123
174
  },
124
175
  del: () => {
125
176
  try {
126
- browserStorage.removeItem(key);
177
+ const oldValue = storage.getItem(key);
178
+ storage.removeItem(key);
179
+ dispatchChangeEvent({key, oldValue, newValue: null, storage});
127
180
  } catch (err) {
128
181
  console.error(`Docusaurus storage error, can't delete key=${key}`, err);
129
182
  }
130
183
  },
184
+ listen: (onChange) => {
185
+ try {
186
+ const listener = (event: StorageEvent) => {
187
+ if (event.storageArea === storage && event.key === key) {
188
+ onChange(event);
189
+ }
190
+ };
191
+ window.addEventListener('storage', listener);
192
+ return () => window.removeEventListener('storage', listener);
193
+ } catch (err) {
194
+ console.error(
195
+ `Docusaurus storage error, can't listen for changes of key=${key}`,
196
+ err,
197
+ );
198
+ return () => {};
199
+ }
200
+ },
131
201
  };
132
202
  }
133
203
 
204
+ export function useStorageSlot(
205
+ key: string | null,
206
+ options?: {persistence?: StorageType},
207
+ ): [string | null, StorageSlot] {
208
+ // Not ideal but good enough: assumes storage slot config is constant
209
+ const storageSlot = useRef(() => {
210
+ if (key === null) {
211
+ return NoopStorageSlot;
212
+ }
213
+ return createStorageSlot(key, options);
214
+ }).current();
215
+
216
+ const listen: StorageSlot['listen'] = useCallback(
217
+ (onChange) => {
218
+ // Do not try to add a listener during SSR
219
+ if (typeof window === 'undefined') {
220
+ return () => {};
221
+ }
222
+ return storageSlot.listen(onChange);
223
+ },
224
+ [storageSlot],
225
+ );
226
+
227
+ const currentValue = useSyncExternalStore(
228
+ listen,
229
+ () => {
230
+ // TODO this check should be useless after React 18
231
+ if (typeof window === 'undefined') {
232
+ return null;
233
+ }
234
+ return storageSlot.get();
235
+ },
236
+ () => null,
237
+ );
238
+
239
+ return [currentValue, storageSlot];
240
+ }
241
+
134
242
  /**
135
243
  * Returns a list of all the keys currently stored in browser storage,
136
244
  * or an empty list if browser storage can't be accessed.