From dc4664b489bdce6eb29e0ffb2c3883c1eadcf5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Tue, 27 Jul 2021 18:45:12 +0200 Subject: [PATCH] feat: details/summary theme / MDX component (#5216) * Details component * polish arrow animation * fix text selection bug * fix some edge cases + polish * example of overriding baseClassName * Move Details component to theme-common * make component work even when JS is disabled or failed to load * update arrow transform * Details component: better handling of no-JS fallback mode: avoid delaying arrow navigation when JS (see review) * prefix css vars with --docusaurus * improve css arrow styling * slightly change details/summary design * better md doc + include quotes and details in doc --- .../src/theme/Details/index.tsx | 24 +++++ .../src/theme/Details/styles.module.css | 13 +++ .../src/theme/MDXComponents/index.tsx | 17 ++- .../docusaurus-theme-classic/src/types.d.ts | 7 ++ .../copyUntypedFiles.js | 20 ++++ packages/docusaurus-theme-common/package.json | 6 +- .../Collapsible/index.tsx} | 13 ++- .../src/components/Details/index.tsx | 94 ++++++++++++++++ .../src/components/Details/styles.module.css | 59 ++++++++++ packages/docusaurus-theme-common/src/index.ts | 7 +- .../_markdown-partial-example.mdx | 3 + .../markdown-features-intro.mdx | 102 +++++++++++++++++- .../markdown-features-react.mdx | 34 ++++-- 13 files changed, 378 insertions(+), 21 deletions(-) create mode 100644 packages/docusaurus-theme-classic/src/theme/Details/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/Details/styles.module.css create mode 100644 packages/docusaurus-theme-common/copyUntypedFiles.js rename packages/docusaurus-theme-common/src/{utils/useCollapsible.tsx => components/Collapsible/index.tsx} (91%) create mode 100644 packages/docusaurus-theme-common/src/components/Details/index.tsx create mode 100644 packages/docusaurus-theme-common/src/components/Details/styles.module.css create mode 100644 website/docs/guides/markdown-features/_markdown-partial-example.mdx diff --git a/packages/docusaurus-theme-classic/src/theme/Details/index.tsx b/packages/docusaurus-theme-classic/src/theme/Details/index.tsx new file mode 100644 index 0000000000..2a870227b6 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Details/index.tsx @@ -0,0 +1,24 @@ +/** + * 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 {Details as DetailsGeneric} from '@docusaurus/theme-common'; +import type {Props} from '@theme/Details'; +import styles from './styles.module.css'; + +// Should we have a custom details/summary comp in Infima instead of reusing alert classes? +const InfimaClasses = 'alert alert--info'; + +export default function Details({...props}: Props): JSX.Element { + return ( + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Details/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Details/styles.module.css new file mode 100644 index 0000000000..dd8a2a825e --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Details/styles.module.css @@ -0,0 +1,13 @@ +/** + * 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. + */ + +.details { + --docusaurus-details-decoration-color: var(--ifm-alert-border-color); + --docusaurus-details-transition: transform var(--ifm-transition-fast) ease; + margin: 0 0 var(--ifm-spacing-vertical); + border: 1px solid var(--ifm-alert-border-color); +} diff --git a/packages/docusaurus-theme-classic/src/theme/MDXComponents/index.tsx b/packages/docusaurus-theme-classic/src/theme/MDXComponents/index.tsx index e21aa50031..7d8bcef2ee 100644 --- a/packages/docusaurus-theme-classic/src/theme/MDXComponents/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/MDXComponents/index.tsx @@ -5,10 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import React, {isValidElement} from 'react'; +import React, {ComponentProps, isValidElement, ReactElement} from 'react'; import Link from '@docusaurus/Link'; import CodeBlock, {Props} from '@theme/CodeBlock'; import Heading from '@theme/Heading'; +import Details from '@theme/Details'; import type {MDXComponentsObject} from '@theme/MDXComponents'; const MDXComponents: MDXComponentsObject = { @@ -44,6 +45,20 @@ const MDXComponents: MDXComponentsObject = { /> ); }, + details: (props): JSX.Element => { + const items = React.Children.toArray(props.children); + // Split summary item from the rest to pass it as a separate prop to the Detais theme component + const summary: ReactElement< + ComponentProps<'summary'> + > = (items as any[]).find((item) => item?.props?.mdxType === 'summary'); + const children = <>{items.filter((item) => item !== summary)}; + + return ( +
+ {children} +
+ ); + }, h1: Heading('h1'), h2: Heading('h2'), h3: Heading('h3'), diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 1b8df834e0..03f39a4c77 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -328,6 +328,7 @@ declare module '@theme/MDXComponents' { readonly code: typeof CodeBlock; readonly a: (props: ComponentProps<'a'>) => JSX.Element; readonly pre: typeof CodeBlock; + readonly details: (props: ComponentProps<'details'>) => JSX.Element; readonly h1: (props: ComponentProps<'h1'>) => JSX.Element; readonly h2: (props: ComponentProps<'h2'>) => JSX.Element; readonly h3: (props: ComponentProps<'h3'>) => JSX.Element; @@ -529,6 +530,12 @@ declare module '@theme/ThemedImage' { export default ThemedImage; } +declare module '@theme/Details' { + export type Props = import('@docusaurus/theme-common').Details; + const Props: (props: Props) => JSX.Element; + export default Props; +} + declare module '@theme/ThemeProvider' { import type {ReactNode} from 'react'; diff --git a/packages/docusaurus-theme-common/copyUntypedFiles.js b/packages/docusaurus-theme-common/copyUntypedFiles.js new file mode 100644 index 0000000000..e5555c5536 --- /dev/null +++ b/packages/docusaurus-theme-common/copyUntypedFiles.js @@ -0,0 +1,20 @@ +/** + * 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. + */ + +const path = require('path'); +const fs = require('fs-extra'); + +/** + * Copy all untyped and static assets files to lib. + */ +const srcDir = path.resolve(__dirname, 'src'); +const libDir = path.resolve(__dirname, 'lib'); +fs.copySync(srcDir, libDir, { + filter(filepath) { + return !/__tests__/.test(filepath) && !/\.tsx?$/.test(filepath); + }, +}); diff --git a/packages/docusaurus-theme-common/package.json b/packages/docusaurus-theme-common/package.json index 8b90af4c58..3ddfc8acfa 100644 --- a/packages/docusaurus-theme-common/package.json +++ b/packages/docusaurus-theme-common/package.json @@ -5,8 +5,8 @@ "main": "./lib/index.js", "types": "./lib/index.d.ts", "scripts": { - "build": "tsc", - "watch": "tsc --watch" + "build": "node copyUntypedFiles.js && tsc", + "watch": "node copyUntypedFiles.js && tsc --watch" }, "publishConfig": { "access": "public" @@ -23,6 +23,8 @@ "@docusaurus/plugin-content-docs": "2.0.0-beta.3", "@docusaurus/plugin-content-pages": "2.0.0-beta.3", "@docusaurus/types": "2.0.0-beta.3", + "clsx": "^1.1.1", + "fs-extra": "^10.0.0", "tslib": "^2.1.0" }, "devDependencies": { diff --git a/packages/docusaurus-theme-common/src/utils/useCollapsible.tsx b/packages/docusaurus-theme-common/src/components/Collapsible/index.tsx similarity index 91% rename from packages/docusaurus-theme-common/src/utils/useCollapsible.tsx rename to packages/docusaurus-theme-common/src/components/Collapsible/index.tsx index fd29fbc825..08b0d37477 100644 --- a/packages/docusaurus-theme-common/src/utils/useCollapsible.tsx +++ b/packages/docusaurus-theme-common/src/components/Collapsible/index.tsx @@ -164,7 +164,12 @@ type CollapsibleBaseProps = { collapsed: boolean; children: ReactNode; animation?: CollapsibleAnimationConfig; + onCollapseTransitionEnd?: (collapsed: boolean) => void; className?: string; + + // This is mostly useful for details/summary component where ssrStyle is not needed (as details are hidden natively) + // and can mess-up with the default native behavior of the browser when JS fails to load or is disabled + disableSSRStyle?: boolean; }; function CollapsibleBase({ @@ -172,7 +177,9 @@ function CollapsibleBase({ collapsed, children, animation, + onCollapseTransitionEnd, className, + disableSSRStyle, }: CollapsibleBaseProps) { // any because TS is a pain for HTML element refs, see https://twitter.com/sebastienlorber/status/1412784677795110914 const collapsibleRef = useRef(null); @@ -183,7 +190,7 @@ function CollapsibleBase({ { if (e.propertyName !== 'height') { return; @@ -197,10 +204,12 @@ function CollapsibleBase({ parseInt(currentCollapsibleElementHeight, 10) === el.scrollHeight ) { applyCollapsedStyle(el, false); + onCollapseTransitionEnd?.(false); } if (currentCollapsibleElementHeight === CollapsedStyles.height) { applyCollapsedStyle(el, true); + onCollapseTransitionEnd?.(true); } }} className={className}> @@ -239,7 +248,7 @@ type CollapsibleProps = CollapsibleBaseProps & { lazy: boolean; }; -export function Collapsible({lazy, ...props}: CollapsibleProps) { +export function Collapsible({lazy, ...props}: CollapsibleProps): JSX.Element { const Comp = lazy ? CollapsibleLazy : CollapsibleBase; return ; } diff --git a/packages/docusaurus-theme-common/src/components/Details/index.tsx b/packages/docusaurus-theme-common/src/components/Details/index.tsx new file mode 100644 index 0000000000..1645d4edbd --- /dev/null +++ b/packages/docusaurus-theme-common/src/components/Details/index.tsx @@ -0,0 +1,94 @@ +/** + * 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, {ComponentProps, ReactElement, useRef, useState} from 'react'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import clsx from 'clsx'; +import {useCollapsible, Collapsible} from '../Collapsible'; +import styles from './styles.module.css'; + +function isInSummary(node: HTMLElement | null): boolean { + if (!node) { + return false; + } + return node.tagName === 'SUMMARY' || isInSummary(node.parentElement); +} + +function hasParent(node: HTMLElement | null, parent: HTMLElement): boolean { + if (!node) { + return false; + } + return node === parent || hasParent(node.parentElement, parent); +} + +export type DetailsProps = { + summary?: ReactElement; +} & ComponentProps<'details'>; + +const Details = ({summary, children, ...props}: DetailsProps): JSX.Element => { + const {isClient} = useDocusaurusContext(); + const detailsRef = useRef(null); + + const {collapsed, setCollapsed} = useCollapsible({ + initialState: !props.open, + }); + // We use a separate prop because it must be set only after animation completes + // Otherwise close anim won't work + const [open, setOpen] = useState(props.open); + + return ( +
{ + const target = e.target as HTMLElement; + // Prevent a double-click to highlight summary text + if (isInSummary(target) && e.detail > 1) { + e.preventDefault(); + } + }} + onClick={(e) => { + e.stopPropagation(); // For isolation of multiple nested details/summary + const target = e.target as HTMLElement; + const shouldToggle = + isInSummary(target) && hasParent(target, detailsRef.current!); + if (!shouldToggle) { + return; + } + e.preventDefault(); + if (collapsed) { + setCollapsed(false); + setOpen(true); + } else { + setCollapsed(true); + // setOpen(false); // Don't do this, it breaks close animation! + } + }}> + {summary} + + { + setCollapsed(newCollapsed); + setOpen(!newCollapsed); + }}> +
{children}
+
+
+ ); +}; + +export default Details; diff --git a/packages/docusaurus-theme-common/src/components/Details/styles.module.css b/packages/docusaurus-theme-common/src/components/Details/styles.module.css new file mode 100644 index 0000000000..4c3eccdfdd --- /dev/null +++ b/packages/docusaurus-theme-common/src/components/Details/styles.module.css @@ -0,0 +1,59 @@ +/** + * 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. + */ + +/* +CSS variables, meant to be overriden by final theme + */ +.details { + --docusaurus-details-summary-arrow-size: 0.38rem; + --docusaurus-details-transition: transform 200ms ease; + --docusaurus-details-decoration-color: grey; +} + +.details > summary { + position: relative; + cursor: pointer; + list-style: none; + margin-left: 1.8rem; +} + +.details > summary::-webkit-details-marker { + display: none; +} + +.details > summary:before { + position: absolute; + top: 0.45rem; + left: -1.2rem; + + /* CSS-only Arrow */ + content: ''; + width: 0; + height: 0; + border-top: var(--docusaurus-details-summary-arrow-size) solid transparent; + border-bottom: var(--docusaurus-details-summary-arrow-size) solid transparent; + border-left: var(--docusaurus-details-summary-arrow-size) solid + var(--docusaurus-details-decoration-color); + + /* Arrow rotation anim */ + transform: rotate(0deg); + transition: var(--docusaurus-details-transition); + transform-origin: calc(var(--docusaurus-details-summary-arrow-size) / 2) 50%; +} + +/* When JS disabled/failed to load: we use the open property for arrow animation: */ +.details[open]:not(.isClient) > summary:before, +/* When JS works: we use the data-attribute for arrow animation */ +.details[data-collapsed='false'].isClient > summary:before { + transform: rotate(90deg); +} + +.collapsibleContent { + margin-top: 1rem; + border-top: 1px solid var(--docusaurus-details-decoration-color); + padding-top: 1rem; +} diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index f22920beb0..cabe5f23f0 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -37,11 +37,14 @@ export {useLocationChange} from './utils/useLocationChange'; export {usePrevious} from './utils/usePrevious'; -export {useCollapsible, Collapsible} from './utils/useCollapsible'; +export {useCollapsible, Collapsible} from './components/Collapsible'; export type { UseCollapsibleConfig, UseCollapsibleReturns, -} from './utils/useCollapsible'; +} from './components/Collapsible'; + +export {default as Details} from './components/Details'; +export type {DetailsProps} from './components/Details'; export { MobileSecondaryMenuProvider, diff --git a/website/docs/guides/markdown-features/_markdown-partial-example.mdx b/website/docs/guides/markdown-features/_markdown-partial-example.mdx new file mode 100644 index 0000000000..5eb3f3bf11 --- /dev/null +++ b/website/docs/guides/markdown-features/_markdown-partial-example.mdx @@ -0,0 +1,3 @@ +Hello {props.name} + +This is text some content from `_markdown-partial-example.md`. diff --git a/website/docs/guides/markdown-features/markdown-features-intro.mdx b/website/docs/guides/markdown-features/markdown-features-intro.mdx index 28f215471c..e5e615d374 100644 --- a/website/docs/guides/markdown-features/markdown-features-intro.mdx +++ b/website/docs/guides/markdown-features/markdown-features-intro.mdx @@ -1,23 +1,115 @@ --- id: introduction -title: Markdown Features introduction +title: Markdown Features sidebar_label: Introduction description: Docusaurus uses GitHub Flavored Markdown (GFM). Find out more about Docusaurus-specific features when writing Markdown. slug: /markdown-features --- +```mdx-code-block +import BrowserWindow from '@site/src/components/BrowserWindow'; +``` + Documentation is one of your product's interfaces with your users. A well-written and well-organized set of docs helps your users understand your product quickly. Our aligned goal here is to help your users find and understand the information they need, as quickly as possible. Docusaurus 2 uses modern tooling to help you compose your interactive documentations with ease. You may embed React components, or build live coding blocks where your users may play with the code on the spot. Start sharing your eureka moments with the code your audience cannot walk away from. It is perhaps the most effective way of attracting potential users. In this section, we'd like to introduce you to the tools we've picked that we believe will help you build a powerful documentation. Let us walk you through with an example. -Markdown is a syntax that enables you to write formatted content in a readable syntax. - -The [standard Markdown syntax](https://daringfireball.net/projects/markdown/syntax) is supported, and we use [MDX](https://mdxjs.com/) as the parsing engine, which can do much more than just parsing Markdown, like rendering React components inside your documents. - :::important This section assumes you are using the official Docusaurus content plugins. ::: + +## Standard features + +Markdown is a syntax that enables you to write formatted content in a readable syntax. + +The [standard Markdown syntax](https://daringfireball.net/projects/markdown/syntax) is supported, and we use [MDX](https://mdxjs.com/) as the parsing engine, which can do much more than just parsing Markdown, like rendering React components inside your documents. + +```md +### My Doc Section + +Hello world message with some **bold** text, some _italic_ text and a [link](/) + +![img alt](/img/docusaurus.png) +``` + +```mdx-code-block + + +

My Doc Section

+ +Hello world message with some **bold** text, some _italic_ text and a [link](/) + +![img alt](/img/docusaurus.png) + +
+``` + +## Quotes + +Markdown quotes are beautifully styled: + +```md +> This is a quote +``` + +> This is a quote + +## Details + +Markdown can embed HTML elements, and [`details`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) HTML elements are beautifully styled: + +```md +### Details element example + +
+ Toggle me! +
+
This is the detailed content
+
+ +
Nested toggle!
+
Some surprise inside...
+
+
+ 😲😲😲😲😲 +
+
+
+
+``` + +```mdx-code-block + + +

Details element example

+ +
+ Toggle me! +
+
This is the detailed content
+
+ +
Nested toggle!
+
Some surprise inside...
+
+
+ 😲😲😲😲😲 +
+
+
+
+ +
+ +
+``` + +:::note + +In practice, those are not really HTML elements, but React JSX elements, which we'll cover next! + +::: diff --git a/website/docs/guides/markdown-features/markdown-features-react.mdx b/website/docs/guides/markdown-features/markdown-features-react.mdx index 90d78ab74b..4f920c5d20 100644 --- a/website/docs/guides/markdown-features/markdown-features-react.mdx +++ b/website/docs/guides/markdown-features/markdown-features-react.mdx @@ -1,11 +1,13 @@ --- id: react -title: Using React +title: MDX and React description: Using the power of React in Docusaurus Markdown documents, thanks to MDX slug: /markdown-features/react --- +```mdx-code-block import BrowserWindow from '@site/src/components/BrowserWindow'; +``` ## Using JSX in Markdown {#using-jsx-in-markdown} @@ -17,6 +19,14 @@ While both `.md` and `.mdx` files are parsed using MDX, some of the syntax are t ::: +:::caution + +MDX is not [100% compatible with CommonMark](https://github.com/facebook/docusaurus/issues/3018). + +Use the **[MDX playground](https://mdxjs.com/playground)** to ensure that your syntax is valid MDX. + +::: + Try this block here: ```jsx @@ -126,21 +136,27 @@ This feature is experimental and might be subject to API breaking changes in the ## Importing Markdown {#importing-markdown} -You can use Markdown files as components and import them elsewhere, either in Markdown files or in React pages. Below we are importing from [another file](./markdown-features-intro.mdx) and inserting it as a component. +You can use Markdown files as components and import them elsewhere, either in Markdown files or in React pages. -```jsx -import Intro from './markdown-features-intro.mdx'; +By convention, using the **`_` filename prefix** will not create any doc page and means the markdown file is a **"partial"**, to be imported by other files. -; +```md title="_markdown-partial-example.mdx" +Hello {props.name} + +This is text some content from `_markdown-partial-example.mdx`. +``` + +```jsx title="someOtherDoc.mdx" +import PartialExample from './_markdown-partial-example.mdx'; + +; ``` ```mdx-code-block -import Intro from './markdown-features-intro.mdx'; +import PartialExample from './_markdown-partial-example.mdx'; - - - +