feat(theme): add versions attribute to docsVersionDropdown navbar item (#10852)

Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
Oleksiy Gapotchenko 2025-01-30 18:21:54 +01:00 committed by GitHub
parent 8bc3e8a092
commit 4d7a28963a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 276 additions and 10 deletions

View file

@ -827,6 +827,110 @@ describe('themeConfig', () => {
);
});
});
describe('docsVersionDropdown', () => {
describe('versions', () => {
it('accepts array of strings', () => {
const config = {
navbar: {
items: [
{
type: 'docsVersionDropdown',
versions: ['current', '1.0'],
},
],
},
};
testValidateThemeConfig(config);
});
it('rejects empty array of strings', () => {
const config = {
navbar: {
items: [
{
type: 'docsVersionDropdown',
versions: [],
},
],
},
};
expect(() =>
testValidateThemeConfig(config),
).toThrowErrorMatchingInlineSnapshot(
`""navbar.items[0].versions" must contain at least 1 items"`,
);
});
it('rejects array of non-strings', () => {
const config = {
navbar: {
items: [
{
type: 'docsVersionDropdown',
versions: [1, 2],
},
],
},
};
expect(() =>
testValidateThemeConfig(config),
).toThrowErrorMatchingInlineSnapshot(
`""navbar.items[0].versions[0]" must be a string"`,
);
});
it('accepts dictionary of version objects', () => {
const config = {
navbar: {
items: [
{
type: 'docsVersionDropdown',
versions: {current: {}, '1.0': {label: '1.x'}},
},
],
},
};
testValidateThemeConfig(config);
});
it('rejects empty dictionary of objects', () => {
const config = {
navbar: {
items: [
{
type: 'docsVersionDropdown',
versions: {},
},
],
},
};
expect(() =>
testValidateThemeConfig(config),
).toThrowErrorMatchingInlineSnapshot(
`""navbar.items[0].versions" must have at least 1 key"`,
);
});
it('rejects dictionary of invalid objects', () => {
const config = {
navbar: {
items: [
{
type: 'docsVersionDropdown',
versions: {current: {}, '1.0': {invalid: '1.x'}},
},
],
},
};
expect(() =>
testValidateThemeConfig(config),
).toThrowErrorMatchingInlineSnapshot(
`""navbar.items[0].versions.1.0.invalid" is not allowed"`,
);
});
});
});
});
describe('validateOptions', () => {

View file

@ -7,6 +7,10 @@
import {themes} from 'prism-react-renderer';
import {Joi, URISchema} from '@docusaurus/utils-validation';
import type {
PropVersionItem,
PropVersionItems,
} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
import type {Options, PluginOptions} from '@docusaurus/theme-classic';
import type {ThemeConfig} from '@docusaurus/theme-common';
import type {
@ -210,6 +214,17 @@ const DocsVersionDropdownNavbarItemSchema = NavbarItemBaseSchema.append({
dropdownActiveClassDisabled: Joi.boolean(),
dropdownItemsBefore: Joi.array().items(DropdownSubitemSchema).default([]),
dropdownItemsAfter: Joi.array().items(DropdownSubitemSchema).default([]),
versions: Joi.alternatives().try(
Joi.array().items(Joi.string().min(1)).min(1),
Joi.object<PropVersionItems>()
.pattern(
Joi.string().min(1),
Joi.object<PropVersionItem>({
label: Joi.string().min(1),
}),
)
.min(1),
),
});
const LocaleDropdownNavbarItemSchema = NavbarItemBaseSchema.append({

View file

@ -1257,11 +1257,22 @@ declare module '@theme/NavbarItem/DocsVersionDropdownNavbarItem' {
import type {Props as DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
type PropVersionItem = {
readonly label?: string;
};
type PropVersionItems = {
readonly [version: string]: PropVersionItem;
};
type PropVersions = string[] | PropVersionItems;
export interface Props extends DropdownNavbarItemProps {
readonly docsPluginId?: string;
readonly dropdownActiveClassDisabled?: boolean;
readonly dropdownItemsBefore: LinkLikeNavbarItemProps[];
readonly dropdownItemsAfter: LinkLikeNavbarItemProps[];
readonly versions?: PropVersions;
}
export default function DocsVersionDropdownNavbarItem(

View file

@ -16,7 +16,11 @@ import {translate} from '@docusaurus/Translate';
import {useLocation} from '@docusaurus/router';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import type {Props} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
import type {
Props,
PropVersions,
PropVersionItem,
} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
import type {
GlobalVersion,
@ -24,6 +28,56 @@ import type {
ActiveDocContext,
} from '@docusaurus/plugin-content-docs/client';
type VersionItem = {
version: GlobalVersion;
label: string;
};
function getVersionItems(
versions: GlobalVersion[],
configs?: PropVersions,
): VersionItem[] {
if (configs) {
// Collect all the versions we have
const versionMap = new Map<string, GlobalVersion>(
versions.map((version) => [version.name, version]),
);
const toVersionItem = (
name: string,
config?: PropVersionItem,
): VersionItem => {
const version = versionMap.get(name);
if (!version) {
throw new Error(`No docs version exist for name '${name}', please verify your 'docsVersionDropdown' navbar item versions config.
Available version names:\n- ${versions.map((v) => `${v.name}`).join('\n- ')}`);
}
return {version, label: config?.label ?? version.label};
};
if (Array.isArray(configs)) {
return configs.map((name) => toVersionItem(name, undefined));
} else {
return Object.entries(configs).map(([name, config]) =>
toVersionItem(name, config),
);
}
} else {
return versions.map((version) => ({version, label: version.label}));
}
}
function useVersionItems({
docsPluginId,
configs,
}: {
docsPluginId: Props['docsPluginId'];
configs: Props['versions'];
}): VersionItem[] {
const versions = useVersions(docsPluginId);
return getVersionItems(versions, configs);
}
function getVersionMainDoc(version: GlobalVersion): GlobalDoc {
return version.docs.find((doc) => doc.id === version.mainDocId)!;
}
@ -40,23 +94,47 @@ function getVersionTargetDoc(
);
}
// The version item to use for the "dropdown button"
function useDisplayedVersionItem({
docsPluginId,
versionItems,
}: {
docsPluginId: Props['docsPluginId'];
versionItems: VersionItem[];
}): VersionItem {
// The order of the candidates matters!
const candidates = useDocsVersionCandidates(docsPluginId);
const candidateItems = candidates
.map((candidate) => versionItems.find((vi) => vi.version === candidate))
.filter((vi) => vi !== undefined);
return candidateItems[0] ?? versionItems[0]!;
}
export default function DocsVersionDropdownNavbarItem({
mobile,
docsPluginId,
dropdownActiveClassDisabled,
dropdownItemsBefore,
dropdownItemsAfter,
versions: configs,
...props
}: Props): ReactNode {
const {search, hash} = useLocation();
const activeDocContext = useActiveDocContext(docsPluginId);
const versions = useVersions(docsPluginId);
const {savePreferredVersionName} = useDocsPreferredVersion(docsPluginId);
const versionItems = useVersionItems({docsPluginId, configs});
const displayedVersionItem = useDisplayedVersionItem({
docsPluginId,
versionItems,
});
function versionToLink(version: GlobalVersion): LinkLikeNavbarItemProps {
function versionItemToLink({
version,
label,
}: VersionItem): LinkLikeNavbarItemProps {
const targetDoc = getVersionTargetDoc(version, activeDocContext);
return {
label: version.label,
label,
// preserve ?search#hash suffix on version switches
to: `${targetDoc.path}${search}${hash}`,
isActive: () => version === activeDocContext.activeVersion,
@ -66,12 +144,10 @@ export default function DocsVersionDropdownNavbarItem({
const items: LinkLikeNavbarItemProps[] = [
...dropdownItemsBefore,
...versions.map(versionToLink),
...versionItems.map(versionItemToLink),
...dropdownItemsAfter,
];
const dropdownVersion = useDocsVersionCandidates(docsPluginId)[0];
// Mobile dropdown is handled a bit differently
const dropdownLabel =
mobile && items.length > 1
@ -81,11 +157,13 @@ export default function DocsVersionDropdownNavbarItem({
description:
'The label for the navbar versions dropdown on mobile view',
})
: dropdownVersion.label;
: displayedVersionItem.label;
const dropdownTo =
mobile && items.length > 1
? undefined
: getVersionTargetDoc(dropdownVersion, activeDocContext).path;
: getVersionTargetDoc(displayedVersionItem.version, activeDocContext)
.path;
// We don't want to render a version dropdown with 0 or 1 item. If we build
// the site with a single docs version (onlyIncludeVersions: ['1.0.0']),

View file

@ -597,11 +597,23 @@ Accepted fields:
| `dropdownItemsAfter` | <code>[LinkLikeItem](#navbar-dropdown)[]</code> | `[]` | Add additional dropdown items at the end of the dropdown. |
| `docsPluginId` | `string` | `'default'` | The ID of the docs plugin that the doc versioning belongs to. |
| `dropdownActiveClassDisabled` | `boolean` | `false` | Do not add the link active class when browsing docs. |
| `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. |
```mdx-code-block
</APITable>
```
Types:
```ts
type DropdownVersion = {
/** Allows you to provide a custom display label for each version. */
label?: string;
};
type DropdownVersions = string[] | {[versionName: string]: DropdownVersion};
```
Example configuration:
```js title="docusaurus.config.js"

View file

@ -258,7 +258,7 @@ See [docs plugin configuration](../../api/plugins/plugin-content-docs.mdx#config
## Navbar items {#navbar-items}
We offer several navbar items to help you quickly set up navigation without worrying about versioned routes.
We offer several docs navbar items to help you quickly set up navigation without worrying about versioned routes.
- [`doc`](../../api/themes/theme-configuration.mdx#navbar-doc-link): a link to a doc.
- [`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
2. **Preferred version**: the version that the user last viewed. If there's no history, fall back to...
3. **Latest version**: the default version that we navigate to, configured by the `lastVersion` option.
## `docsVersionDropdown` {#docsVersionDropdown}
By default, the [`docsVersionDropdown`](../../api/themes/theme-configuration.mdx#navbar-docs-version-dropdown) displays a dropdown with all the available docs versions.
The `versions` attribute allows you to display a subset of the available docs versions in a given order:
```js title="docusaurus.config.js"
export default {
themeConfig: {
navbar: {
items: [
{
type: 'docsVersionDropdown',
// highlight-start
versions: ['current', '3.0', '2.0'],
// highlight-end
},
],
},
},
};
```
Passing a `versions` object, lets you override the display label of each version:
```js title="docusaurus.config.js"
export default {
themeConfig: {
navbar: {
items: [
{
type: 'docsVersionDropdown',
// highlight-start
versions: {
current: {label: 'Version 4.0'},
'3.0': {label: 'Version 3.0'},
'2.0': {label: 'Version 2.0'},
},
// highlight-end
},
],
},
},
};
```
## Recommended practices {#recommended-practices}
### Version your documentation only when needed {#version-your-documentation-only-when-needed}