Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change docs to render stories out of context #14911

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 102 additions & 0 deletions addons/docs/src/blocks/LegacyStory.tsx
@@ -0,0 +1,102 @@
import React, { FunctionComponent, ReactNode, ElementType, ComponentProps } 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 { CURRENT_SELECTION } from './types';

import { DocsContext, DocsContextProps } from './DocsContext';

export const storyBlockIdFromId = (storyId: string) => `story--${storyId}`;

type PureStoryProps = ComponentProps<typeof PureStory>;

type CommonProps = BaseAnnotations<Args, any> & {
height?: string;
inline?: boolean;
};

type StoryDefProps = {
name: string;
children: ReactNode;
};

type StoryRefProps = {
id?: string;
};

type StoryImportProps = {
name: string;
story: ElementType;
};

export type StoryProps = (StoryDefProps | StoryRefProps | StoryImportProps) & CommonProps;

export const lookupStoryId = (
storyName: string,
{ mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps
) =>
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
storyNameFromExport(mdxStoryNameToKey[storyName])
);

export const getStoryProps = (props: StoryProps, context: DocsContextProps): PureStoryProps => {
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 data = context.storyStore.fromId(previewId) || {};

const { height, inline } = props;
const { storyFn = undefined, name: storyName = undefined, parameters = {} } = data;
const { docs = {} } = parameters;

if (docs.disable) {
return null;
}

// prefer block props, then story parameters defined by the framework-specific settings and optionally overridden by users
const { inlineStories = false, iframeHeight = 100, prepareForInline } = docs;
const storyIsInline = typeof inline === 'boolean' ? inline : inlineStories;
if (storyIsInline && !prepareForInline) {
throw new Error(
`Story '${storyName}' is set to render inline, but no 'prepareForInline' function is implemented in your docs configuration!`
);
}

return {
parameters,
inline: storyIsInline,
id: previewId,
storyFn: prepareForInline && storyFn ? () => prepareForInline(storyFn, data) : storyFn,
height: height || (storyIsInline ? undefined : iframeHeight),
title: storyName,
};
};

const Story: FunctionComponent<StoryProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const storyProps = getStoryProps(props, context);

if (!storyProps) {
return null;
}
return (
<div id={storyBlockIdFromId(storyProps.id)}>
<MDXProvider components={resetComponents}>
<PureStory {...storyProps} />
</MDXProvider>
</div>
);
}}
</DocsContext.Consumer>
);

Story.defaultProps = {
children: null,
name: null,
};

export { Story };
83 changes: 83 additions & 0 deletions addons/docs/src/blocks/ModernStory.tsx
@@ -0,0 +1,83 @@
import React, { FunctionComponent, ReactNode, ElementType, useEffect } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { resetComponents } from '@storybook/components';
import { DOCS_TARGETTED_RENDER, DOCS_TARGETTED_DESTROY } from '@storybook/core-events';
import { toId, storyNameFromExport } from '@storybook/csf';
import { Args, BaseAnnotations, addons } from '@storybook/addons';
import { CURRENT_SELECTION } from './types';

import { DocsContext, DocsContextProps } from './DocsContext';

export const storyBlockIdFromId = (storyId: string) => `story--${storyId}`;

type CommonProps = BaseAnnotations<Args, any> & {
height?: string;
inline?: boolean;
};

type StoryDefProps = {
name: string;
children: ReactNode;
};

type StoryRefProps = {
id?: string;
};

type StoryImportProps = {
name: string;
story: ElementType;
};

export type StoryProps = (StoryDefProps | StoryRefProps | StoryImportProps) & CommonProps;

export const lookupStoryId = (
storyName: string,
{ mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps
) =>
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
storyNameFromExport(mdxStoryNameToKey[storyName])
);

const Placeholder: FunctionComponent<any> = ({ id, name }) => {
const channel = addons.getChannel();
const identifier = storyBlockIdFromId(id);
useEffect(() => {
channel.emit(DOCS_TARGETTED_RENDER, { identifier, id, name });
return () => {
channel.emit(DOCS_TARGETTED_DESTROY, { identifier, id, name });
// TODO this does nothing, should it do something though?
};
});

return (
<div id={identifier} data-name={name}>
<span data-is-loadering-indicator="true">loading story...</span>
</div>
);
};

const Story: FunctionComponent<StoryProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const { id } = props as StoryRefProps;
const { name } = props as StoryDefProps;
const inputId = id === CURRENT_SELECTION ? context.id : id;
const previewId = inputId || lookupStoryId(name, context);

return (
<MDXProvider components={resetComponents}>
<Placeholder id={previewId} name={name} />
</MDXProvider>
);
}}
</DocsContext.Consumer>
);

Story.defaultProps = {
children: null,
name: null,
};

export { Story };
105 changes: 6 additions & 99 deletions addons/docs/src/blocks/Story.tsx
@@ -1,101 +1,8 @@
import React, { FunctionComponent, ReactNode, ElementType, ComponentProps } 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 { CURRENT_SELECTION } from './types';
import global from 'global';
import { Story as LegacyStory } from './LegacyStory';
import { Story as ModernStory } from './ModernStory';

import { DocsContext, DocsContextProps } from './DocsContext';
export const Story = global?.MODERN_INLINE_RENDER ? ModernStory : LegacyStory;

export const storyBlockIdFromId = (storyId: string) => `story--${storyId}`;

type PureStoryProps = ComponentProps<typeof PureStory>;

