mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-28 14:08:21 +02:00
feat: custom navbarItem types (workaround) (#7231)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
This commit is contained in:
parent
0ffdfe9c22
commit
6265f6dabb
9 changed files with 194 additions and 44 deletions
|
@ -227,6 +227,44 @@ describe('themeConfig', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accept "custom-" prefixed custom navbar item type', () => {
|
||||||
|
const config = {
|
||||||
|
navbar: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'custom-x',
|
||||||
|
position: 'left',
|
||||||
|
xyz: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dropdown with custom item',
|
||||||
|
position: 'right',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Facebook',
|
||||||
|
href: 'https://.facebook.com/',
|
||||||
|
target: '_self',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'custom-y',
|
||||||
|
any: new Date(),
|
||||||
|
prop: 42,
|
||||||
|
isAccepted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(testValidateThemeConfig(config)).toEqual({
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
navbar: {
|
||||||
|
...DEFAULT_CONFIG.navbar,
|
||||||
|
...config.navbar,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects unknown navbar item type', () => {
|
it('rejects unknown navbar item type', () => {
|
||||||
const config = {
|
const config = {
|
||||||
navbar: {
|
navbar: {
|
||||||
|
|
|
@ -206,6 +206,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
|
||||||
description:
|
description:
|
||||||
'A component wrapping all MDX content and providing the MDXComponents to the MDX context',
|
'A component wrapping all MDX content and providing the MDXComponents to the MDX context',
|
||||||
},
|
},
|
||||||
|
'NavbarItem/ComponentTypes': {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'forbidden',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The Navbar item components mapping. Can be ejected to add custom navbar item types. See https://github.com/facebook/docusaurus/issues/7227.',
|
||||||
|
},
|
||||||
// TODO should probably not even appear here
|
// TODO should probably not even appear here
|
||||||
'NavbarItem/utils': {
|
'NavbarItem/utils': {
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -893,6 +893,37 @@ declare module '@theme/NavbarItem/HtmlNavbarItem' {
|
||||||
export default function HtmlNavbarItem(props: Props): JSX.Element;
|
export default function HtmlNavbarItem(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/NavbarItem/ComponentTypes' {
|
||||||
|
import type {ComponentType} from 'react';
|
||||||
|
|
||||||
|
import type DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
import type DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
|
import type LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
||||||
|
import type SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
|
||||||
|
import type HtmlNavbarItem from '@theme/NavbarItem/HtmlNavbarItem';
|
||||||
|
import type DocNavbarItem from '@theme/NavbarItem/DocNavbarItem';
|
||||||
|
import type DocSidebarNavbarItem from '@theme/NavbarItem/DocSidebarNavbarItem';
|
||||||
|
import type DocsVersionNavbarItem from '@theme/NavbarItem/DocsVersionNavbarItem';
|
||||||
|
import type DocsVersionDropdownNavbarItem from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
|
||||||
|
|
||||||
|
export type ComponentTypesObject = {
|
||||||
|
readonly default: typeof DefaultNavbarItem;
|
||||||
|
readonly localeDropdown: typeof LocaleDropdownNavbarItem;
|
||||||
|
readonly search: typeof SearchNavbarItem;
|
||||||
|
readonly dropdown: typeof DropdownNavbarItem;
|
||||||
|
readonly html: typeof HtmlNavbarItem;
|
||||||
|
readonly doc: typeof DocNavbarItem;
|
||||||
|
readonly docSidebar: typeof DocSidebarNavbarItem;
|
||||||
|
readonly docsVersion: typeof DocsVersionNavbarItem;
|
||||||
|
readonly docsVersionDropdown: typeof DocsVersionDropdownNavbarItem;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[customComponentType: string]: ComponentType<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ComponentTypes: ComponentTypesObject;
|
||||||
|
export default ComponentTypes;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/NavbarItem' {
|
declare module '@theme/NavbarItem' {
|
||||||
import type {ComponentProps} from 'react';
|
import type {ComponentProps} from 'react';
|
||||||
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
|
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
||||||
|
import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
|
||||||
|
import HtmlNavbarItem from '@theme/NavbarItem/HtmlNavbarItem';
|
||||||
|
import DocNavbarItem from '@theme/NavbarItem/DocNavbarItem';
|
||||||
|
import DocSidebarNavbarItem from '@theme/NavbarItem/DocSidebarNavbarItem';
|
||||||
|
import DocsVersionNavbarItem from '@theme/NavbarItem/DocsVersionNavbarItem';
|
||||||
|
import DocsVersionDropdownNavbarItem from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
|
||||||
|
|
||||||
|
import type {ComponentTypesObject} from '@theme/NavbarItem/ComponentTypes';
|
||||||
|
|
||||||
|
const ComponentTypes: ComponentTypesObject = {
|
||||||
|
default: DefaultNavbarItem,
|
||||||
|
localeDropdown: LocaleDropdownNavbarItem,
|
||||||
|
search: SearchNavbarItem,
|
||||||
|
dropdown: DropdownNavbarItem,
|
||||||
|
html: HtmlNavbarItem,
|
||||||
|
doc: DocNavbarItem,
|
||||||
|
docSidebar: DocSidebarNavbarItem,
|
||||||
|
docsVersion: DocsVersionNavbarItem,
|
||||||
|
docsVersionDropdown: DocsVersionDropdownNavbarItem,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComponentTypes;
|
|
@ -6,57 +6,26 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
import {type Props as DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
import DropdownNavbarItem, {
|
|
||||||
type Props as DropdownNavbarItemProps,
|
|
||||||
} from '@theme/NavbarItem/DropdownNavbarItem';
|
|
||||||
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
|
||||||
import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
|
|
||||||
import HtmlNavbarItem from '@theme/NavbarItem/HtmlNavbarItem';
|
|
||||||
import type {Types, Props} from '@theme/NavbarItem';
|
import type {Types, Props} from '@theme/NavbarItem';
|
||||||
|
|
||||||
const NavbarItemComponents: {
|
import ComponentTypes from '@theme/NavbarItem/ComponentTypes';
|
||||||
// Not really worth typing, as we pass all props down immediately
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
[type in Exclude<Types, undefined>]: () => (props: any) => JSX.Element;
|
|
||||||
} = {
|
|
||||||
default: () => DefaultNavbarItem,
|
|
||||||
localeDropdown: () => LocaleDropdownNavbarItem,
|
|
||||||
search: () => SearchNavbarItem,
|
|
||||||
dropdown: () => DropdownNavbarItem,
|
|
||||||
html: () => HtmlNavbarItem,
|
|
||||||
|
|
||||||
// Need to lazy load these items as we don't know for sure the docs plugin is
|
const getNavbarItemComponent = (type: NonNullable<Types>) => {
|
||||||
// loaded. See https://github.com/facebook/docusaurus/issues/3360
|
const component = ComponentTypes[type];
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires, global-require */
|
if (!component) {
|
||||||
docsVersion: () => require('@theme/NavbarItem/DocsVersionNavbarItem').default,
|
|
||||||
docsVersionDropdown: () =>
|
|
||||||
require('@theme/NavbarItem/DocsVersionDropdownNavbarItem').default,
|
|
||||||
doc: () => require('@theme/NavbarItem/DocNavbarItem').default,
|
|
||||||
docSidebar: () => require('@theme/NavbarItem/DocSidebarNavbarItem').default,
|
|
||||||
/* eslint-enable @typescript-eslint/no-var-requires, global-require */
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type NavbarItemComponentType = keyof typeof NavbarItemComponents;
|
|
||||||
|
|
||||||
const getNavbarItemComponent = (type: NavbarItemComponentType) => {
|
|
||||||
const navbarItemComponentFn = NavbarItemComponents[type];
|
|
||||||
if (!navbarItemComponentFn) {
|
|
||||||
throw new Error(`No NavbarItem component found for type "${type}".`);
|
throw new Error(`No NavbarItem component found for type "${type}".`);
|
||||||
}
|
}
|
||||||
return navbarItemComponentFn();
|
return component;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getComponentType(
|
function getComponentType(type: Types, isDropdown: boolean) {
|
||||||
type: Types,
|
|
||||||
isDropdown: boolean,
|
|
||||||
): NavbarItemComponentType {
|
|
||||||
// Backward compatibility: navbar item with no type set
|
// Backward compatibility: navbar item with no type set
|
||||||
// but containing dropdown items should use the type "dropdown"
|
// but containing dropdown items should use the type "dropdown"
|
||||||
if (!type || type === 'default') {
|
if (!type || type === 'default') {
|
||||||
return isDropdown ? 'dropdown' : 'default';
|
return isDropdown ? 'dropdown' : 'default';
|
||||||
}
|
}
|
||||||
return type as NavbarItemComponentType;
|
return type as NonNullable<Types>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavbarItem({type, ...props}: Props): JSX.Element {
|
export default function NavbarItem({type, ...props}: Props): JSX.Element {
|
||||||
|
@ -65,5 +34,5 @@ export default function NavbarItem({type, ...props}: Props): JSX.Element {
|
||||||
(props as DropdownNavbarItemProps).items !== undefined,
|
(props as DropdownNavbarItemProps).items !== undefined,
|
||||||
);
|
);
|
||||||
const NavbarItemComponent = getNavbarItemComponent(componentType);
|
const NavbarItemComponent = getNavbarItemComponent(componentType);
|
||||||
return <NavbarItemComponent {...props} />;
|
return <NavbarItemComponent {...(props as never)} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,11 +99,22 @@ const HtmlNavbarItemSchema = Joi.object({
|
||||||
value: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemWithType = (type: string | undefined) => {
|
// A temporary workaround to allow users to add custom navbar items
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/7227
|
||||||
|
const CustomNavbarItemRegexp = /custom-.*/;
|
||||||
|
const CustomNavbarItemSchema = Joi.object({
|
||||||
|
type: Joi.string().regex(CustomNavbarItemRegexp).required(),
|
||||||
|
}).unknown();
|
||||||
|
|
||||||
|
const itemWithType = (type: string | RegExp | undefined) => {
|
||||||
// Because equal(undefined) is not supported :/
|
// Because equal(undefined) is not supported :/
|
||||||
const typeSchema = type
|
const typeSchema =
|
||||||
? Joi.string().required().equal(type)
|
// eslint-disable-next-line no-nested-ternary
|
||||||
: Joi.string().forbidden();
|
type instanceof RegExp
|
||||||
|
? Joi.string().required().regex(type)
|
||||||
|
: type
|
||||||
|
? Joi.string().required().equal(type)
|
||||||
|
: Joi.string().forbidden();
|
||||||
return Joi.object({
|
return Joi.object({
|
||||||
type: typeSchema,
|
type: typeSchema,
|
||||||
})
|
})
|
||||||
|
@ -135,6 +146,10 @@ const DropdownSubitemSchema = Joi.object({
|
||||||
is: itemWithType('html'),
|
is: itemWithType('html'),
|
||||||
then: HtmlNavbarItemSchema,
|
then: HtmlNavbarItemSchema,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
is: itemWithType(CustomNavbarItemRegexp),
|
||||||
|
then: CustomNavbarItemSchema,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
is: Joi.alternatives().try(
|
is: Joi.alternatives().try(
|
||||||
itemWithType('dropdown'),
|
itemWithType('dropdown'),
|
||||||
|
@ -210,6 +225,10 @@ const NavbarItemSchema = Joi.object({
|
||||||
is: itemWithType('html'),
|
is: itemWithType('html'),
|
||||||
then: HtmlNavbarItemSchema,
|
then: HtmlNavbarItemSchema,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
is: itemWithType(CustomNavbarItemRegexp),
|
||||||
|
then: CustomNavbarItemSchema,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
is: itemWithType(undefined),
|
is: itemWithType(undefined),
|
||||||
then: Joi.object().when('.', {
|
then: Joi.object().when('.', {
|
||||||
|
|
|
@ -409,12 +409,18 @@ const config = {
|
||||||
position: 'left',
|
position: 'left',
|
||||||
activeBaseRegex: `/community/`,
|
activeBaseRegex: `/community/`,
|
||||||
},
|
},
|
||||||
|
// This item links to a draft doc: only displayed in dev
|
||||||
{
|
{
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
docId: 'test-draft',
|
docId: 'test-draft',
|
||||||
label: 'Tests',
|
label: 'Tests',
|
||||||
docsPluginId: 'docs-tests',
|
docsPluginId: 'docs-tests',
|
||||||
},
|
},
|
||||||
|
// Custom item for dogfooding: only displayed in /tests/ routes
|
||||||
|
{
|
||||||
|
type: 'custom-dogfood-navbar-item',
|
||||||
|
content: '😉',
|
||||||
|
},
|
||||||
// Right
|
// Right
|
||||||
{
|
{
|
||||||
type: 'docsVersionDropdown',
|
type: 'docsVersionDropdown',
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {useLocation} from '@docusaurus/router';
|
||||||
|
|
||||||
|
// used to dogfood custom navbar elements are possible
|
||||||
|
// see https://github.com/facebook/docusaurus/issues/7227
|
||||||
|
export default function CustomDogfoodNavbarItem(props: {
|
||||||
|
content: string;
|
||||||
|
mobile?: boolean;
|
||||||
|
}): JSX.Element | null {
|
||||||
|
const {pathname} = useLocation();
|
||||||
|
const shouldRender = pathname.includes('/tests/');
|
||||||
|
if (!shouldRender) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert("I'm a custom navbar item type example");
|
||||||
|
}}
|
||||||
|
type="button">
|
||||||
|
{props.content}
|
||||||
|
{props.mobile ? ' (mobile)' : ''}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
14
website/src/theme/NavbarItem/ComponentTypes.tsx
Normal file
14
website/src/theme/NavbarItem/ComponentTypes.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes';
|
||||||
|
import CustomDogfoodNavbarItem from '@site/src/components/NavbarItems/CustomDogfoodNavbarItem';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...ComponentTypes,
|
||||||
|
'custom-dogfood-navbar-item': CustomDogfoodNavbarItem,
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue