Skip to content

Commit

Permalink
feat(theme-classic): store selected tab in query string. (#8225)
Browse files Browse the repository at this point in the history
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
Closes #7008
  • Loading branch information
mturoci authored and slorber committed Jan 26, 2023
1 parent cc95fb6 commit 78d5a53
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 111 deletions.
1 change: 1 addition & 0 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,7 @@ declare module '@theme/Tabs' {
}[];
readonly groupId?: string;
readonly className?: string;
readonly queryString?: string | boolean;
}

export default function Tabs(props: Props): JSX.Element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,42 @@
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import React, {type ReactNode} from 'react';
import renderer from 'react-test-renderer';
import {
TabGroupChoiceProvider,
ScrollControllerProvider,
} from '@docusaurus/theme-common/internal';
import {StaticRouter} from 'react-router-dom';
import Tabs from '../index';
import TabItem from '../../TabItem';

function TestProviders({
children,
pathname = '/',
}: {
children: ReactNode;
pathname?: string;
}) {
return (
<StaticRouter location={{pathname}}>
<ScrollControllerProvider>
<TabGroupChoiceProvider>{children}</TabGroupChoiceProvider>
</ScrollControllerProvider>
</StaticRouter>
);
}

describe('Tabs', () => {
it('rejects bad Tabs child', () => {
expect(() => {
renderer.create(
<Tabs>
<div>Naughty</div>
<TabItem value="good">Good</TabItem>
</Tabs>,
<TestProviders>
<Tabs>
<div>Naughty</div>
<TabItem value="good">Good</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus error: Bad <Tabs> child <div>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop."`,
Expand All @@ -30,10 +49,12 @@ describe('Tabs', () => {
it('rejects bad Tabs defaultValue', () => {
expect(() => {
renderer.create(
<Tabs defaultValue="bad">
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>,
<TestProviders>
<Tabs defaultValue="bad">
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrowErrorMatchingInlineSnapshot(
`"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."`,
Expand All @@ -42,14 +63,16 @@ describe('Tabs', () => {
it('rejects duplicate values', () => {
expect(() => {
renderer.create(
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
<TabItem value="v3">Tab 3</TabItem>
<TabItem value="v4">Tab 4</TabItem>
<TabItem value="v1">Tab 5</TabItem>
<TabItem value="v2">Tab 6</TabItem>
</Tabs>,
<TestProviders>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
<TabItem value="v3">Tab 3</TabItem>
<TabItem value="v4">Tab 4</TabItem>
<TabItem value="v1">Tab 5</TabItem>
<TabItem value="v2">Tab 6</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus error: Duplicate values "v1, v2" found in <Tabs>. Every value needs to be unique."`,
Expand All @@ -58,54 +81,52 @@ describe('Tabs', () => {
it('accepts valid Tabs config', () => {
expect(() => {
renderer.create(
<ScrollControllerProvider>
<TabGroupChoiceProvider>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2" default>
Tab 2
</TabItem>
</Tabs>
<Tabs defaultValue="v1">
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
<Tabs
defaultValue="v1"
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs
defaultValue={null}
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs defaultValue={null}>
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
</TabGroupChoiceProvider>
</ScrollControllerProvider>,
<TestProviders>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2" default>
Tab 2
</TabItem>
</Tabs>
<Tabs defaultValue="v1">
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
<Tabs
defaultValue="v1"
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs
defaultValue={null}
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs defaultValue={null}>
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
</TestProviders>,
);
}).not.toThrow(); // TODO Better Jest infrastructure to mock the Layout
});
Expand All @@ -114,22 +135,60 @@ describe('Tabs', () => {
expect(() => {
const tabs = ['Apple', 'Banana', 'Carrot'];
renderer.create(
<ScrollControllerProvider>
<TabGroupChoiceProvider>
<Tabs
// @ts-expect-error: for an edge-case that we didn't write types for
values={tabs.map((t, idx) => ({label: t, value: idx}))}
<TestProviders>
<Tabs
// @ts-expect-error: for an edge-case that we didn't write types for
values={tabs.map((t, idx) => ({label: t, value: idx}))}
// @ts-expect-error: for an edge-case that we didn't write types for
defaultValue={0}>
{tabs.map((t, idx) => (
// @ts-expect-error: for an edge-case that we didn't write types for
defaultValue={0}>
{tabs.map((t, idx) => (
// @ts-expect-error: for an edge-case that we didn't write types for
<TabItem key={idx} value={idx}>
{t}
</TabItem>
))}
</Tabs>
</TabGroupChoiceProvider>
</ScrollControllerProvider>,
<TabItem key={idx} value={idx}>
{t}
</TabItem>
))}
</Tabs>
</TestProviders>,
);
}).not.toThrow();
});
it('rejects if querystring is true, but groupId falsy', () => {
expect(() => {
renderer.create(
<TestProviders>
<Tabs queryString>
<TabItem value="val1">Val1</TabItem>
<TabItem value="val2">Val2</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrow(
'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".',
);
});

it('accept querystring=true when groupId is defined', () => {
expect(() => {
renderer.create(
<TestProviders>
<Tabs queryString groupId="my-group-id">
<TabItem value="val1">Val1</TabItem>
<TabItem value="val2">Val2</TabItem>
</Tabs>
</TestProviders>,
);
}).not.toThrow();
});

it('accept querystring as string, but groupId falsy', () => {
expect(() => {
renderer.create(
<TestProviders>
<Tabs queryString="qsKey">
<TabItem value="val1">Val1</TabItem>
<TabItem value="val2">Val2</TabItem>
</Tabs>
</TestProviders>,
);
}).not.toThrow();
});
Expand Down
Loading

0 comments on commit 78d5a53

Please sign in to comment.