Skip to content

Commit 4d7a289

Browse files
hrumhurumslorber
andauthored
feat(theme): add versions attribute to docsVersionDropdown navbar item (#10852)
Co-authored-by: sebastien <[email protected]>
1 parent 8bc3e8a commit 4d7a289

File tree

6 files changed

+276
-10
lines changed

6 files changed

+276
-10
lines changed

packages/docusaurus-theme-classic/src/__tests__/options.test.ts

+104
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,110 @@ describe('themeConfig', () => {
827827
);
828828
});
829829
});
830+
831+
describe('docsVersionDropdown', () => {
832+
describe('versions', () => {
833+
it('accepts array of strings', () => {
834+
const config = {
835+
navbar: {
836+
items: [
837+
{
838+
type: 'docsVersionDropdown',
839+
versions: ['current', '1.0'],
840+
},
841+
],
842+
},
843+
};
844+
testValidateThemeConfig(config);
845+
});
846+
847+
it('rejects empty array of strings', () => {
848+
const config = {
849+
navbar: {
850+
items: [
851+
{
852+
type: 'docsVersionDropdown',
853+
versions: [],
854+
},
855+
],
856+
},
857+
};
858+
expect(() =>
859+
testValidateThemeConfig(config),
860+
).toThrowErrorMatchingInlineSnapshot(
861+
`""navbar.items[0].versions" must contain at least 1 items"`,
862+
);
863+
});
864+
865+
it('rejects array of non-strings', () => {
866+
const config = {
867+
navbar: {
868+
items: [
869+
{
870+
type: 'docsVersionDropdown',
871+
versions: [1, 2],
872+
},
873+
],
874+
},
875+
};
876+
expect(() =>
877+
testValidateThemeConfig(config),
878+
).toThrowErrorMatchingInlineSnapshot(
879+
`""navbar.items[0].versions[0]" must be a string"`,
880+
);
881+
});
882+
883+
it('accepts dictionary of version objects', () => {
884+
const config = {
885+
navbar: {
886+
items: [
887+
{
888+
type: 'docsVersionDropdown',
889+
versions: {current: {}, '1.0': {label: '1.x'}},
890+
},
891+
],
892+
},
893+
};
894+
testValidateThemeConfig(config);
895+
});
896+
897+
it('rejects empty dictionary of objects', () => {
898+
const config = {
899+
navbar: {
900+
items: [
901+
{
902+
type: 'docsVersionDropdown',
903+
versions: {},
904+
},
905+
],
906+
},
907+
};
908+
expect(() =>
909+
testValidateThemeConfig(config),
910+
).toThrowErrorMatchingInlineSnapshot(
911+
`""navbar.items[0].versions" must have at least 1 key"`,
912+
);
913+
});
914+
915+
it('rejects dictionary of invalid objects', () => {
916+
const config = {
917+
navbar: {
918+
items: [
919+
{
920+
type: 'docsVersionDropdown',
921+
versions: {current: {}, '1.0': {invalid: '1.x'}},
922+
},
923+
],
924+
},
925+
};
926+
expect(() =>
927+
testValidateThemeConfig(config),
928+
).toThrowErrorMatchingInlineSnapshot(
929+
`""navbar.items[0].versions.1.0.invalid" is not allowed"`,
930+
);
931+
});
932+
});
933+
});
830934
});
831935

