refactor(docs,theme): split DocItem comp, useDoc hook (#7644)

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
This commit is contained in:
Sébastien Lorber 2022-06-22 14:27:23 +02:00 committed by GitHub
parent 2316900174
commit fd87afd249
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 357 additions and 144 deletions

View file

@ -8,7 +8,8 @@
/// <reference types="@docusaurus/module-type-aliases" />
declare module '@docusaurus/plugin-content-docs' {
import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {MDXOptions, LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {
ContentPaths,
FrontMatterTag,
@ -491,6 +492,12 @@ declare module '@docusaurus/plugin-content-docs' {
[docId: string]: PropVersionDoc;
};
export type PropDocContent = LoadedMDXContent<
DocFrontMatter,
DocMetadata,
Assets
>;
export type PropVersionMetadata = Pick<
VersionMetadata,
'label' | 'banner' | 'badge' | 'className' | 'isLast'
@ -549,13 +556,7 @@ declare module '@docusaurus/plugin-content-docs' {
}
declare module '@theme/DocItem' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {
PropVersionMetadata,
Assets,
DocMetadata,
DocFrontMatter,
} from '@docusaurus/plugin-content-docs';
import type {PropDocContent} from '@docusaurus/plugin-content-docs';
export type DocumentRoute = {
readonly component: () => JSX.Element;
@ -566,8 +567,7 @@ declare module '@theme/DocItem' {
export interface Props {
readonly route: DocumentRoute;
readonly versionMetadata: PropVersionMetadata;
readonly content: LoadedMDXContent<DocFrontMatter, DocMetadata, Assets>;
readonly content: PropDocContent;
}
export default function DocItem(props: Props): JSX.Element;

View file

@ -261,10 +261,40 @@ declare module '@theme/DocCardList' {
export default function DocCardList(props: Props): JSX.Element;
}
declare module '@theme/DocItemFooter' {
import type {Props} from '@theme/DocItem';
declare module '@theme/DocItem/Layout' {
export interface Props {
readonly children: JSX.Element;
}
export default function DocItemFooter(props: Props): JSX.Element;
export default function DocItemLayout(props: Props): JSX.Element;
}
declare module '@theme/DocItem/Metadata' {
export default function DocItemMetadata(): JSX.Element;
}
declare module '@theme/DocItem/Content' {
export interface Props {
readonly children: JSX.Element;
}
export default function DocItemContent(props: Props): JSX.Element;
}
declare module '@theme/DocItem/TOC/Mobile' {
export default function DocItemTOCMobile(): JSX.Element;
}
declare module '@theme/DocItem/TOC/Desktop' {
export default function DocItemTOCDesktop(): JSX.Element;
}
declare module '@theme/DocItem/Paginator' {
export default function DocItemPaginator(): JSX.Element;
}
declare module '@theme/DocItem/Footer' {
export default function DocItemFooter(): JSX.Element;
}
declare module '@theme/DocPage/Layout' {

View file

@ -0,0 +1,47 @@
/**
* 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 clsx from 'clsx';
import {ThemeClassNames, useDoc} from '@docusaurus/theme-common';
import Heading from '@theme/Heading';
import MDXContent from '@theme/MDXContent';
import type {Props} from '@theme/DocItem/Content';
/**
Title can be declared inside md content or declared through
front matter and added manually. To make both cases consistent,
the added title is added under the same div.markdown block
See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
We render a "synthetic title" if:
- user doesn't ask to hide it with front matter
- the markdown content does not already contain a top-level h1 heading
*/
function useSyntheticTitle(): string | null {
const {metadata, frontMatter, contentTitle} = useDoc();
const shouldRender =
!frontMatter.hide_title && typeof contentTitle === 'undefined';
if (!shouldRender) {
return null;
}
return metadata.title;
}
export default function DocItemContent({children}: Props): JSX.Element {
const syntheticTitle = useSyntheticTitle();
return (
<div className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}>
{syntheticTitle && (
<header>
<Heading as="h1">{syntheticTitle}</Heading>
</header>
)}
<MDXContent>{children}</MDXContent>
</div>
);
}

View file

@ -7,13 +7,16 @@
import React from 'react';
import clsx from 'clsx';
import {ThemeClassNames} from '@docusaurus/theme-common';
import {
ThemeClassNames,
useDoc,
type DocContextValue,
} from '@docusaurus/theme-common';
import LastUpdated from '@theme/LastUpdated';
import EditThisPage from '@theme/EditThisPage';
import TagsListInline, {
type Props as TagsListInlineProps,
} from '@theme/TagsListInline';
import type {Props} from '@theme/DocItem';
import styles from './styles.module.css';
@ -32,7 +35,7 @@ function TagsRow(props: TagsListInlineProps) {
}
type EditMetaRowProps = Pick<
Props['content']['metadata'],
DocContextValue['metadata'],
'editUrl' | 'lastUpdatedAt' | 'lastUpdatedBy' | 'formattedLastUpdatedAt'
>;
function EditMetaRow({
@ -58,9 +61,8 @@ function EditMetaRow({
);
}
export default function DocItemFooter(props: Props): JSX.Element | null {
const {content: DocContent} = props;
const {metadata} = DocContent;
export default function DocItemFooter(): JSX.Element | null {
const {metadata} = useDoc();
const {editUrl, lastUpdatedAt, formattedLastUpdatedAt, lastUpdatedBy, tags} =
metadata;

View file

@ -0,0 +1,67 @@
/**
* 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 clsx from 'clsx';
import {useWindowSize, useDoc} from '@docusaurus/theme-common';
import DocItemPaginator from '@theme/DocItem/Paginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import DocItemFooter from '@theme/DocItem/Footer';
import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile';
import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop';
import DocItemContent from '@theme/DocItem/Content';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import type {Props} from '@theme/DocItem/Layout';
import styles from './styles.module.css';
/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const {frontMatter, toc} = useDoc();
const windowSize = useWindowSize();
const hidden = frontMatter.hide_table_of_contents;
const canRender = !hidden && toc.length > 0;
const mobile = canRender ? <DocItemTOCMobile /> : undefined;
const desktop =
canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? (
<DocItemTOCDesktop />
) : undefined;
return {
hidden,
mobile,
desktop,
};
}
export default function DocItemLayout({children}: Props): JSX.Element {
const docTOC = useDocTOC();
return (
<div className="row">
<div className={clsx('col', !docTOC.hidden && styles.docItemCol)}>
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<DocItemFooter />
</article>
<DocItemPaginator />
</div>
</div>
{docTOC.desktop && <div className="col col--3">{docTOC.desktop}</div>}
</div>
);
}

View file

@ -0,0 +1,17 @@
/**
* 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.
*/
.docItemContainer header + *,
.docItemContainer article > *:first-child {
margin-top: 0;
}
@media (min-width: 997px) {
.docItemCol {
max-width: 75% !important;
}
}

View file

@ -0,0 +1,21 @@
/**
* 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 {PageMetadata, useDoc} from '@docusaurus/theme-common';
export default function DocItemMetadata(): JSX.Element {
const {metadata, frontMatter, assets} = useDoc();
return (
<PageMetadata
title={metadata.title}
description={metadata.description}
keywords={frontMatter.keywords}
image={assets.image ?? frontMatter.image}
/>
);
}

View file

@ -0,0 +1,19 @@
/**
* 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 {useDoc} from '@docusaurus/theme-common';
import DocPaginator from '@theme/DocPaginator';
/**
* This extra component is needed, because <DocPaginator> should remain generic.
* DocPaginator is used in non-docs contexts too: generated-index pages...
*/
export default function DocItemPaginator(): JSX.Element {
const {metadata} = useDoc();
return <DocPaginator previous={metadata.previous} next={metadata.next} />;
}

View file

@ -0,0 +1,23 @@
/**
* 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 {ThemeClassNames, useDoc} from '@docusaurus/theme-common';
import TOC from '@theme/TOC';
export default function DocItemTOCDesktop(): JSX.Element {
const {toc, frontMatter} = useDoc();
return (
<TOC
toc={toc}
minHeadingLevel={frontMatter.toc_min_heading_level}
maxHeadingLevel={frontMatter.toc_max_heading_level}
className={ThemeClassNames.docs.docTocDesktop}
/>
);
}

View file

@ -0,0 +1,25 @@
/**
* 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 clsx from 'clsx';
import {ThemeClassNames, useDoc} from '@docusaurus/theme-common';
import TOCCollapsible from '@theme/TOCCollapsible';
import styles from './styles.module.css';
export default function DocItemTOCMobile(): JSX.Element {
const {toc, frontMatter} = useDoc();
return (
<TOCCollapsible
toc={toc}
minHeadingLevel={frontMatter.toc_min_heading_level}
maxHeadingLevel={frontMatter.toc_max_heading_level}
className={clsx(ThemeClassNames.docs.docTocMobile, styles.tocMobile)}
/>
);
}

View file

@ -5,16 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
.docItemContainer header + *,
.docItemContainer article > *:first-child {
margin-top: 0;
}
@media (min-width: 997px) {
.docItemCol {
max-width: 75% !important;
}
/* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */
.tocMobile {
display: none;

View file

@ -6,124 +6,22 @@
*/
import React from 'react';
import clsx from 'clsx';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
useWindowSize,
} from '@docusaurus/theme-common';
import DocPaginator from '@theme/DocPaginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import DocItemFooter from '@theme/DocItemFooter';
import TOC from '@theme/TOC';
import TOCCollapsible from '@theme/TOCCollapsible';
import Heading from '@theme/Heading';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import MDXContent from '@theme/MDXContent';
import {HtmlClassNameProvider, DocProvider} from '@docusaurus/theme-common';
import DocItemMetadata from '@theme/DocItem/Metadata';
import DocItemLayout from '@theme/DocItem/Layout';
import type {Props} from '@theme/DocItem';
import styles from './styles.module.css';
function DocItemMetadata(props: Props): JSX.Element {
const {content: DocContent} = props;
const {metadata, frontMatter, assets} = DocContent;
const {keywords} = frontMatter;
const {description, title} = metadata;
const image = assets.image ?? frontMatter.image;
return <PageMetadata {...{title, description, keywords, image}} />;
}
function DocItemContent(props: Props): JSX.Element {
const {content: DocContent} = props;
const {metadata, frontMatter} = DocContent;
const {
hide_title: hideTitle,
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
const {title} = metadata;
// We only add a title if:
// - user doesn't ask to hide it with front matter
// - the markdown content does not already contain a top-level h1 heading
const shouldAddTitle =
!hideTitle && typeof DocContent.contentTitle === 'undefined';
const windowSize = useWindowSize();
const canRenderTOC = !hideTableOfContents && DocContent.toc.length > 0;
const renderTocDesktop =
canRenderTOC && (windowSize === 'desktop' || windowSize === 'ssr');
return (
<div className="row">
<div className={clsx('col', !hideTableOfContents && styles.docItemCol)}>
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{canRenderTOC && (
<TOCCollapsible
toc={DocContent.toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
className={clsx(
ThemeClassNames.docs.docTocMobile,
styles.tocMobile,
)}
/>
)}
<div className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}>
{/*
Title can be declared inside md content or declared through
front matter and added manually. To make both cases consistent,
the added title is added under the same div.markdown block
See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
*/}
{shouldAddTitle && (
<header>
<Heading as="h1">{title}</Heading>
</header>
)}
<MDXContent>
<DocContent />
</MDXContent>
</div>
<DocItemFooter {...props} />
</article>
<DocPaginator previous={metadata.previous} next={metadata.next} />
</div>
</div>
{renderTocDesktop && (
<div className="col col--3">
<TOC
toc={DocContent.toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
className={ThemeClassNames.docs.docTocDesktop}
/>
</div>
)}
</div>
);
}
export default function DocItem(props: Props): JSX.Element {
const docHtmlClassName = `docs-doc-id-${props.content.metadata.unversionedId}`;
const MDXComponent = props.content;
return (
<DocProvider content={props.content}>
<HtmlClassNameProvider className={docHtmlClassName}>
<DocItemMetadata {...props} />
<DocItemContent {...props} />
<DocItemMetadata />
<DocItemLayout>
<MDXComponent />
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
}

View file

@ -12,7 +12,6 @@ import type {Props} from '@theme/DocPaginator';
export default function DocPaginator(props: Props): JSX.Element {
const {previous, next} = props;
return (
<nav
className="pagination-nav docusaurus-mt-lg"

View file

@ -0,0 +1,72 @@
/**
* 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, {useMemo, type ReactNode, useContext} from 'react';
import {ReactContextError} from '../utils/reactUtils';
import type {PropDocContent} from '@docusaurus/plugin-content-docs';
/**
* The React context value returned by the `useDoc()` hook.
* It contains useful data related to the currently browsed doc.
*/
export type DocContextValue = Pick<
PropDocContent,
'metadata' | 'frontMatter' | 'toc' | 'assets' | 'contentTitle'
>;
const Context = React.createContext<DocContextValue | null>(null);
/**
* Note: we don't use `PropDoc` as context value on purpose. Metadata is
* currently stored inside the MDX component, but we may want to change that in
* the future. This layer is a good opportunity to decouple storage from
* consuming API, potentially allowing us to provide metadata as both props and
* route context without duplicating the chunks in the future.
*/
function useContextValue(content: PropDocContent): DocContextValue {
return useMemo(
() => ({
metadata: content.metadata,
frontMatter: content.frontMatter,
assets: content.assets,
contentTitle: content.contentTitle,
toc: content.toc,
}),
[content],
);
}
/**
* This is a very thin layer around the `content` received from the MDX loader.
* It provides the component to be rendered and other metadata about the doc to
* the children.
*/
export function DocProvider({
children,
content,
}: {
children: ReactNode;
content: PropDocContent;
}): JSX.Element {
const contextValue = useContextValue(content);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
/**
* Returns the data of the currently browsed doc. Gives access to the doc's MDX
* Component, front matter, metadata, TOC, etc. When swizzling a low-level
* component (e.g. the "Edit this page" link) and you need some extra metadata,
* you don't have to drill the props all the way through the component tree:
* simply use this hook instead.
*/
export function useDoc(): DocContextValue {
const doc = useContext(Context);
if (doc === null) {
throw new ReactContextError('DocProvider');
}
return doc;
}

View file

@ -25,6 +25,8 @@ export {
} from './contexts/docSidebarItemsExpandedState';
export {DocsVersionProvider, useDocsVersion} from './contexts/docsVersion';
export {DocsSidebarProvider, useDocsSidebar} from './contexts/docsSidebar';
export {DocProvider, useDoc} from './contexts/doc';
export type {DocContextValue} from './contexts/doc';
export {createStorageSlot, listStorageKeys} from './utils/storageUtils';

View file

@ -324,7 +324,7 @@ export function useDocRouteMetadata({
? versionMetadata.docsSidebars[sidebarName]
: undefined;
const docElement = renderRoutes(docRoutes, {versionMetadata});
const docElement = renderRoutes(docRoutes);
return {
docElement,