Skip to content

Commit

Permalink
Merge pull request #20563 from storybookjs/block-description-improvem…
Browse files Browse the repository at this point in the history
…ents

Blocks: new Description API, introduce useOf
  • Loading branch information
JReinhold authored Jan 11, 2023
2 parents 16877c4 + 19cba6e commit f7826d5
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 35 deletions.
13 changes: 10 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- [Docs Changes](#docs-changes)
- [Standalone docs files](#standalone-docs-files)
- [Referencing stories in docs files](#referencing-stories-in-docs-files)
- [Description block, `parameters.notes` and `parameters.info`](#description-block-parametersnotes-and-parametersinfo)
- [Autodocs](#autodocs)
- [Configuring the Docs Container](#configuring-the-docs-container)
- [External Docs](#external-docs)
Expand Down Expand Up @@ -306,8 +307,8 @@ To opt-out of the old behavior you can set the `storyStoreV7` feature flag to `f
module.exports = {
features: {
storyStoreV7: false,
}
}
},
};
```

#### Removed global client APIs
Expand Down Expand Up @@ -796,6 +797,13 @@ import * as SecondComponentStories from './second-component.stories';
<Story of={SecondComponentStories.standard} meta={SecondComponentStories} />
```

#### Description block, `parameters.notes` and `parameters.info`

In 6.5 the Description doc block accepted a range of different props, `markdown`, `type` and `children` as a way to customize the content.
The props have been simplified and the block now only accepts an `of` prop, which can be a reference to either a CSF file, a default export (meta) or a story export, depending on which description you want to be shown. See TDB DOCS LINK for a deeper explanation of the new prop.

`parameters.notes` and `parameters.info` have been deprecated as a way to specify descriptions. Instead use JSDoc comments above the default export or story export, or use `parameters.docs.description.story | component` directly. See TDB DOCS LINK for a deeper explanation on how to write descriptions.

#### Autodocs

In 7.0, rather than rendering each story in "docs view mode", Autodocs (formerly known as "Docs Page") operates by adding additional sidebar entries for each component. By default it uses the same template as was used in 6.x, and the entries are entitled `Docs`.
Expand Down Expand Up @@ -3812,4 +3820,3 @@ If you **are** using these addons, it takes two steps to migrate:
```
<!-- markdown-link-check-enable -->
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
if (attach) this.attachCSFFile(resolved.csfFile);
}

get projectAnnotations() {
const { projectAnnotations } = this.store;
if (!projectAnnotations) {
throw new Error("Can't get projectAnnotations from DocsContext before they are initialized");
}
return projectAnnotations;
}

resolveModuleExport(moduleExportOrType: ModuleExport | ResolvedModuleExport<TRenderer>['type']) {
// If passed a type, we return the attached file, component or primary story
if (moduleExportOrType === 'story') {
Expand Down
13 changes: 12 additions & 1 deletion code/lib/types/src/modules/docs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { Channel } from '@storybook/channels';
import type { Renderer, StoryContextForLoaders, StoryId, StoryName, Parameters } from './csf';
import type { ModuleExport, ModuleExports, CSFFile, PreparedStory } from './story';
import type {
ModuleExport,
ModuleExports,
CSFFile,
PreparedStory,
NormalizedProjectAnnotations,
} from './story';

export type StoryRenderOptions = {
autoplay?: boolean;
Expand Down Expand Up @@ -72,6 +78,11 @@ export interface DocsContextProps<TRenderer extends Renderer = Renderer> {
* Storybook channel -- use for low level event watching/emitting
*/
channel: Channel;

/**
* Project annotations -- can be read to get the project's global annotations
*/
projectAnnotations: NormalizedProjectAnnotations<TRenderer>;
}

export type DocsRenderFunction<TRenderer extends Renderer> = (
Expand Down
117 changes: 113 additions & 4 deletions code/ui/blocks/src/blocks/Description.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,132 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Description } from './Description';
import { Button } from '../examples/Button';
import { Button as ButtonComponent } from '../examples/Button';
import * as DefaultButtonStories from '../examples/Button.stories';
import * as ButtonStoriesWithMetaDescriptionAsParameter from '../examples/ButtonWithMetaDescriptionAsParameter.stories';
import * as ButtonStoriesWithMetaDescriptionAsComment from '../examples/ButtonWithMetaDescriptionAsComment.stories';
import * as ButtonStoriesWithMetaDescriptionAsBoth from '../examples/ButtonWithMetaDescriptionAsBoth.stories';

const meta: Meta<typeof Description> = {
component: Description,
parameters: {
relativeCsfPaths: ['../examples/Button.stories'],
controls: {
include: [],
hideNoControlsWarning: true,
},
// workaround for https://github.com/storybookjs/storybook/issues/20505
docs: { source: { type: 'code' } },
attached: false,
},
};
export default meta;

type Story = StoryObj<typeof meta>;

export const ButtonComponent: Story = {
export const OfComponentAsComponentComment: Story = {
args: {
of: Button,
of: ButtonComponent,
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'] },
};
export const OfCSFFileAsComponentComment: Story = {
args: {
of: DefaultButtonStories,
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'] },
};
export const OfCSFFileAsMetaComment: Story = {
args: {
of: ButtonStoriesWithMetaDescriptionAsComment,
},
parameters: {
relativeCsfPaths: ['../examples/ButtonWithMetaDescriptionAsComment.stories'],
},
};
export const OfCSFFileAsParameter: Story = {
args: {
of: ButtonStoriesWithMetaDescriptionAsParameter,
},
parameters: {
relativeCsfPaths: ['../examples/ButtonWithMetaDescriptionAsParameter.stories'],
},
};
export const OfCSFFileAsMetaCommentAndParameter: Story = {
args: {
of: ButtonStoriesWithMetaDescriptionAsBoth,
},
parameters: {
relativeCsfPaths: ['../examples/ButtonWithMetaDescriptionAsBoth.stories'],
},
};
export const OfMetaAsComponentComment: Story = {
args: {
of: DefaultButtonStories.default,
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'] },
};
export const OfMetaAsMetaComment: Story = {
args: {
of: ButtonStoriesWithMetaDescriptionAsComment.default,
},
parameters: {
relativeCsfPaths: ['../examples/ButtonWithMetaDescriptionAsComment.stories'],
},
};
export const OfMetaAsParameter: Story = {
args: {
of: ButtonStoriesWithMetaDescriptionAsParameter.default,
},
parameters: {
relativeCsfPaths: ['../examples/ButtonWithMetaDescriptionAsParameter.stories'],
},
};
export const OfMetaAsMetaCommentAndParameter: Story = {
args: {
of: ButtonStoriesWithMetaDescriptionAsBoth.default,
},
parameters: {
relativeCsfPaths: ['../examples/ButtonWithMetaDescriptionAsBoth.stories'],
},
};
export const OfStoryAsComment: Story = {
args: {
of: DefaultButtonStories.Primary,
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'] },
};
export const OfStoryAsParameter: Story = {
args: {
of: DefaultButtonStories.Secondary,
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'] },
};
export const OfStoryAsStoryCommentAndParameter: Story = {
args: {
of: DefaultButtonStories.Large,
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'] },
};
export const OfUndefinedAttached: Story = {
parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true },
};
export const OfStringComponentAttached: Story = {
name: 'Of "component" Attached',
args: {
of: 'component',
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true },
};
export const OfStringMetaAttached: Story = {
name: 'Of "meta" Attached',
args: {
of: 'meta',
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true },
};
export const OfStringStoryAttached: Story = {
name: 'Of "story" Attached',
args: {
of: 'story',
},
parameters: { relativeCsfPaths: ['../examples/Button.stories'], attached: true },
};
101 changes: 77 additions & 24 deletions code/ui/blocks/src/blocks/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { FC } from 'react';
import React, { useContext } from 'react';
import { str } from '@storybook/docs-tools';
import { deprecate } from '@storybook/client-logger';
import type { DescriptionProps as PureDescriptionProps } from '../components';
import { combineParameters } from '@storybook/preview-api';
import { Description } from '../components';

import type { DocsContextProps } from './DocsContext';
import { DocsContext } from './DocsContext';
import type { Component } from './types';
import { PRIMARY_STORY } from './types';
import type { Of } from './useOf';
import { useOf } from './useOf';

export enum DescriptionType {
INFO = 'info',
Expand All @@ -20,18 +21,25 @@ export enum DescriptionType {
type Notes = string | any;
type Info = string | any;

const DEPRECATION_MIGRATION_LINK =
'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#description-block-parametersnotes-and-parametersinfo';

interface DescriptionProps {
of?: '.' | Component;
/**
* @deprecated Manually specifying description type is deprecated. In the future all descriptions will be extracted from JSDocs on the meta, story or component.
* Specify where to get the description from. Can be a component, a CSF file or a story.
* If not specified, the description will be extracted from the meta of the attached CSF file.
*/
of?: Of;
/**
* @deprecated Manually specifying description type is deprecated. See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#description-block-parametersnotes-and-parametersinfo
*/
type?: DescriptionType;
/**
* @deprecated The 'markdown' prop on the Description block is deprecated. Write the markdown directly in the .mdx file instead
* @deprecated The 'markdown' prop on the Description block is deprecated. See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#description-block-parametersnotes-and-parametersinfo
*/
markdown?: string;
/**
* @deprecated The 'children' prop on the Description block is deprecated. Write the markdown directly in the .mdx file instead
* @deprecated The 'children' prop on the Description block is deprecated. See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#description-block-parametersnotes-and-parametersinfo
*/
children?: string;
}
Expand All @@ -43,58 +51,103 @@ const getInfo = (info?: Info) => info && (typeof info === 'string' ? info : str(

const noDescription = (component?: Component): string | null => null;

export const useDescriptionProps = (
{ of, type, markdown, children }: DescriptionProps,
const getDescriptionFromResolvedOf = (resolvedOf: ReturnType<typeof useOf>): string | null => {
const { projectAnnotations, ...resolvedModule } = resolvedOf;
switch (resolvedModule.type) {
case 'story': {
return resolvedModule.story.parameters.docs?.description?.story || null;
}
case 'meta': {
const { meta } = resolvedModule.csfFile;
const metaDescription = meta.parameters.docs?.description?.component;
if (metaDescription) {
return metaDescription;
}
return (
projectAnnotations.parameters.docs?.extractComponentDescription(meta.component, {
component: meta.component,
...combineParameters(projectAnnotations.parameters, meta.parameters),
}) || null
);
}
case 'component': {
const { component } = resolvedModule;
return (
projectAnnotations.parameters.docs?.extractComponentDescription(component, {
component,
...projectAnnotations.parameters,
}) || null
);
}
default: {
throw new Error(
`Unrecognized module type resolved from 'useOf', got: ${(resolvedModule as any).type}`
);
}
}
};

const getDescriptionFromDeprecatedProps = (
{ type, markdown, children }: DescriptionProps,
{ storyById }: DocsContextProps<any>
): PureDescriptionProps => {
): string => {
const { component, parameters } = storyById();
if (children || markdown) {
return { markdown: children || markdown };
return children || markdown;
}
const { notes, info, docs } = parameters;
if (Boolean(notes) || Boolean(info)) {
deprecate(
"Using 'parameters.notes' or 'parameters.info' properties to describe stories is deprecated. Write JSDocs directly at the meta, story or component instead."
`Using 'parameters.notes' or 'parameters.info' properties to describe stories is deprecated. See ${DEPRECATION_MIGRATION_LINK}`
);
}

const { extractComponentDescription = noDescription, description } = docs || {};
const target = [PRIMARY_STORY].includes(of) ? component : of;

// override component description
const componentDescriptionParameter = description?.component;
if (componentDescriptionParameter) {
return { markdown: componentDescriptionParameter };
return componentDescriptionParameter;
}

switch (type) {
case DescriptionType.INFO:
return { markdown: getInfo(info) };
return getInfo(info);
case DescriptionType.NOTES:
return { markdown: getNotes(notes) };
return getNotes(notes);
case DescriptionType.DOCGEN:
case DescriptionType.AUTO:
default:
return { markdown: extractComponentDescription(target, { component, ...parameters }) };
return extractComponentDescription(component, { component, ...parameters });
}
};

const DescriptionContainer: FC<DescriptionProps> = (props = { of: PRIMARY_STORY }) => {
const DescriptionContainer: FC<DescriptionProps> = (props) => {
const { of, type, markdown: markdownProp, children } = props;
const context = useContext(DocsContext);
const { markdown } = useDescriptionProps(props, context);
if (props.type) {
const resolvedOf = useOf(of || 'meta');
let markdown;
if (type || markdownProp || children) {
// pre 7.0 mode with deprecated props
markdown = getDescriptionFromDeprecatedProps(props, context);
} else {
// 7.0 mode with new 'of' prop
// pre 7.0 with only 'of' prop only supported referencing a component, which 7.0 supports as well here
markdown = getDescriptionFromResolvedOf(resolvedOf);
}
if (type) {
deprecate(
'Manually specifying description type is deprecated. In the future all descriptions will be extracted from JSDocs on the meta, story or component.'
`Manually specifying description type is deprecated. See ${DEPRECATION_MIGRATION_LINK}`
);
}
if (props.markdown) {
if (markdownProp) {
deprecate(
"The 'markdown' prop on the Description block is deprecated. Write the markdown directly in the .mdx file instead"
`The 'markdown' prop on the Description block is deprecated. See ${DEPRECATION_MIGRATION_LINK}`
);
}
if (props.children) {
if (children) {
deprecate(
"The 'children' prop on the Description block is deprecated. Write the markdown directly in the .mdx file instead."
`The 'children' prop on the Description block is deprecated. See ${DEPRECATION_MIGRATION_LINK}`
);
}
return markdown ? <Description markdown={markdown} /> : null;
Expand Down
1 change: 1 addition & 0 deletions code/ui/blocks/src/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ export * from './Title';
export * from './Unstyled';
export * from './Wrapper';

export * from './useOf';
export * from './types';
export * from './mdx';
Loading

0 comments on commit f7826d5

Please sign in to comment.