@docusaurus/theme-classic 0.0.0-5406 → 0.0.0-5408

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.
@@ -4,20 +4,66 @@
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
- import React, {useState, cloneElement, isValidElement} from 'react';
7
+ import React, {
8
+ cloneElement,
9
+ isValidElement,
10
+ useCallback,
11
+ useEffect,
12
+ useState,
13
+ } from 'react';
8
14
  import clsx from 'clsx';
9
- import useIsBrowser from '@docusaurus/useIsBrowser';
10
- import {duplicates} from '@docusaurus/theme-common';
15
+ import {useHistory, useLocation} from '@docusaurus/router';
16
+ import {duplicates, useEvent} from '@docusaurus/theme-common';
11
17
  import {
12
18
  useScrollPositionBlocker,
13
19
  useTabGroupChoice,
14
20
  } from '@docusaurus/theme-common/internal';
21
+ import useIsBrowser from '@docusaurus/useIsBrowser';
15
22
  import styles from './styles.module.css';
16
23
  // A very rough duck type, but good enough to guard against mistakes while
17
24
  // allowing customization
18
25
  function isTabItem(comp) {
19
26
  return 'value' in comp.props;
20
27
  }
28
+ function getSearchKey({queryString = false, groupId}) {
29
+ if (typeof queryString === 'string') {
30
+ return queryString;
31
+ }
32
+ if (queryString === false) {
33
+ return undefined;
34
+ }
35
+ if (queryString === true && !groupId) {
36
+ throw new Error(
37
+ `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".`,
38
+ );
39
+ }
40
+ return groupId;
41
+ }
42
+ function useTabQueryString({queryString = false, groupId}) {
43
+ // TODO not re-render optimized
44
+ // See https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api
45
+ const location = useLocation();
46
+ const history = useHistory();
47
+ const searchKey = getSearchKey({queryString, groupId});
48
+ const get = useCallback(() => {
49
+ if (!searchKey) {
50
+ return undefined;
51
+ }
52
+ return new URLSearchParams(location.search).get(searchKey);
53
+ }, [searchKey, location.search]);
54
+ const set = useCallback(
55
+ (newTabValue) => {
56
+ if (!searchKey) {
57
+ return; // no-op
58
+ }
59
+ const searchParams = new URLSearchParams(location.search);
60
+ searchParams.set(searchKey, newTabValue);
61
+ history.replace({...location, search: searchParams.toString()});
62
+ },
63
+ [searchKey, history, location],
64
+ );
65
+ return {get, set};
66
+ }
21
67
  function TabsComponent(props) {
22
68
  const {
23
69
  lazy,
@@ -26,6 +72,7 @@ function TabsComponent(props) {
26
72
  values: valuesProp,
27
73
  groupId,
28
74
  className,
75
+ queryString = false,
29
76
  } = props;
30
77
  const children = React.Children.map(props.children, (child) => {
31
78
  if (isValidElement(child) && isTabItem(child)) {
@@ -40,6 +87,7 @@ function TabsComponent(props) {
40
87
  }>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`,
41
88
  );
42
89
  });
90
+ const tabQueryString = useTabQueryString({queryString, groupId});
43
91
  const values =
44
92
  valuesProp ??
45
93
  // Only pick keys that we recognize. MDX would inject some keys by default
@@ -56,37 +104,57 @@ function TabsComponent(props) {
56
104
  .join(', ')}" found in <Tabs>. Every value needs to be unique.`,
57
105
  );
58
106
  }
59
- // When defaultValueProp is null, don't show a default tab
60
- const defaultValue =
61
- defaultValueProp === null
62
- ? defaultValueProp
63
- : defaultValueProp ??
64
- children.find((child) => child.props.default)?.props.value ??
65
- children[0].props.value;
66
- if (defaultValue !== null && !values.some((a) => a.value === defaultValue)) {
107
+ // Warn user about passing incorrect defaultValue as prop.
108
+ if (
109
+ defaultValueProp !== null &&
110
+ defaultValueProp !== undefined &&
111
+ !values.some((a) => a.value === defaultValueProp)
112
+ ) {
67
113
  throw new Error(
68
- `Docusaurus error: The <Tabs> has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${values
114
+ `Docusaurus error: The <Tabs> has a defaultValue "${defaultValueProp}" but none of its children has the corresponding value. Available values are: ${values
69
115
  .map((a) => a.value)
70
116
  .join(
71
117
  ', ',
72
118
  )}. If you intend to show no default tab, use defaultValue={null} instead.`,
73
119
  );
74
120
  }
75
- const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoice();
121
+ const {
122
+ ready: tabGroupChoicesReady,
123
+ tabGroupChoices,
124
+ setTabGroupChoices,
125
+ } = useTabGroupChoice();
126
+ const defaultValue =
127
+ defaultValueProp !== undefined
128
+ ? defaultValueProp
129
+ : children.find((child) => child.props.default)?.props.value ??
130
+ children[0].props.value;
76
131
  const [selectedValue, setSelectedValue] = useState(defaultValue);
77
132
  const tabRefs = [];
78
133
  const {blockElementScrollPositionUntilNextRender} =
79
134
  useScrollPositionBlocker();
80
- if (groupId != null) {
81
- const relevantTabGroupChoice = tabGroupChoices[groupId];
82
- if (
83
- relevantTabGroupChoice != null &&
84
- relevantTabGroupChoice !== selectedValue &&
85
- values.some((value) => value.value === relevantTabGroupChoice)
86
- ) {
87
- setSelectedValue(relevantTabGroupChoice);
135
+ // Lazily restore the appropriate tab selected value
136
+ // We can't read queryString/localStorage on first render
137
+ // It would trigger a React SSR/client hydration mismatch
138
+ const restoreTabSelectedValue = useEvent(() => {
139
+ // wait for localStorage values to be set (initially empty object :s)
140
+ if (tabGroupChoicesReady) {
141
+ // querystring value > localStorage value
142
+ const valueToRestore =
143
+ tabQueryString.get() ?? (groupId && tabGroupChoices[groupId]);
144
+ const isValid =
145
+ valueToRestore &&
146
+ values.some((value) => value.value === valueToRestore);
147
+ if (isValid) {
148
+ setSelectedValue(valueToRestore);
149
+ }
88
150
  }
89
- }
151
+ });
152
+ useEffect(() => {
153
+ // wait for localStorage values to be set (initially empty object :s)
154
+ if (tabGroupChoicesReady) {
155
+ restoreTabSelectedValue();
156
+ }
157
+ }, [tabGroupChoicesReady, restoreTabSelectedValue]);
90
158
  const handleTabChange = (event) => {
91
159
  const newTab = event.currentTarget;
92
160
  const newTabIndex = tabRefs.indexOf(newTab);
@@ -94,6 +162,7 @@ function TabsComponent(props) {
94
162
  if (newTabValue !== selectedValue) {
95
163
  blockElementScrollPositionUntilNextRender(newTab);
96
164
  setSelectedValue(newTabValue);
165
+ tabQueryString.set(newTabValue);
97
166
  if (groupId != null) {
98
167
  setTabGroupChoices(groupId, String(newTabValue));
99
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docusaurus/theme-classic",
3
- "version": "0.0.0-5406",
3
+ "version": "0.0.0-5408",
4
4
  "description": "Classic theme for Docusaurus",
5
5
  "main": "lib/index.js",
6
6
  "types": "src/theme-classic.d.ts",
@@ -20,18 +20,18 @@
20
20
  "copy:watch": "node ../../admin/scripts/copyUntypedFiles.js --watch"
21
21
  },
22
22
  "dependencies": {
23
- "@docusaurus/core": "0.0.0-5406",
24
- "@docusaurus/mdx-loader": "0.0.0-5406",
25
- "@docusaurus/module-type-aliases": "0.0.0-5406",
26
- "@docusaurus/plugin-content-blog": "0.0.0-5406",
27
- "@docusaurus/plugin-content-docs": "0.0.0-5406",
28
- "@docusaurus/plugin-content-pages": "0.0.0-5406",
29
- "@docusaurus/theme-common": "0.0.0-5406",
30
- "@docusaurus/theme-translations": "0.0.0-5406",
31
- "@docusaurus/types": "0.0.0-5406",
32
- "@docusaurus/utils": "0.0.0-5406",
33
- "@docusaurus/utils-common": "0.0.0-5406",
34
- "@docusaurus/utils-validation": "0.0.0-5406",
23
+ "@docusaurus/core": "0.0.0-5408",
24
+ "@docusaurus/mdx-loader": "0.0.0-5408",
25
+ "@docusaurus/module-type-aliases": "0.0.0-5408",
26
+ "@docusaurus/plugin-content-blog": "0.0.0-5408",
27
+ "@docusaurus/plugin-content-docs": "0.0.0-5408",
28
+ "@docusaurus/plugin-content-pages": "0.0.0-5408",
29
+ "@docusaurus/theme-common": "0.0.0-5408",
30
+ "@docusaurus/theme-translations": "0.0.0-5408",
31
+ "@docusaurus/types": "0.0.0-5408",
32
+ "@docusaurus/utils": "0.0.0-5408",
33
+ "@docusaurus/utils-common": "0.0.0-5408",
34
+ "@docusaurus/utils-validation": "0.0.0-5408",
35
35
  "@mdx-js/react": "^1.6.22",
36
36
  "clsx": "^1.2.1",
37
37
  "copy-text-to-clipboard": "^3.0.1",
@@ -62,5 +62,5 @@
62
62
  "engines": {
63
63
  "node": ">=16.14"
64
64
  },
65
- "gitHead": "5b76959dddeb67005b131390645836e88ac84637"
65
+ "gitHead": "6d1f31ee146538494968633a12e92c47cd3de973"
66
66
  }
@@ -6,21 +6,23 @@
6
6
  */
7
7
 
8
8
  import React, {
9
- useState,
10
9
  cloneElement,
11
10
  isValidElement,
11
+ useCallback,
12
+ useEffect,
13
+ useState,
12
14
  type ReactElement,
13
15
  } from 'react';
14
16
  import clsx from 'clsx';
15
- import useIsBrowser from '@docusaurus/useIsBrowser';
16
- import {duplicates} from '@docusaurus/theme-common';
17
+ import {useHistory, useLocation} from '@docusaurus/router';
18
+ import {duplicates, useEvent} from '@docusaurus/theme-common';
17
19
  import {
18
20
  useScrollPositionBlocker,
19
21
  useTabGroupChoice,
20
22
  } from '@docusaurus/theme-common/internal';
21
- import type {Props} from '@theme/Tabs';
23
+ import useIsBrowser from '@docusaurus/useIsBrowser';
22
24
  import type {Props as TabItemProps} from '@theme/TabItem';
23
-
25
+ import type {Props} from '@theme/Tabs';
24
26
  import styles from './styles.module.css';
25
27
 
26
28
  // A very rough duck type, but good enough to guard against mistakes while
@@ -31,6 +33,57 @@ function isTabItem(
31
33
  return 'value' in comp.props;
32
34
  }
33
35
 
36
+ function getSearchKey({
37
+ queryString = false,
38
+ groupId,
39
+ }: Pick<Props, 'queryString' | 'groupId'>) {
40
+ if (typeof queryString === 'string') {
41
+ return queryString;
42
+ }
43
+ if (queryString === false) {
44
+ return undefined;
45
+ }
46
+ if (queryString === true && !groupId) {
47
+ throw new Error(
48
+ `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".`,
49
+ );
50
+ }
51
+ return groupId;
52
+ }
53
+
54
+ function useTabQueryString({
55
+ queryString = false,
56
+ groupId,
57
+ }: Pick<Props, 'queryString' | 'groupId'>) {
58
+ // TODO not re-render optimized
59
+ // See https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api
60
+ const location = useLocation();
61
+ const history = useHistory();
62
+
63
+ const searchKey = getSearchKey({queryString, groupId});
64
+
65
+ const get = useCallback(() => {
66
+ if (!searchKey) {
67
+ return undefined;
68
+ }
69
+ return new URLSearchParams(location.search).get(searchKey);
70
+ }, [searchKey, location.search]);
71
+
72
+ const set = useCallback(
73
+ (newTabValue: string) => {
74
+ if (!searchKey) {
75
+ return; // no-op
76
+ }
77
+ const searchParams = new URLSearchParams(location.search);
78
+ searchParams.set(searchKey, newTabValue);
79
+ history.replace({...location, search: searchParams.toString()});
80
+ },
81
+ [searchKey, history, location],
82
+ );
83
+
84
+ return {get, set};
85
+ }
86
+
34
87
  function TabsComponent(props: Props): JSX.Element {
35
88
  const {
36
89
  lazy,
@@ -39,6 +92,7 @@ function TabsComponent(props: Props): JSX.Element {
39
92
  values: valuesProp,
40
93
  groupId,
41
94
  className,
95
+ queryString = false,
42
96
  } = props;
43
97
  const children = React.Children.map(props.children, (child) => {
44
98
  if (isValidElement(child) && isTabItem(child)) {
@@ -53,6 +107,7 @@ function TabsComponent(props: Props): JSX.Element {
53
107
  }>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`,
54
108
  );
55
109
  });
110
+ const tabQueryString = useTabQueryString({queryString, groupId});
56
111
  const values =
57
112
  valuesProp ??
58
113
  // Only pick keys that we recognize. MDX would inject some keys by default
@@ -69,16 +124,15 @@ function TabsComponent(props: Props): JSX.Element {
69
124
  .join(', ')}" found in <Tabs>. Every value needs to be unique.`,
70
125
  );
71
126
  }
72
- // When defaultValueProp is null, don't show a default tab
73
- const defaultValue =
74
- defaultValueProp === null
75
- ? defaultValueProp
76
- : defaultValueProp ??
77
- children.find((child) => child.props.default)?.props.value ??
78
- children[0]!.props.value;
79
- if (defaultValue !== null && !values.some((a) => a.value === defaultValue)) {
127
+
128
+ // Warn user about passing incorrect defaultValue as prop.
129
+ if (
130
+ defaultValueProp !== null &&
131
+ defaultValueProp !== undefined &&
132
+ !values.some((a) => a.value === defaultValueProp)
133
+ ) {
80
134
  throw new Error(
81
- `Docusaurus error: The <Tabs> has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${values
135
+ `Docusaurus error: The <Tabs> has a defaultValue "${defaultValueProp}" but none of its children has the corresponding value. Available values are: ${values
82
136
  .map((a) => a.value)
83
137
  .join(
84
138
  ', ',
@@ -86,22 +140,45 @@ function TabsComponent(props: Props): JSX.Element {
86
140
  );
87
141
  }
88
142
 
89
- const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoice();
143
+ const {
144
+ ready: tabGroupChoicesReady,
145
+ tabGroupChoices,
146
+ setTabGroupChoices,
147
+ } = useTabGroupChoice();
148
+ const defaultValue =
149
+ defaultValueProp !== undefined
150
+ ? defaultValueProp
151
+ : children.find((child) => child.props.default)?.props.value ??
152
+ children[0]!.props.value;
153
+
90
154
  const [selectedValue, setSelectedValue] = useState(defaultValue);
91
155
  const tabRefs: (HTMLLIElement | null)[] = [];
92
156
  const {blockElementScrollPositionUntilNextRender} =
93
157
  useScrollPositionBlocker();
94
158
 
95
- if (groupId != null) {
96
- const relevantTabGroupChoice = tabGroupChoices[groupId];
97
- if (
98
- relevantTabGroupChoice != null &&
99
- relevantTabGroupChoice !== selectedValue &&
100
- values.some((value) => value.value === relevantTabGroupChoice)
101
- ) {
102
- setSelectedValue(relevantTabGroupChoice);
159
+ // Lazily restore the appropriate tab selected value
160
+ // We can't read queryString/localStorage on first render
161
+ // It would trigger a React SSR/client hydration mismatch
162
+ const restoreTabSelectedValue = useEvent(() => {
163
+ // wait for localStorage values to be set (initially empty object :s)
164
+ if (tabGroupChoicesReady) {
165
+ // querystring value > localStorage value
166
+ const valueToRestore =
167
+ tabQueryString.get() ?? (groupId && tabGroupChoices[groupId]);
168
+ const isValid =
169
+ valueToRestore &&
170
+ values.some((value) => value.value === valueToRestore);
171
+ if (isValid) {
172
+ setSelectedValue(valueToRestore);
173
+ }
103
174
  }
104
- }
175
+ });
176
+ useEffect(() => {
177
+ // wait for localStorage values to be set (initially empty object :s)
178
+ if (tabGroupChoicesReady) {
179
+ restoreTabSelectedValue();
180
+ }
181
+ }, [tabGroupChoicesReady, restoreTabSelectedValue]);
105
182
 
106
183
  const handleTabChange = (
107
184
  event:
@@ -116,7 +193,7 @@ function TabsComponent(props: Props): JSX.Element {
116
193
  if (newTabValue !== selectedValue) {
117
194
  blockElementScrollPositionUntilNextRender(newTab);
118
195
  setSelectedValue(newTabValue);
119
-
196
+ tabQueryString.set(newTabValue);
120
197
  if (groupId != null) {
121
198
  setTabGroupChoices(groupId, String(newTabValue));
122
199
  }
@@ -1263,6 +1263,7 @@ declare module '@theme/Tabs' {
1263
1263
  }[];
1264
1264
  readonly groupId?: string;
1265
1265
  readonly className?: string;
1266
+ readonly queryString?: string | boolean;
1266
1267
  }
1267
1268
 
1268
1269
  export default function Tabs(props: Props): JSX.Element;