diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index ad361efcb4..7c807019c7 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -275,6 +275,40 @@ declare module '@theme/DocPage' { export default function DocPage(props: Props): JSX.Element; } +declare module '@theme/DocPage/Layout' { + import type {ReactNode} from 'react'; + + export interface Props { + children: ReactNode; + } + + export default function DocPageLayout(props: Props): JSX.Element; +} + +declare module '@theme/DocPage/Layout/Aside' { + import type {Dispatch, SetStateAction} from 'react'; + import type {PropSidebar} from '@docusaurus/plugin-content-docs'; + + export interface Props { + sidebar: PropSidebar; + hiddenSidebarContainer: boolean; + setHiddenSidebarContainer: Dispatch>; + } + + export default function DocPageLayoutAside(props: Props): JSX.Element; +} + +declare module '@theme/DocPage/Layout/Main' { + import type {ReactNode} from 'react'; + + export interface Props { + hiddenSidebarContainer: boolean; + children: ReactNode; + } + + export default function DocPageLayoutMain(props: Props): JSX.Element; +} + // TODO until TS supports exports field... hope it's in 4.6 declare module '@docusaurus/plugin-content-docs/client' { export type ActivePlugin = { diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/Aside.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/Aside.tsx new file mode 100644 index 0000000000..236382eae4 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/Aside.tsx @@ -0,0 +1,100 @@ +/** + * 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, {type ReactNode, useState, useCallback} from 'react'; +import DocSidebar from '@theme/DocSidebar'; +import IconArrow from '@theme/IconArrow'; +import {translate} from '@docusaurus/Translate'; +import {useLocation} from '@docusaurus/router'; +import type {Props} from '@theme/DocPage/Layout/Aside'; + +import clsx from 'clsx'; +import styles from './styles.module.css'; + +import {ThemeClassNames, useDocsSidebar} from '@docusaurus/theme-common'; + +function SidebarExpandButton({toggleSidebar}: {toggleSidebar: () => void}) { + return ( +
+ +
+ ); +} + +// Reset sidebar state when sidebar changes +// Use React key to unmount/remount the children +// See https://github.com/facebook/docusaurus/issues/3414 +function ResetOnSidebarChange({children}: {children: ReactNode}) { + const sidebar = useDocsSidebar(); + return ( + + {children} + + ); +} + +export default function DocPageLayoutAside({ + sidebar, + hiddenSidebarContainer, + setHiddenSidebarContainer, +}: Props): JSX.Element { + const {pathname} = useLocation(); + + const [hiddenSidebar, setHiddenSidebar] = useState(false); + const toggleSidebar = useCallback(() => { + if (hiddenSidebar) { + setHiddenSidebar(false); + } + setHiddenSidebarContainer((value) => !value); + }, [setHiddenSidebarContainer, hiddenSidebar]); + + return ( + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/Main.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/Main.tsx new file mode 100644 index 0000000000..3cc4f0479e --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/Main.tsx @@ -0,0 +1,37 @@ +/** + * 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 {useDocsSidebar} from '@docusaurus/theme-common'; + +import clsx from 'clsx'; +import styles from './styles.module.css'; +import type {Props} from '@theme/DocPage/Layout/Main'; + +export default function DocPageLayoutMain({ + hiddenSidebarContainer, + children, +}: Props): JSX.Element { + const sidebar = useDocsSidebar(); + return ( +
+
+ {children} +
+
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/index.tsx new file mode 100644 index 0000000000..77d1488118 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/index.tsx @@ -0,0 +1,39 @@ +/** + * 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, {useState} from 'react'; +import Layout from '@theme/Layout'; +import BackToTopButton from '@theme/BackToTopButton'; +import type {Props} from '@theme/DocPage/Layout'; +import DocPageLayoutAside from '@theme/DocPage/Layout/Aside'; +import DocPageLayoutMain from '@theme/DocPage/Layout/Main'; + +import styles from './styles.module.css'; + +import {useDocsSidebar} from '@docusaurus/theme-common'; + +export default function DocPageLayout({children}: Props): JSX.Element { + const sidebar = useDocsSidebar(); + const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false); + return ( + + +
+ {sidebar && ( + + )} + + {children} + +
+
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocPage/Layout/styles.module.css similarity index 100% rename from packages/docusaurus-theme-classic/src/theme/DocPage/styles.module.css rename to packages/docusaurus-theme-classic/src/theme/DocPage/Layout/styles.module.css diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index d75ffa55a5..8ee721f986 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -5,145 +5,30 @@ * LICENSE file in the root directory of this source tree. */ -import React, {type ReactNode, useState, useCallback} from 'react'; +import React from 'react'; import renderRoutes from '@docusaurus/renderRoutes'; -import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs'; -import Layout from '@theme/Layout'; -import DocSidebar from '@theme/DocSidebar'; +import type {PropSidebar} from '@docusaurus/plugin-content-docs'; import NotFound from '@theme/NotFound'; -import type {DocumentRoute} from '@theme/DocItem'; import type {Props} from '@theme/DocPage'; -import IconArrow from '@theme/IconArrow'; -import BackToTopButton from '@theme/BackToTopButton'; +import DocPageLayout from '@theme/DocPage/Layout'; import {matchPath} from '@docusaurus/router'; -import {translate} from '@docusaurus/Translate'; import clsx from 'clsx'; -import styles from './styles.module.css'; import { HtmlClassNameProvider, ThemeClassNames, docVersionSearchTag, DocsSidebarProvider, - useDocsSidebar, DocsVersionProvider, } from '@docusaurus/theme-common'; import SearchMetadata from '@theme/SearchMetadata'; -type DocPageContentProps = { - readonly currentDocRoute: DocumentRoute; - readonly versionMetadata: PropVersionMetadata; - readonly children: ReactNode; - readonly sidebarName: string | undefined; -}; - -function DocPageContent({ - currentDocRoute, - versionMetadata, - children, - sidebarName, -}: DocPageContentProps): JSX.Element { - const sidebar = useDocsSidebar(); - const {pluginId, version} = versionMetadata; - const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false); - const [hiddenSidebar, setHiddenSidebar] = useState(false); - const toggleSidebar = useCallback(() => { - if (hiddenSidebar) { - setHiddenSidebar(false); - } - - setHiddenSidebarContainer((value) => !value); - }, [hiddenSidebar]); - - return ( - <> - - -
- - - {sidebar && ( - - )} -
-
- {children} -
-
-
-
- - ); -} - -export default function DocPage(props: Props): JSX.Element { +function extractDocRouteMetadata(props: Props): null | { + docElement: JSX.Element; + sidebarName: string | undefined; + sidebarItems: PropSidebar | undefined; +} { const { route: {routes: docRoutes}, versionMetadata, @@ -153,33 +38,55 @@ export default function DocPage(props: Props): JSX.Element { matchPath(location.pathname, docRoute), ); if (!currentDocRoute) { - return ; + return null; } // For now, the sidebarName is added as route config: not ideal! const sidebarName = currentDocRoute.sidebar; - const sidebar = sidebarName + const sidebarItems = sidebarName ? versionMetadata.docsSidebars[sidebarName] - : null; + : undefined; + const docElement = renderRoutes(props.route.routes, { + versionMetadata, + }); + + return { + docElement, + sidebarName, + sidebarItems, + }; +} + +export default function DocPage(props: Props): JSX.Element { + const {versionMetadata} = props; + const currentDocRouteMetadata = extractDocRouteMetadata(props); + if (!currentDocRouteMetadata) { + return ; + } + const {docElement, sidebarName, sidebarItems} = currentDocRouteMetadata; return ( - - - - - {renderRoutes(docRoutes, {versionMetadata})} - - - - + <> + + + + + {docElement} + + + + ); } diff --git a/packages/docusaurus-theme-common/src/contexts/__tests__/docsSidebar.test.tsx b/packages/docusaurus-theme-common/src/contexts/__tests__/docsSidebar.test.tsx index 5d66881b7d..2082a9779a 100644 --- a/packages/docusaurus-theme-common/src/contexts/__tests__/docsSidebar.test.tsx +++ b/packages/docusaurus-theme-common/src/contexts/__tests__/docsSidebar.test.tsx @@ -13,19 +13,24 @@ import type {PropSidebar} from '@docusaurus/plugin-content-docs'; describe('useDocsSidebar', () => { it('throws if context provider is missing', () => { expect( - () => renderHook(() => useDocsSidebar()).result.current, + () => renderHook(() => useDocsSidebar()).result.current?.items, ).toThrowErrorMatchingInlineSnapshot( `"Hook useDocsSidebar is called outside the . "`, ); }); it('reads value from context provider', () => { - const sidebar: PropSidebar = []; + const name = 'mySidebarName'; + const items: PropSidebar = []; const {result} = renderHook(() => useDocsSidebar(), { wrapper: ({children}) => ( - {children} + + {children} + ), }); - expect(result.current).toBe(sidebar); + expect(result.current).toBeDefined(); + expect(result.current!.name).toBe(name); + expect(result.current!.items).toBe(items); }); }); diff --git a/packages/docusaurus-theme-common/src/contexts/docsSidebar.tsx b/packages/docusaurus-theme-common/src/contexts/docsSidebar.tsx index 4d2f7caa61..2fb4d1cb0f 100644 --- a/packages/docusaurus-theme-common/src/contexts/docsSidebar.tsx +++ b/packages/docusaurus-theme-common/src/contexts/docsSidebar.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, {type ReactNode, useContext} from 'react'; +import React, {useMemo, useContext, type ReactNode} from 'react'; import type {PropSidebar} from '@docusaurus/plugin-content-docs'; import {ReactContextError} from '../utils/reactUtils'; @@ -13,30 +13,44 @@ import {ReactContextError} from '../utils/reactUtils'; // Inspired by https://github.com/jamiebuilds/unstated-next/blob/master/src/unstated-next.tsx const EmptyContext: unique symbol = Symbol('EmptyContext'); -const Context = React.createContext( - EmptyContext, -); +type SidebarContextValue = {name: string; items: PropSidebar}; + +const Context = React.createContext< + SidebarContextValue | null | typeof EmptyContext +>(EmptyContext); /** * Provide the current sidebar to your children. */ export function DocsSidebarProvider({ children, - sidebar, + name, + items, }: { children: ReactNode; - sidebar: PropSidebar | null; + name: string | undefined; + items: PropSidebar | undefined; }): JSX.Element { - return {children}; + const stableValue: SidebarContextValue | null = useMemo( + () => + name && items + ? { + name, + items, + } + : null, + [name, items], + ); + return {children}; } /** - * Gets the sidebar that's currently displayed, or `null` if there isn't one + * Gets the sidebar data that's currently displayed, or `null` if there isn't one */ -export function useDocsSidebar(): PropSidebar | null { - const sidebar = useContext(Context); - if (sidebar === EmptyContext) { +export function useDocsSidebar(): SidebarContextValue | null { + const value = useContext(Context); + if (value === EmptyContext) { throw new ReactContextError('DocsSidebarProvider'); } - return sidebar; + return value; } diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx b/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx index 4fd8df849b..532fb20a0a 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx +++ b/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx @@ -307,7 +307,7 @@ describe('useSidebarBreadcrumbs', () => { }, }, }}> - + {children} @@ -430,7 +430,7 @@ describe('useCurrentSidebarCategory', () => { (sidebar?: PropSidebar) => (location: string) => renderHook(() => useCurrentSidebarCategory(), { wrapper: ({children}) => ( - + {children} ), diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index 2a16dc6f8e..e5e54478b4 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -105,7 +105,7 @@ export function useCurrentSidebarCategory(): PropSidebarItemCategory { if (!sidebar) { throw new Error('Unexpected: cant find current sidebar in context'); } - const category = findSidebarCategory(sidebar, (item) => + const category = findSidebarCategory(sidebar.items, (item) => isSamePath(item.href, pathname), ); if (!category) { @@ -174,7 +174,7 @@ export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null { return false; } - extract(sidebar); + extract(sidebar.items); return breadcrumbs.reverse(); }