Skip to content

Commit

Permalink
Addon-docs: DocsPage and doc blocks (#7119)
Browse files Browse the repository at this point in the history
Addon-docs: DocsPage and doc blocks
  • Loading branch information
shilman authored Jun 21, 2019
2 parents 475b3ba + 723037d commit d579617
Show file tree
Hide file tree
Showing 51 changed files with 2,374 additions and 10 deletions.
11 changes: 11 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Migration

- [From version 5.1.x to 5.2.x](#from-version-51x-to-52x)
- [Docs mode docgen](#docs-mode-docgen)
- [From version 5.0.x to 5.1.x](#from-version-50x-to-51x)
- [React native server](#react-native-server)
- [Angular 7](#angular-7)
Expand Down Expand Up @@ -56,6 +58,15 @@
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)

## From version 5.1.x to 5.2.x

### Docs mode docgen

This isn't a breaking change per se, because `addon-docs` is a new feature. However it's intended to replace `addon-info`, so if you're migrating from `addon-info` there are a few things you should know:

1. Support for only one prop table
2. Prop table docgen info should be stored on the component and not in the global variable `STORYBOOK_REACT_CLASSES` as before.

## From version 5.0.x to 5.1.x

### React native server
Expand Down
1 change: 1 addition & 0 deletions addons/docs/blocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/blocks');
9 changes: 8 additions & 1 deletion addons/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.23",
"@storybook/api": "5.2.0-alpha.23"
"@storybook/api": "5.2.0-alpha.23",
"@storybook/components": "5.2.0-alpha.23",
"@storybook/router": "5.2.0-alpha.23",
"@storybook/theming": "5.2.0-alpha.23",
"core-js": "^3.0.1",
"global": "^4.3.2",
"prop-types": "^15.7.2"
},
"devDependencies": {
"@types/prop-types": "^15.5.9",
"@types/util-deprecate": "^1.0.0",
"@types/webpack-env": "^1.13.7"
},
Expand Down
68 changes: 68 additions & 0 deletions addons/docs/src/blocks/Description.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable no-underscore-dangle */
import React from 'react';
import { Description, DescriptionProps as PureDescriptionProps } from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION } from './shared';

export enum DescriptionType {
INFO = 'info',
NOTES = 'notes',
DOCGEN = 'docgen',
AUTO = 'auto',
}

type Notes = string | any;
type Info = string | any;

interface DescriptionProps {
of?: '.' | Component;
type?: DescriptionType;
markdown?: string;
}

const getNotes = (notes?: Notes) =>
notes && (typeof notes === 'string' ? notes : notes.markdown || notes.text);

const getInfo = (info?: Info) => info && (typeof info === 'string' ? info : info.text);

const getDocgen = (component?: Component) =>
(component && component.__docgenInfo && component.__docgenInfo.description) || '';

export const getDescriptionProps = (
{ of, type, markdown }: DescriptionProps,
{ parameters }: DocsContextProps
): PureDescriptionProps => {
if (markdown) {
return { markdown };
}
const { component, notes, info } = parameters;
const target = of === CURRENT_SELECTION ? component : of;
switch (type) {
case DescriptionType.INFO:
return { markdown: getInfo(info) };
case DescriptionType.NOTES:
return { markdown: getNotes(notes) };
case DescriptionType.DOCGEN:
return { markdown: getDocgen(target) };
case DescriptionType.AUTO:
default:
return {
markdown: `
${getNotes(notes) || getInfo(info) || ''}
${getDocgen(target)}
`.trim(),
};
}
};

const DescriptionContainer: React.FunctionComponent<DescriptionProps> = props => (
<DocsContext.Consumer>
{context => {
const { markdown } = getDescriptionProps(props, context);
return markdown && <Description markdown={markdown} />;
}}
</DocsContext.Consumer>
);

export { DescriptionContainer as Description };
52 changes: 52 additions & 0 deletions addons/docs/src/blocks/DocsContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable react/destructuring-assignment */

import React from 'react';
// import { MDXProvider } from '@mdx-js/react';
import { Global, createGlobal, ThemeProvider, ensure as ensureTheme } from '@storybook/theming';
import { DocumentFormatting, DocsWrapper, DocsContent } from '@storybook/components';
import { DocsContextProps, DocsContext } from './DocsContext';

interface DocsContainerProps {
context: DocsContextProps;
content: React.ElementType<any>;
}

const defaultComponents = {
// p: ({ children }) => <b>{children}</b>,
wrapper: DocumentFormatting,
};

const globalWithOverflow = (args: any) => {
const global = createGlobal(args);
const { body, ...rest } = global;
const { overflow, ...bodyRest } = body;
return {
body: bodyRest,
...rest,
};
};

export const DocsContainer: React.FunctionComponent<DocsContainerProps> = ({
context,
content: MDXContent,
}) => {
const parameters = (context && context.parameters) || {};
const options = parameters.options || {};
const theme = ensureTheme(options.theme);
const { components: userComponents = null } = options.docs || {};
const components = { ...defaultComponents, ...userComponents };
return (
<DocsContext.Provider value={context}>
<ThemeProvider theme={theme}>
<Global styles={globalWithOverflow} />
{/* <MDXProvider components={components}> */}
<DocsWrapper>
<DocsContent>
<MDXContent components={components} />
</DocsContent>
</DocsWrapper>
{/* </MDXProvider> */}
</ThemeProvider>
</DocsContext.Provider>
);
};
23 changes: 23 additions & 0 deletions addons/docs/src/blocks/DocsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';

export interface DocsContextProps {
id?: string;
selectedKind?: string;
selectedStory?: string;

/**
* mdxKind is a statically-generated "kind" that corresponds to the
* component that's being documented in the MDX file, It's combined
* with the MDX story name `<Story name='story name'>...</Story>` to
* generate a storyId. In the case that the user is viewing a non-MDX
* story, the value of `mdxKind` will be the currently-selected kind.
* (I can't remember the corner case in which using the currentl-selected
* kind breaks down in MDX-defined stories, but there is one!)
*/
mdxKind?: string;
parameters?: any;
storyStore?: any;
forceRender?: () => void;
}

export const DocsContext: React.Context<DocsContextProps> = React.createContext({});
140 changes: 140 additions & 0 deletions addons/docs/src/blocks/DocsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React from 'react';

import { parseKind } from '@storybook/router';
import { styled } from '@storybook/theming';
import { DocsPage as PureDocsPage, DocsPageProps } from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
import { DocsContainer } from './DocsContainer';
import { Description } from './Description';
import { Story } from './Story';
import { Preview } from './Preview';
import { Props } from './Props';

enum DocsStoriesType {
ALL = 'all',
PRIMARY = 'primary',
REST = 'rest',
}

interface DocsStoriesProps {
type?: DocsStoriesType;
}

interface DocsStoryProps {
id: string;
name: string;
description?: string;
expanded?: boolean;
}

interface StoryData {
id: string;
kind: string;
name: string;
parameters?: any;
}

const getDocsStories = (type: DocsStoriesType, componentStories: StoryData[]): DocsStoryProps[] => {
let stories = componentStories;
if (type !== DocsStoriesType.ALL) {
const primary = stories.find(s => s.parameters && s.parameters.primary);
const [first, ...rest] = stories;
if (type === DocsStoriesType.PRIMARY) {
stories = [primary || first];
} else {
stories = primary ? stories.filter(s => !s.parameters || !s.parameters.primary) : rest;
}
}
return stories.map(({ id, name, parameters: { notes, info } }) => ({
id,
name,
description: notes || info || null,
}));
};

const StoriesHeading = styled.h2();
const StoryHeading = styled.h3();

const DocsStory: React.FunctionComponent<DocsStoryProps> = ({
id,
name,
description,
expanded = true,
}) => (
<>
{expanded && <StoryHeading>{name}</StoryHeading>}
{expanded && description && <Description markdown={description} />}
<Preview>
<Story id={id} />
</Preview>
</>
);

const DocsStories: React.FunctionComponent<DocsStoriesProps> = ({ type = DocsStoriesType.ALL }) => (
<DocsContext.Consumer>
{({ selectedKind, storyStore }) => {
const componentStories = (storyStore.raw() as StoryData[]).filter(
s => s.kind === selectedKind
);
const stories = getDocsStories(type, componentStories);
if (stories.length === 0) {
return null;
}
const expanded = type !== DocsStoriesType.PRIMARY;
return (
<>
{expanded && <StoriesHeading>Stories</StoriesHeading>}
{stories.map(s => (
<DocsStory key={s.id} expanded={expanded} {...s} />
))}
</>
);
}}
</DocsContext.Consumer>
);

const getDocsPageProps = (context: DocsContextProps): DocsPageProps => {
const { selectedKind, selectedStory, parameters } = context;
const {
hierarchyRootSeparator: rootSeparator,
hierarchySeparator: groupSeparator,
} = (parameters && parameters.options) || {
hierarchyRootSeparator: '|',
hierarchySeparator: '/',
};

const { groups } = parseKind(selectedKind, { rootSeparator, groupSeparator });
const title = (groups && groups[groups.length - 1]) || selectedKind;

return {
title,
subtitle: parameters && parameters.componentDescription,
};
};

const DocsPage: React.FunctionComponent = () => (
<DocsContext.Consumer>
{context => {
const docsPageProps = getDocsPageProps(context);
return (
<PureDocsPage {...docsPageProps}>
<Description of="." />
<DocsStories type={DocsStoriesType.PRIMARY} />
<Props of="." />
<DocsStories type={DocsStoriesType.REST} />
</PureDocsPage>
);
}}
</DocsContext.Consumer>
);

interface DocsPageWrapperProps {
context: DocsContextProps;
}

const DocsPageWrapper: React.FunctionComponent<DocsPageWrapperProps> = ({ context }) => (
/* eslint-disable react/destructuring-assignment */
<DocsContainer context={{ ...context, mdxKind: context.selectedKind }} content={DocsPage} />
);

export { DocsPageWrapper as DocsPage };
16 changes: 16 additions & 0 deletions addons/docs/src/blocks/Meta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

type Decorator = (...args: any) => any;

interface MetaProps {
title: string;
decorators?: [Decorator];
parameters?: any;
}

/**
* This component is used to declare component metadata in docs
* and gets transformed into a default export underneath the hood.
* It doesn't actually render anything.
*/
export const Meta: React.FunctionComponent<MetaProps> = props => null;
48 changes: 48 additions & 0 deletions addons/docs/src/blocks/Preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { ReactNodeArray } from 'react';
import { Preview as PurePreview, PreviewProps as PurePreviewProps } from '@storybook/components';
import { toId } from '@storybook/router';
import { getSourceProps } from './Source';
import { DocsContext, DocsContextProps } from './DocsContext';

export enum SourceState {
OPEN = 'open',
CLOSED = 'closed',
NONE = 'none',
}

type PreviewProps = PurePreviewProps & {
withSource?: SourceState;
};

const getPreviewProps = (
{
withSource = SourceState.CLOSED,
children,
...props
}: PreviewProps & { children?: React.ReactNode },
{ mdxKind, storyStore }: DocsContextProps
): PurePreviewProps => {
if (withSource === SourceState.NONE && !children) {
return props;
}
const childArray: ReactNodeArray = Array.isArray(children) ? children : [children];
const stories = childArray.filter(
(c: React.ReactElement) => c.props && (c.props.id || c.props.name)
) as React.ReactElement[];
const targetIds = stories.map(s => s.props.id || toId(mdxKind, s.props.name));
const sourceProps = getSourceProps({ ids: targetIds }, { storyStore });
return {
...props, // pass through columns etc.
withSource: sourceProps,
isExpanded: withSource === SourceState.OPEN,
};
};

export const Preview: React.FunctionComponent<PreviewProps> = props => (
<DocsContext.Consumer>
{context => {
const previewProps = getPreviewProps(props, context);
return <PurePreview {...previewProps}>{props.children}</PurePreview>;
}}
</DocsContext.Consumer>
);
Loading

1 comment on commit d579617

@vercel
Copy link

@vercel vercel bot commented on d579617 Jun 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.