832936
describe('validateOptions', () => {

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

+15
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
import {themes} from 'prism-react-renderer';
99
import {Joi, URISchema} from '@docusaurus/utils-validation';
10+
import type {
11+
PropVersionItem,
12+
PropVersionItems,
13+
} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
1014
import type {Options, PluginOptions} from '@docusaurus/theme-classic';
1115
import type {ThemeConfig} from '@docusaurus/theme-common';
1216
import type {
@@ -210,6 +214,17 @@ const DocsVersionDropdownNavbarItemSchema = NavbarItemBaseSchema.append({
210214
dropdownActiveClassDisabled: Joi.boolean(),
211215
dropdownItemsBefore: Joi.array().items(DropdownSubitemSchema).default([]),
212216
dropdownItemsAfter: Joi.array().items(DropdownSubitemSchema).default([]),
217+
versions: Joi.alternatives().try(
218+
Joi.array().items(Joi.string().min(1)).min(1),
219+
Joi.object<PropVersionItems>()
220+
.pattern(
221+
Joi.string().min(1),
222+
Joi.object<PropVersionItem>({
223+
label: Joi.string().min(1),
224+
}),
225+
)
226+
.min(1),
227+
),
213228
});
214229

215230
const LocaleDropdownNavbarItemSchema = NavbarItemBaseSchema.append({

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

+11
Original file line numberDiff line numberDiff line change
@@ -1257,11 +1257,22 @@ declare module '@theme/NavbarItem/DocsVersionDropdownNavbarItem' {
12571257
import type {Props as DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
12581258
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
12591259

1260+
type PropVersionItem = {
1261+
readonly label?: string;
1262+
};
1263+
1264+
type PropVersionItems = {
1265+
readonly [version: string]: PropVersionItem;
1266+
};
1267+
1268+
type PropVersions = string[] | PropVersionItems;
1269+
12601270
export interface Props extends DropdownNavbarItemProps {
12611271
readonly docsPluginId?: string;
12621272
readonly dropdownActiveClassDisabled?: boolean;
12631273
readonly dropdownItemsBefore: LinkLikeNavbarItemProps[];
12641274
readonly dropdownItemsAfter: LinkLikeNavbarItemProps[];
1275+
readonly versions?: PropVersions;
12651276
}
12661277

12671278
export default function DocsVersionDropdownNavbarItem(

packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx

+87-9
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,68 @@ import {translate} from '@docusaurus/Translate';
1616
import {useLocation} from '@docusaurus/router';
1717
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
1818
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
19-
import type {Props} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
19+
import type {
20+
Props,
21+
PropVersions,
22+
PropVersionItem,
23+
} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
2024
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
2125
import type {
2226
GlobalVersion,
2327
GlobalDoc,
2428
ActiveDocContext,
2529
} from '@docusaurus/plugin-content-docs/client';
2630

31+
type VersionItem = {
32+
version: GlobalVersion;
33+
label: string;
34+
};
35+
36+
function getVersionItems(
37+
versions: GlobalVersion[],
38+
configs?: PropVersions,
39+
): VersionItem[] {
40+
if (configs) {
41+
// Collect all the versions we have
42+
const versionMap = new Map<string, GlobalVersion>(
43+
versions.map((version) => [version.name, version]),
44+
);
45+
46+
const toVersionItem = (
47+
name: string,
48+
config?: PropVersionItem,
49+
): VersionItem => {
50+
const version = versionMap.get(name);
51+
if (!version) {
52+
throw new Error(`No docs version exist for name '${name}', please verify your 'docsVersionDropdown' navbar item versions config.
53+
Available version names:\n- ${versions.map((v) => `${v.name}`).join('\n- ')}`);
54+
}
55+
return {version, label: config?.label ?? version.label};
56+
};
57+
58+
if (Array.isArray(configs)) {
59+
return configs.map((name) => toVersionItem(name, undefined));
60+
} else {
61+
return Object.entries(configs).map(([name, config]) =>
62+
toVersionItem(name, config),
63+
);
64+
}
65+
} else {
66+
return versions.map((version) => ({version, label: version.label}));
67+
}
68+
}
69+
70+
function useVersionItems({
71+
docsPluginId,
72+
configs,
73+
}: {
74+
docsPluginId: Props['docsPluginId'];
75+
configs: Props['versions'];
76+
}): VersionItem[] {
77+
const versions = useVersions(docsPluginId);
78+
return getVersionItems(versions, configs);
79+
}
80+
2781
function getVersionMainDoc(version: GlobalVersion): GlobalDoc {
2882
return version.docs.find((doc) => doc.id === version.mainDocId)!;
2983
}
@@ -40,23 +94,47 @@ function getVersionTargetDoc(
4094
);
4195
}
4296

97+
// The version item to use for the "dropdown button"
98+
function useDisplayedVersionItem({
99+
docsPluginId,
100+
versionItems,
101+
}: {
102+
docsPluginId: Props['docsPluginId'];
103+
versionItems: VersionItem[];
104+
}): VersionItem {
105+
// The order of the candidates matters!
106+
const candidates = useDocsVersionCandidates(docsPluginId);
107+
const candidateItems = candidates
108+
.map((candidate) => versionItems.find((vi) => vi.version === candidate))
109+
.filter((vi) => vi !== undefined);
110+
return candidateItems[0] ?? versionItems[0]!;
111+
}
112+
43113
export default function DocsVersionDropdownNavbarItem({
44114
mobile,
45115
docsPluginId,
46116
dropdownActiveClassDisabled,
47117
dropdownItemsBefore,
48118
dropdownItemsAfter,
119+
versions: configs,
49120
...props
50121
}: Props): ReactNode {
51122
const {search, hash} = useLocation();
52123
const activeDocContext = useActiveDocContext(docsPluginId);
53-
const versions = useVersions(docsPluginId);
54124
const {savePreferredVersionName} = useDocsPreferredVersion(docsPluginId);
125+
const versionItems = useVersionItems({docsPluginId, configs});
126+
const displayedVersionItem = useDisplayedVersionItem({
127+
docsPluginId,
128+
versionItems,
129+
});
55130

56-
function versionToLink(version: GlobalVersion): LinkLikeNavbarItemProps {
131+
function versionItemToLink({
132+
version,
133+
label,
134+
}: VersionItem): LinkLikeNavbarItemProps {
57135
const targetDoc = getVersionTargetDoc(version, activeDocContext);
58136
return {
59-
label: version.label,
137+
label,
60138
// preserve ?search#hash suffix on version switches
61139
to: `${targetDoc.path}${search}${hash}`,
62140
isActive: () => version === activeDocContext.activeVersion,
@@ -66,12 +144,10 @@ export default function DocsVersionDropdownNavbarItem({
66144

67145
const items: LinkLikeNavbarItemProps[] = [
68146
...dropdownItemsBefore,
69-
...versions.map(versionToLink),
147+
...versionItems.map(versionItemToLink),
70148
...dropdownItemsAfter,
71149
];
72150

73-
const dropdownVersion = useDocsVersionCandidates(docsPluginId)[0];
74-
75151
// Mobile dropdown is handled a bit differently
76152
const dropdownLabel =
77153
mobile && items.length > 1
@@ -81,11 +157,13 @@ export default function DocsVersionDropdownNavbarItem({
81157
description:
82158
'The label for the navbar versions dropdown on mobile view',
83159
})
84-
: dropdownVersion.label;
160+
: displayedVersionItem.label;
161+
85162
const dropdownTo =
86163
mobile && items.length > 1
87164
? undefined
88-
: getVersionTargetDoc(dropdownVersion, activeDocContext).path;
165+
: getVersionTargetDoc(displayedVersionItem.version, activeDocContext)
166+
.path;
89167

90168
// We don't want to render a version dropdown with 0 or 1 item. If we build
91169
// the site with a single docs version (onlyIncludeVersions: ['1.0.0']),

website/docs/api/themes/theme-configuration.mdx

+12
Original file line numberDiff line numberDiff line change
@@ -597,11 +597,23 @@ Accepted fields:
597597
| `dropdownItemsAfter` | <code>[LinkLikeItem](#navbar-dropdown)[]</code> | `[]` | Add additional dropdown items at the end of the dropdown. |
598598
| `docsPluginId` | `string` | `'default'` | The ID of the docs plugin that the doc versioning belongs to. |
599599
| `dropdownActiveClassDisabled` | `boolean` | `false` | Do not add the link active class when browsing docs. |
600+
| `versions` | `DropdownVersions` | `undefined` | Specify a custom list of versions to include in the dropdown. See [the versioning guide](../../guides/docs/versioning.mdx#docsVersionDropdown) for details. |
600601

601602
```mdx-code-block
602603
</APITable>
603604
```
604605

606+
Types:
607+
608+
```ts
609+
type DropdownVersion = {
610+
/** Allows you to provide a custom display label for each version. */
611+
label?: string;
612+
};
613+
614+
type DropdownVersions = string[] | {[versionName: string]: DropdownVersion};
615+
```
616+
605617
Example configuration:
606618

607619
```js title="docusaurus.config.js"

website/docs/guides/docs/versioning.mdx

+47-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ See [docs plugin configuration](../../api/plugins/plugin-content-docs.mdx#config
258258

259259
## Navbar items {#navbar-items}
260260

261-
We offer several navbar items to help you quickly set up navigation without worrying about versioned routes.
261+
We offer several docs navbar items to help you quickly set up navigation without worrying about versioned routes.
262262

263263
- [`doc`](../../api/themes/theme-configuration.mdx#navbar-doc-link): a link to a doc.
264264
- [`docSidebar`](../../api/themes/theme-configuration.mdx#navbar-doc-sidebar): a link to the first item in a sidebar.
@@ -271,6 +271,52 @@ These links would all look for an appropriate version to link to, in the followi
271271
2. **Preferred version**: the version that the user last viewed. If there's no history, fall back to...
272272
3. **Latest version**: the default version that we navigate to, configured by the `lastVersion` option.
273273

274+
## `docsVersionDropdown` {#docsVersionDropdown}
275+
276+
By default, the [`docsVersionDropdown`](../../api/themes/theme-configuration.mdx#navbar-docs-version-dropdown) displays a dropdown with all the available docs versions.
277+
278+
The `versions` attribute allows you to display a subset of the available docs versions in a given order:
279+
280+
```js title="docusaurus.config.js"
281+
export default {
282+
themeConfig: {
283+
navbar: {
284+
items: [
285+
{
286+
type: 'docsVersionDropdown',
287+
// highlight-start
288+
versions: ['current', '3.0', '2.0'],
289+
// highlight-end
290+
},
291+
],
292+
},
293+
},
294+
};
295+
```
296+
297+
Passing a `versions` object, lets you override the display label of each version:
298+
299+
```js title="docusaurus.config.js"
300+
export default {
301+
themeConfig: {
302+
navbar: {
303+
items: [
304+
{
305+
type: 'docsVersionDropdown',
306+
// highlight-start
307+
versions: {
308+
current: {label: 'Version 4.0'},
309+
'3.0': {label: 'Version 3.0'},
310+
'2.0': {label: 'Version 2.0'},
311+
},
312+
// highlight-end
313+
},
314+
],
315+
},
316+
},
317+
};
318+
```
319+
274320
## Recommended practices {#recommended-practices}
275321

276322
### Version your documentation only when needed {#version-your-documentation-only-when-needed}

0 commit comments

Comments
 (0)