Skip to content

Commit

Permalink
Merge pull request #20683 from storybookjs/tom/sb-1152-implement-cont…
Browse files Browse the repository at this point in the history
…rols-block

Docs: Implement Controls block
  • Loading branch information
tmeasday authored Jan 20, 2023
2 parents 00a64b4 + 5f2a98f commit 7eaa9c4
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 6 deletions.
4 changes: 1 addition & 3 deletions code/ui/blocks/src/blocks/ArgTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ type ArgTypesParameters = {
};

type ArgTypesProps = ArgTypesParameters & {
of: Renderer['component'] | ModuleExports;
of?: Renderer['component'] | ModuleExports;
};

// TODO: generalize
function extractComponentArgTypes(
component: Renderer['component'],
parameters: Parameters
Expand Down
69 changes: 69 additions & 0 deletions code/ui/blocks/src/blocks/Controls.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Controls } from './Controls';
import * as ExampleStories from '../examples/ControlsParameters.stories';

const meta: Meta<typeof Controls> = {
component: Controls,
parameters: {
relativeCsfPaths: ['../examples/ControlsParameters.stories'],
},
};
export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const OfStory: Story = {
args: {
of: ExampleStories.NoParameters,
},
};

// NOTE: this will throw with no of prop
export const OfStoryUnattached: Story = {
parameters: { attached: false },
args: {
of: ExampleStories.NoParameters,
},
};

export const IncludeProp: Story = {
args: {
of: ExampleStories.NoParameters,
include: ['a'],
},
};

export const IncludeParameter: Story = {
args: {
of: ExampleStories.Include,
},
};

export const ExcludeProp: Story = {
args: {
of: ExampleStories.NoParameters,
exclude: ['a'],
},
};

export const ExcludeParameter: Story = {
args: {
of: ExampleStories.Exclude,
},
};

export const SortProp: Story = {
args: {
of: ExampleStories.NoParameters,
sort: 'alpha',
},
};

export const SortParameter: Story = {
args: {
of: ExampleStories.Sort,
},
};
98 changes: 98 additions & 0 deletions code/ui/blocks/src/blocks/Controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* eslint-disable react/destructuring-assignment */
import type { Args, Globals, Renderer } from '@storybook/csf';
import type { DocsContextProps, ModuleExports, PreparedStory } from '@storybook/types';
import type { FC } from 'react';
import React, { useCallback, useEffect, useState, useContext } from 'react';
import type { PropDescriptor } from '@storybook/preview-api';
import { filterArgTypes } from '@storybook/preview-api';
import {
STORY_ARGS_UPDATED,
UPDATE_STORY_ARGS,
RESET_STORY_ARGS,
GLOBALS_UPDATED,
} from '@storybook/core-events';

import type { SortType } from '../components';
import { ArgsTable as PureArgsTable } from '../components';
import { DocsContext } from './DocsContext';

type ControlsParameters = {
include?: PropDescriptor;
exclude?: PropDescriptor;
sort?: SortType;
};

type ControlsProps = ControlsParameters & {
of?: Renderer['component'] | ModuleExports;
};

const useArgs = (
story: PreparedStory,
context: DocsContextProps
): [Args, (args: Args) => void, (argNames?: string[]) => void] => {
const storyContext = context.getStoryContext(story);
const { id: storyId } = story;

const [args, setArgs] = useState(storyContext.args);
useEffect(() => {
const onArgsUpdated = (changed: { storyId: string; args: Args }) => {
if (changed.storyId === storyId) {
setArgs(changed.args);
}
};
context.channel.on(STORY_ARGS_UPDATED, onArgsUpdated);
return () => context.channel.off(STORY_ARGS_UPDATED, onArgsUpdated);
}, [storyId, context.channel]);
const updateArgs = useCallback(
(updatedArgs) => context.channel.emit(UPDATE_STORY_ARGS, { storyId, updatedArgs }),
[storyId, context.channel]
);
const resetArgs = useCallback(
(argNames?: string[]) => context.channel.emit(RESET_STORY_ARGS, { storyId, argNames }),
[storyId, context.channel]
);
return [args, updateArgs, resetArgs];
};

