Skip to content

Commit 78d5a53

Browse files
mturocislorber
andcommitted
feat(theme-classic): store selected tab in query string. (#8225)
Co-authored-by: sebastienlorber <[email protected]> Closes #7008
1 parent cc95fb6 commit 78d5a53

File tree

6 files changed

+329
-111
lines changed

6 files changed

+329
-111
lines changed

packages/docusaurus-theme-classic/src/theme-classic.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,7 @@ declare module '@theme/Tabs' {
11541154
}[];
11551155
readonly groupId?: string;
11561156
readonly className?: string;
1157+
readonly queryString?: string | boolean;
11571158
}
11581159

11591160
export default function Tabs(props: Props): JSX.Element;

packages/docusaurus-theme-classic/src/theme/Tabs/__tests__/index.test.tsx

Lines changed: 139 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,42 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import React from 'react';
8+
import React, {type ReactNode} from 'react';
99
import renderer from 'react-test-renderer';
1010
import {
1111
TabGroupChoiceProvider,
1212
ScrollControllerProvider,
1313
} from '@docusaurus/theme-common/internal';
14+
import {StaticRouter} from 'react-router-dom';
1415
import Tabs from '../index';
1516
import TabItem from '../../TabItem';
1617

18+
function TestProviders({
19+
children,
20+
pathname = '/',
21+
}: {
22+
children: ReactNode;
23+
pathname?: string;
24+
}) {
25+
return (
26+
<StaticRouter location={{pathname}}>
27+
<ScrollControllerProvider>
28+
<TabGroupChoiceProvider>{children}</TabGroupChoiceProvider>
29+
</ScrollControllerProvider>
30+
</StaticRouter>
31+
);
32+
}
33+
1734
describe('Tabs', () => {
1835
it('rejects bad Tabs child', () => {
1936
expect(() => {
2037
renderer.create(
21-
<Tabs>
22-
<div>Naughty</div>
23-
<TabItem value="good">Good</TabItem>
24-
</Tabs>,
38+
<TestProviders>
39+
<Tabs>
40+
<div>Naughty</div>
41+
<TabItem value="good">Good</TabItem>
42+
</Tabs>
43+
</TestProviders>,
2544
);
2645
}).toThrowErrorMatchingInlineSnapshot(
2746
`"Docusaurus error: Bad <Tabs> child <div>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop."`,
@@ -30,10 +49,12 @@ describe('Tabs', () => {
3049
it('rejects bad Tabs defaultValue', () => {
3150
expect(() => {
3251
renderer.create(
33-
<Tabs defaultValue="bad">
34-
<TabItem value="v1">Tab 1</TabItem>
35-
<TabItem value="v2">Tab 2</TabItem>
36-
</Tabs>,
52+
<TestProviders>
53+
<Tabs defaultValue="bad">
54+
<TabItem value="v1">Tab 1</TabItem>
55+
<TabItem value="v2">Tab 2</TabItem>
56+
</Tabs>
57+
</TestProviders>,
3758
);
3859
}).toThrowErrorMatchingInlineSnapshot(
3960
`"Docusaurus error: The <Tabs> has a defaultValue "bad" but none of its children has the corresponding value. Available values are: v1, v2. If you intend to show no default tab, use defaultValue={null} instead."`,
@@ -42,14 +63,16 @@ describe('Tabs', () => {
4263
it('rejects duplicate values', () => {
4364
expect(() => {
4465
renderer.create(
45-
<Tabs>
46-
<TabItem value="v1">Tab 1</TabItem>
47-
<TabItem value="v2">Tab 2</TabItem>
48-
<TabItem value="v3">Tab 3</TabItem>
49-
<TabItem value="v4">Tab 4</TabItem>
50-
<TabItem value="v1">Tab 5</TabItem>
51-
<TabItem value="v2">Tab 6</TabItem>
52-
</Tabs>,
66+
<TestProviders>
67+
<Tabs>
68+
<TabItem value="v1">Tab 1</TabItem>
69+
<TabItem value="v2">Tab 2</TabItem>
70+
<TabItem value="v3">Tab 3</TabItem>
71+
<TabItem value="v4">Tab 4</TabItem>
72+
<TabItem value="v1">Tab 5</TabItem>
73+
<TabItem value="v2">Tab 6</TabItem>
74+
</Tabs>
75+
</TestProviders>,
5376
);
5477
}).toThrowErrorMatchingInlineSnapshot(
5578
`"Docusaurus error: Duplicate values "v1, v2" found in <Tabs>. Every value needs to be unique."`,
@@ -58,54 +81,52 @@ describe('Tabs', () => {
5881
it('accepts valid Tabs config', () => {
5982
expect(() => {
6083
renderer.create(
61-
<ScrollControllerProvider>
62-
<TabGroupChoiceProvider>
63-
<Tabs>
64-
<TabItem value="v1">Tab 1</TabItem>
65-
<TabItem value="v2">Tab 2</TabItem>
66-
</Tabs>
67-
<Tabs>
68-
<TabItem value="v1">Tab 1</TabItem>
69-
<TabItem value="v2" default>
70-
Tab 2
71-
</TabItem>
72-
</Tabs>
73-
<Tabs defaultValue="v1">
74-
<TabItem value="v1" label="V1">
75-
Tab 1
76-
</TabItem>
77-
<TabItem value="v2" label="V2">
78-
Tab 2
79-
</TabItem>
80-
</Tabs>
81-
<Tabs
82-
defaultValue="v1"
83-
values={[
84-
{value: 'v1', label: 'V1'},
85-
{value: 'v2', label: 'V2'},
86-
]}>
87-
<TabItem value="v1">Tab 1</TabItem>
88-
<TabItem value="v2">Tab 2</TabItem>
89-
</Tabs>
90-
<Tabs
91-
defaultValue={null}
92-
values={[
93-
{value: 'v1', label: 'V1'},
94-
{value: 'v2', label: 'V2'},
95-
]}>
96-
<TabItem value="v1">Tab 1</TabItem>
97-
<TabItem value="v2">Tab 2</TabItem>
98-
</Tabs>
99-
<Tabs defaultValue={null}>
100-
<TabItem value="v1" label="V1">
101-
Tab 1
102-
</TabItem>
103-
<TabItem value="v2" label="V2">
104-
Tab 2
105-
</TabItem>
106-
</Tabs>
107-
</TabGroupChoiceProvider>
108-
</ScrollControllerProvider>,
84+
<TestProviders>
85+
<Tabs>
86+
<TabItem value="v1">Tab 1</TabItem>
87+
<TabItem value="v2">Tab 2</TabItem>
88+
</Tabs>
89+
<Tabs>
90+
<TabItem value="v1">Tab 1</TabItem>
91+
<TabItem value="v2" default>
92+
Tab 2
93+
</TabItem>
94+
</Tabs>
95+
<Tabs defaultValue="v1">
96+
<TabItem value="v1" label="V1">
97+
Tab 1
98+
</TabItem>
99+
<TabItem value="v2" label="V2">
100+
Tab 2
101+
</TabItem>
102+
</Tabs>
103+
<Tabs
104+
defaultValue="v1"
105+
values={[
106+
{value: 'v1', label: 'V1'},
107+
{value: 'v2', label: 'V2'},
108+
]}>
109+
<TabItem value="v1">Tab 1</TabItem>
110+
<TabItem value="v2">Tab 2</TabItem>
111+
</Tabs>
112+
<Tabs
113+
defaultValue={null}
114+
values={[
115+
{value: 'v1', label: 'V1'},
116+
{value: 'v2', label: 'V2'},
117+
]}>
118+
<TabItem value="v1">Tab 1</TabItem>
119+
<TabItem value="v2">Tab 2</TabItem>
120+
</Tabs>
121+
<Tabs defaultValue={null}>
122+
<TabItem value="v1" label="V1">
123+
Tab 1
124+
</TabItem>
125+
<TabItem value="v2" label="V2">
126+
Tab 2
127+
</TabItem>
128+
</Tabs>
129+
</TestProviders>,
109130
);
110131
}).not.toThrow(); // TODO Better Jest infrastructure to mock the Layout
111132
});
@@ -114,22 +135,60 @@ describe('Tabs', () => {
114135
expect(() => {
115136
const tabs = ['Apple', 'Banana', 'Carrot'];
116137
renderer.create(
117-
<ScrollControllerProvider>
118-
<TabGroupChoiceProvider>
119-
<Tabs
120-
// @ts-expect-error: for an edge-case that we didn't write types for
121-
values={tabs.map((t, idx) => ({label: t, value: idx}))}
138+
<TestProviders>
139+
<Tabs
140+
// @ts-expect-error: for an edge-case that we didn't write types for
141+
values={tabs.map((t, idx) => ({label: t, value: idx}))}
142+
// @ts-expect-error: for an edge-case that we didn't write types for
143+
defaultValue={0}>
144+
{tabs.map((t, idx) => (
122145
// @ts-expect-error: for an edge-case that we didn't write types for
123-
defaultValue={0}>
124-
{tabs.map((t, idx) => (
125-
// @ts-expect-error: for an edge-case that we didn't write types for
126-
<TabItem key={idx} value={idx}>
127-
{t}
128-
</TabItem>
129-
))}
130-
</Tabs>
131-
</TabGroupChoiceProvider>
132-
</ScrollControllerProvider>,
146+
<TabItem key={idx} value={idx}>
147+
{t}
148+
</TabItem>
149+
))}
150+
</Tabs>
151+
</TestProviders>,
152+
);
153+
}).not.toThrow();
154+
});
155+
it('rejects if querystring is true, but groupId falsy', () => {
156+
expect(() => {
157+
renderer.create(
158+
<TestProviders>
159+
<Tabs queryString>
160+
<TabItem value="val1">Val1</TabItem>
161+
<TabItem value="val2">Val2</TabItem>
162+
</Tabs>
163+
</TestProviders>,
164+
);
165+
}).toThrow(
166+
'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".',
167+
);
168+
});
169+
170+
it('accept querystring=true when groupId is defined', () => {
171+
expect(() => {
172+
renderer.create(
173+
<TestProviders>
174+
<Tabs queryString groupId="my-group-id">
175+
<TabItem value="val1">Val1</TabItem>
176+
<TabItem value="val2">Val2</TabItem>
177+
</Tabs>
178+
</TestProviders>,
179+
);
180+
}).not.toThrow();
181+
});
182+
183+
it('accept querystring as string, but groupId falsy', () => {
184+
expect(() => {
185+
renderer.create(
186+
<TestProviders>
187+
<Tabs queryString="qsKey">
188+
<TabItem value="val1">Val1</TabItem>
189+
<TabItem value="val2">Val2</TabItem>
190+
</Tabs>
191+
</TestProviders>,
133192
);
134193
}).not.toThrow();
135194
});

0 commit comments

Comments
 (0)