type CommonProps = BaseAnnotations<Args, any> & {
height?: string;
inline?: boolean;
};

type StoryDefProps = {
name: string;
children: ReactNode;
};

type StoryRefProps = {
id?: string;
};

type StoryImportProps = {
name: string;
story: ElementType;
};

export type StoryProps = (StoryDefProps | StoryRefProps | StoryImportProps) & CommonProps;

export const lookupStoryId = (
storyName: string,
{ mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps
) =>
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
storyNameFromExport(mdxStoryNameToKey[storyName])
);

export const getStoryProps = (props: StoryProps, context: DocsContextProps): PureStoryProps => {
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 data = context.storyStore.fromId(previewId) || {};

const { height, inline } = props;
const { storyFn = undefined, name: storyName = undefined, parameters = {} } = data;
const { docs = {} } = parameters;

if (docs.disable) {
return null;
}

// prefer block props, then story parameters defined by the framework-specific settings and optionally overridden by users
const { inlineStories = false, iframeHeight = 100, prepareForInline } = docs;
const storyIsInline = typeof inline === 'boolean' ? inline : inlineStories;
if (storyIsInline && !prepareForInline) {
throw new Error(
`Story '${storyName}' is set to render inline, but no 'prepareForInline' function is implemented in your docs configuration!`
);
}

return {
parameters,
inline: storyIsInline,
id: previewId,
storyFn: prepareForInline && storyFn ? () => prepareForInline(storyFn, data) : storyFn,
height: height || (storyIsInline ? undefined : iframeHeight),
title: storyName,
};
};

const Story: FunctionComponent<StoryProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const storyProps = getStoryProps(props, context);
if (!storyProps) {
return null;
}
return (
<div id={storyBlockIdFromId(storyProps.id)}>
<MDXProvider components={resetComponents}>
<PureStory {...storyProps} />
</MDXProvider>
</div>
);
}}
</DocsContext.Consumer>
);

Story.defaultProps = {
children: null,
name: null,
};

export { Story };
// FIXME: refactor
export { storyBlockIdFromId, lookupStoryId } from './ModernStory';
15 changes: 10 additions & 5 deletions app/ember/src/client/preview/render.ts
Expand Up @@ -6,12 +6,12 @@ const { window: globalWindow, document } = global;

declare let Ember: any;

const rootEl = document.getElementById('root');
const rootElement = document.getElementById('root');

const config = globalWindow.require(`${globalWindow.STORYBOOK_NAME}/config/environment`);
const app = globalWindow.require(`${globalWindow.STORYBOOK_NAME}/app`).default.create({
autoboot: false,
rootElement: rootEl,
rootElement,
...config.APP,
});

Expand Down Expand Up @@ -59,7 +59,13 @@ function render(options: OptionsArgs, el: ElementArgs) {
});
}

export default function renderMain({ storyFn, kind, name, showMain, showError }: RenderContext) {
export default function renderMain({
storyFn,
kind,
name,
showError,
targetDOMNode = rootElement,
}: RenderContext) {
const element = storyFn();

if (!element) {
Expand All @@ -75,6 +81,5 @@ export default function renderMain({ storyFn, kind, name, showMain, showError }:
return;
}

showMain();
render(element, rootEl);
render(element, { el: targetDOMNode });
}
13 changes: 6 additions & 7 deletions app/html/src/client/preview/render.ts
Expand Up @@ -10,23 +10,22 @@ export default function renderMain({
storyFn,
kind,
name,
showMain,
showError,
forceRender,
targetDOMNode = rootElement,
}: RenderContext) {
const element = storyFn();
showMain();
if (typeof element === 'string') {
rootElement.innerHTML = element;
simulatePageLoad(rootElement);
targetDOMNode.innerHTML = element;
simulatePageLoad(targetDOMNode);
} else if (element instanceof Node) {
// Don't re-mount the element if it didn't change and neither did the story
if (rootElement.firstChild === element && forceRender === true) {
if (targetDOMNode.firstChild === element && forceRender === true) {
return;
}

rootElement.innerHTML = '';
rootElement.appendChild(element);
targetDOMNode.innerHTML = '';
targetDOMNode.appendChild(element);
simulateDOMContentLoaded();
} else {
showError({
Expand Down
14 changes: 6 additions & 8 deletions app/preact/src/client/preview/render.tsx
Expand Up @@ -8,12 +8,12 @@ const rootElement = document ? document.getElementById('root') : null;

let renderedStory: Element;

function preactRender(story: StoryFnPreactReturnType): void {
function preactRender(story: StoryFnPreactReturnType, targetDOMNode: HTMLElement): void {
if (preact.Fragment) {
// Preact 10 only:
preact.render(story, rootElement);
preact.render(story, targetDOMNode);
} else {
renderedStory = (preact.render(story, rootElement, renderedStory) as unknown) as Element;
renderedStory = (preact.render(story, targetDOMNode, renderedStory) as unknown) as Element;
}
}

Expand Down Expand Up @@ -41,16 +41,14 @@ export default function renderMain({
storyFn,
kind,
name,
showMain,
showError,
forceRender,
targetDOMNode = rootElement,
}: RenderContext) {
// But forceRender means that it's the same story, so we want to keep the state in that case.
if (!forceRender) {
preactRender(null);
preactRender(null, targetDOMNode);
}

showMain();

preactRender(preact.h(StoryHarness, { name, kind, showError, storyFn }));
preactRender(preact.h(StoryHarness, { name, kind, showError, storyFn }), targetDOMNode);
}