const useGlobals = (story: PreparedStory, context: DocsContextProps): [Globals] => {
const storyContext = context.getStoryContext(story);

const [globals, setGlobals] = useState(storyContext.globals);
useEffect(() => {
const onGlobalsUpdated = (changed: { globals: Globals }) => {
setGlobals(changed.globals);
};
context.channel.on(GLOBALS_UPDATED, onGlobalsUpdated);
return () => context.channel.off(GLOBALS_UPDATED, onGlobalsUpdated);
}, [context.channel]);

return [globals];
};

export const Controls: FC<ControlsProps> = (props) => {
const { of } = props;
const context = useContext(DocsContext);
const { story } = context.resolveOf(of || 'story', ['story']);
const { parameters, argTypes } = story;
const controlsParameters = parameters.docs?.controls || ({} as ControlsParameters);

const include = props.include ?? controlsParameters.include;
const exclude = props.exclude ?? controlsParameters.exclude;
const sort = props.sort ?? controlsParameters.sort;

const [args, updateArgs, resetArgs] = useArgs(story, context);
const [globals] = useGlobals(story, context);

const filteredArgTypes = filterArgTypes(argTypes, include, exclude);

return (
<PureArgsTable
rows={filteredArgTypes}
args={args}
globals={globals}
updateArgs={updateArgs}
resetArgs={resetArgs}
sort={sort}
/>
);
};
5 changes: 2 additions & 3 deletions code/ui/blocks/src/blocks/DocsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { Title } from './Title';
import { Subtitle } from './Subtitle';
import { Description } from './Description';
import { Primary } from './Primary';
import { PRIMARY_STORY } from './types';
import { ArgsTable } from './ArgsTable';
import { Controls } from './Controls';
import { Stories } from './Stories';

export const DocsPage: FC = () => (
Expand All @@ -14,7 +13,7 @@ export const DocsPage: FC = () => (
<Subtitle />
<Description />
<Primary />
<ArgsTable story={PRIMARY_STORY} />
<Controls />
<Stories />
</>
);
50 changes: 50 additions & 0 deletions code/ui/blocks/src/examples/ControlsParameters.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ControlsParameters } from './ControlsParameters';

/**
* Reference stories to be used by the Controls stories
*/
const meta = {
title: 'Example/Stories for the Controls Block',
component: ControlsParameters,
args: { b: 'b' },
argTypes: {
// @ts-expect-error Meta type is trying to force us to use real props as args
extraMetaArgType: {
type: { name: 'string' },
name: 'Extra Meta',
description: 'An extra argtype added at the meta level',
table: { defaultValue: { summary: "'a default value'" } },
},
},
} satisfies Meta<typeof ControlsParameters>;

export default meta;
type Story = StoryObj<typeof meta>;

export const NoParameters: Story = {
argTypes: {
// @ts-expect-error Story type is trying to force us to use real props as args
extraStoryArgType: {
type: { name: 'string' },
name: 'Extra Story',
description: 'An extra argtype added at the story level',
table: { defaultValue: { summary: "'a default value'" } },
},
},
};

export const Include = {
...NoParameters,
parameters: { docs: { controls: { include: ['a'] } } },
};

export const Exclude = {
...NoParameters,
parameters: { docs: { controls: { exclude: ['a'] } } },
};

export const Sort = {
...NoParameters,
parameters: { docs: { controls: { sort: 'alpha' } } },
};
5 changes: 5 additions & 0 deletions code/ui/blocks/src/examples/ControlsParameters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

type PropTypes = { a?: string; b: string };

export const ControlsParameters = ({ a = 'a', b }: PropTypes) => <div>Example story</div>;

0 comments on commit 7eaa9c4

Please sign in to comment.