diff --git a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts index 7ac6a7eda81e..ac986bd0dfda 100644 --- a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts +++ b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts @@ -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', () => { const config = { navbar: { diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index 9e73ef9a17e4..0c34618bdc32 100644 --- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts @@ -206,6 +206,14 @@ export default function getSwizzleConfig(): SwizzleConfig { description: '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 'NavbarItem/utils': { actions: { diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index eec106ba5e1b..a16f02a0933a 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -893,6 +893,37 @@ declare module '@theme/NavbarItem/HtmlNavbarItem' { 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; + }; + + const ComponentTypes: ComponentTypesObject; + export default ComponentTypes; +} + declare module '@theme/NavbarItem' { import type {ComponentProps} from 'react'; import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem'; diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/ComponentTypes.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/ComponentTypes.tsx new file mode 100644 index 000000000000..a06832505558 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/ComponentTypes.tsx @@ -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; diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/index.tsx index 7c3c9b07b56f..7ae0d4990730 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/index.tsx @@ -6,57 +6,26 @@ */ import React from 'react'; -import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; -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 Props as DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem'; import type {Types, Props} from '@theme/NavbarItem'; -const NavbarItemComponents: { - // Not really worth typing, as we pass all props down immediately - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [type in Exclude]: () => (props: any) => JSX.Element; -} = { - default: () => DefaultNavbarItem, - localeDropdown: () => LocaleDropdownNavbarItem, - search: () => SearchNavbarItem, - dropdown: () => DropdownNavbarItem, - html: () => HtmlNavbarItem, +import ComponentTypes from '@theme/NavbarItem/ComponentTypes'; - // Need to lazy load these items as we don't know for sure the docs plugin is - // loaded. See https://github.com/facebook/docusaurus/issues/3360 - /* eslint-disable @typescript-eslint/no-var-requires, global-require */ - 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) { +const getNavbarItemComponent = (type: NonNullable) => { + const component = ComponentTypes[type]; + if (!component) { throw new Error(`No NavbarItem component found for type "${type}".`); } - return navbarItemComponentFn(); + return component; }; -function getComponentType( - type: Types, - isDropdown: boolean, -): NavbarItemComponentType { +function getComponentType(type: Types, isDropdown: boolean) { // Backward compatibility: navbar item with no type set // but containing dropdown items should use the type "dropdown" if (!type || type === 'default') { return isDropdown ? 'dropdown' : 'default'; } - return type as NavbarItemComponentType; + return type as NonNullable; } 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, ); const NavbarItemComponent = getNavbarItemComponent(componentType); - return ; + return ; } diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts index 9a5cd826cc43..cb77452daf2e 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts @@ -99,11 +99,22 @@ const HtmlNavbarItemSchema = Joi.object({ 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 :/ - const typeSchema = type - ? Joi.string().required().equal(type) - : Joi.string().forbidden(); + const typeSchema = + // eslint-disable-next-line no-nested-ternary + type instanceof RegExp + ? Joi.string().required().regex(type) + : type + ? Joi.string().required().equal(type) + : Joi.string().forbidden(); return Joi.object({ type: typeSchema, }) @@ -135,6 +146,10 @@ const DropdownSubitemSchema = Joi.object({ is: itemWithType('html'), then: HtmlNavbarItemSchema, }, + { + is: itemWithType(CustomNavbarItemRegexp), + then: CustomNavbarItemSchema, + }, { is: Joi.alternatives().try( itemWithType('dropdown'), @@ -210,6 +225,10 @@ const NavbarItemSchema = Joi.object({ is: itemWithType('html'), then: HtmlNavbarItemSchema, }, + { + is: itemWithType(CustomNavbarItemRegexp), + then: CustomNavbarItemSchema, + }, { is: itemWithType(undefined), then: Joi.object().when('.', { diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index df57e09ac88c..b8b8f8d4a098 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -409,12 +409,18 @@ const config = { position: 'left', activeBaseRegex: `/community/`, }, + // This item links to a draft doc: only displayed in dev { type: 'doc', docId: 'test-draft', label: 'Tests', docsPluginId: 'docs-tests', }, + // Custom item for dogfooding: only displayed in /tests/ routes + { + type: 'custom-dogfood-navbar-item', + content: '😉', + }, // Right { type: 'docsVersionDropdown', diff --git a/website/src/components/NavbarItems/CustomDogfoodNavbarItem.tsx b/website/src/components/NavbarItems/CustomDogfoodNavbarItem.tsx new file mode 100644 index 000000000000..71eb6a2a6a07 --- /dev/null +++ b/website/src/components/NavbarItems/CustomDogfoodNavbarItem.tsx @@ -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 ( + + ); +} diff --git a/website/src/theme/NavbarItem/ComponentTypes.tsx b/website/src/theme/NavbarItem/ComponentTypes.tsx new file mode 100644 index 000000000000..85937ca85adf --- /dev/null +++ b/website/src/theme/NavbarItem/ComponentTypes.tsx @@ -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, +};