From f8f3dba01d82717dcb4f2256a7ab7dcf8ead8a95 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 6 Aug 2021 15:03:11 +1000 Subject: [PATCH] Add a `modernInlineRender` that renders the story using the framework's `renderToDOM` Replaces https://github.com/storybookjs/storybook/pull/14911 --- addons/docs/src/blocks/Story.tsx | 110 +++++++++++++++++------- app/react/src/client/preview/render.tsx | 17 ++-- examples/react-ts/main.ts | 1 + lib/client-api/src/new/types.ts | 6 +- lib/core-common/src/types.ts | 5 ++ 5 files changed, 98 insertions(+), 41 deletions(-) diff --git a/addons/docs/src/blocks/Story.tsx b/addons/docs/src/blocks/Story.tsx index 323feee337d0..708dd23b480a 100644 --- a/addons/docs/src/blocks/Story.tsx +++ b/addons/docs/src/blocks/Story.tsx @@ -1,8 +1,18 @@ -import React, { FunctionComponent, ReactNode, ElementType, ComponentProps } from 'react'; +import React, { + FunctionComponent, + ReactNode, + ElementType, + ComponentProps, + useContext, + useRef, + useEffect, +} from 'react'; import { MDXProvider } from '@mdx-js/react'; import { resetComponents, Story as PureStory } from '@storybook/components'; import { toId, storyNameFromExport } from '@storybook/csf'; import { Args, BaseAnnotations } from '@storybook/addons'; +import { Story as StoryType } from '@storybook/client-api/dist/ts3.9/new/types'; +import global from 'global'; import { CURRENT_SELECTION } from './types'; import { DocsContext, DocsContextProps } from './DocsContext'; @@ -41,18 +51,32 @@ export const lookupStoryId = ( storyNameFromExport(mdxStoryNameToKey[storyName]) ); -export const getStoryProps = ( - props: StoryProps, - context: DocsContextProps -): PureStoryProps => { +// TODO -- this can be async +export const getStory = (props: StoryProps, context: DocsContextProps): StoryType => { const { id } = props as StoryRefProps; const { name } = props as StoryDefProps; const inputId = id === CURRENT_SELECTION ? context.id : id; const previewId = inputId || lookupStoryId(name, context); - const story = context.storyById(previewId); + return context.storyById(previewId); +}; + +export const getStoryProps = ( + { height, inline }: StoryProps, + story: StoryType, + context: DocsContextProps +): PureStoryProps => { + const defaultIframeHeight = 100; + + if (!story) { + return { + id: story.id, + inline: false, + height: height || defaultIframeHeight.toString(), + title: undefined, + }; + } - const { height, inline } = props; - const { storyFn, name: storyName, parameters } = story; + const { name: storyName, parameters } = story; const { docs = {} } = parameters; if (docs.disable) { @@ -72,32 +96,60 @@ export const getStoryProps = ( return { parameters, inline: storyIsInline, - id: previewId, - // TODO -- how can `storyFn` be undefined? - storyFn: - prepareForInline && boundStoryFn ? () => prepareForInline(boundStoryFn, story) : boundStoryFn, + id: story.id, + storyFn: prepareForInline ? () => prepareForInline(boundStoryFn, story) : boundStoryFn, height: height || (storyIsInline ? undefined : iframeHeight), title: storyName, }; }; -const Story: FunctionComponent = (props) => ( - - {(context) => { - const storyProps = getStoryProps(props, context); - if (!storyProps) { - return null; - } - return ( -
- - - -
- ); - }} -
-); +const Story: FunctionComponent = (props) => { + const context = useContext(DocsContext); + const ref = useRef(); + const story = getStory(props, context); + const { id, title, name } = story; + const renderContext = { + id, + title, + kind: title, + name, + story: name, + // TODO -- shouldn't this be true sometimes? How to react to arg changes + forceRender: false, + // TODO what to do when these fail? + showMain: () => {}, + showError: () => {}, + showException: () => {}, + }; + useEffect(() => { + if (story && ref.current) { + setTimeout(() => { + context.renderStoryToElement({ story, renderContext, element: ref.current as Element }); + }, 1000); + } + return () => story?.cleanup(); + }, [story]); + + if (global?.FEATURES.modernInlineRender) { + return ( +
+ loading story... +
+ ); + } + + const storyProps = getStoryProps(props, story, context); + if (!storyProps) { + return null; + } + return ( +
+ + + +
+ ); +}; Story.defaultProps = { children: null, diff --git a/app/react/src/client/preview/render.tsx b/app/react/src/client/preview/render.tsx index f7ba50dd21b0..5652c2c19973 100644 --- a/app/react/src/client/preview/render.tsx +++ b/app/react/src/client/preview/render.tsx @@ -6,8 +6,6 @@ import { StoryContext, RenderContext } from './types'; const { document, FRAMEWORK_OPTIONS } = global; -const rootEl = document ? document.getElementById('root') : null; - const render = (node: ReactElement, el: Element) => new Promise((resolve) => { ReactDOM.render(node, el, resolve); @@ -47,13 +45,10 @@ class ErrorBoundary extends Component<{ const Wrapper = FRAMEWORK_OPTIONS?.strictMode ? StrictMode : Fragment; -export default async function renderMain({ - storyContext, - unboundStoryFn, - showMain, - showException, - forceRender, -}: RenderContext) { +export default async function renderMain( + { storyContext, unboundStoryFn, showMain, showException, forceRender }: RenderContext, + domElement: Element +) { const Story = unboundStoryFn as FunctionComponent; const content = ( @@ -71,8 +66,8 @@ export default async function renderMain({ // https://github.com/storybookjs/react-storybook/issues/81 // But forceRender means that it's the same story, so we want too keep the state in that case. if (!forceRender) { - ReactDOM.unmountComponentAtNode(rootEl); + ReactDOM.unmountComponentAtNode(domElement); } - await render(element, rootEl); + await render(element, domElement); } diff --git a/examples/react-ts/main.ts b/examples/react-ts/main.ts index 8b6b82792e69..c2ec2ae41868 100644 --- a/examples/react-ts/main.ts +++ b/examples/react-ts/main.ts @@ -23,6 +23,7 @@ const config: StorybookConfig = { postcss: false, previewCsfV3: true, buildStoriesJson: true, + modernInlineRender: true, }, }; diff --git a/lib/client-api/src/new/types.ts b/lib/client-api/src/new/types.ts index fe7d89a45b24..faa828404a3f 100644 --- a/lib/client-api/src/new/types.ts +++ b/lib/client-api/src/new/types.ts @@ -171,7 +171,11 @@ export interface DocsContextProps { name: string; storyById: (id: StoryId) => Story; componentStories: () => Story[]; - renderStoryToElement: (story: Story) => void; + renderStoryToElement: (args: { + story: Story; + renderContext: RenderContextWithoutStoryContext; + element: Element; + }) => void; // TODO -- we need this for the `prepareForInline` docs approach bindStoryFn: (story: Story) => LegacyStoryFn; diff --git a/lib/core-common/src/types.ts b/lib/core-common/src/types.ts index b2c150985f88..c2e850ed9984 100644 --- a/lib/core-common/src/types.ts +++ b/lib/core-common/src/types.ts @@ -264,6 +264,11 @@ export interface StorybookConfig { * Activate preview of CSF v3.0 */ previewCsfV3?: boolean; + + /** + * Activate modern inline rendering + */ + modernInlineRender?: boolean; }; /** * Tells Storybook where to find stories.