From 375cee1edd20d40d95dd0cf86541c00c50ea46e3 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 19 Jan 2023 17:16:19 +1100 Subject: [PATCH 1/4] Implement Controls block --- code/ui/blocks/src/argsHelpers.ts | 0 code/ui/blocks/src/blocks/ArgTypes.tsx | 2 - .../ui/blocks/src/blocks/Controls.stories.tsx | 69 +++++++++++++ code/ui/blocks/src/blocks/Controls.tsx | 98 +++++++++++++++++++ .../examples/ControlsParameters.stories.tsx | 55 +++++++++++ .../src/examples/ControlsParameters.tsx | 5 + 6 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 code/ui/blocks/src/argsHelpers.ts create mode 100644 code/ui/blocks/src/blocks/Controls.stories.tsx create mode 100644 code/ui/blocks/src/blocks/Controls.tsx create mode 100644 code/ui/blocks/src/examples/ControlsParameters.stories.tsx create mode 100644 code/ui/blocks/src/examples/ControlsParameters.tsx diff --git a/code/ui/blocks/src/argsHelpers.ts b/code/ui/blocks/src/argsHelpers.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/code/ui/blocks/src/blocks/ArgTypes.tsx b/code/ui/blocks/src/blocks/ArgTypes.tsx index d7443489a107..3d5f44d68bb2 100644 --- a/code/ui/blocks/src/blocks/ArgTypes.tsx +++ b/code/ui/blocks/src/blocks/ArgTypes.tsx @@ -20,8 +20,6 @@ type ArgTypesParameters = { type ArgTypesProps = ArgTypesParameters & { of: Renderer['component'] | ModuleExports; }; - -// TODO: generalize function extractComponentArgTypes( component: Renderer['component'], parameters: Parameters diff --git a/code/ui/blocks/src/blocks/Controls.stories.tsx b/code/ui/blocks/src/blocks/Controls.stories.tsx new file mode 100644 index 000000000000..ff8b6e9409aa --- /dev/null +++ b/code/ui/blocks/src/blocks/Controls.stories.tsx @@ -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 = { + component: Controls, + parameters: { + relativeCsfPaths: ['../examples/ControlsParameters.stories'], + }, +}; +export default meta; + +type Story = StoryObj; + +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, + }, +}; diff --git a/code/ui/blocks/src/blocks/Controls.tsx b/code/ui/blocks/src/blocks/Controls.tsx new file mode 100644 index 000000000000..3bd42f7d8fa5 --- /dev/null +++ b/code/ui/blocks/src/blocks/Controls.tsx @@ -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 cb = (changed: { storyId: string; args: Args }) => { + if (changed.storyId === storyId) { + setArgs(changed.args); + } + }; + context.channel.on(STORY_ARGS_UPDATED, cb); + return () => context.channel.off(STORY_ARGS_UPDATED, cb); + }, [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 cb = (changed: { globals: Globals }) => { + setGlobals(changed.globals); + }; + context.channel.on(GLOBALS_UPDATED, cb); + return () => context.channel.off(GLOBALS_UPDATED, cb); + }, [context.channel]); + + return [globals]; +}; + +export const Controls: FC = (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 ( + + ); +}; diff --git a/code/ui/blocks/src/examples/ControlsParameters.stories.tsx b/code/ui/blocks/src/examples/ControlsParameters.stories.tsx new file mode 100644 index 000000000000..48228d359460 --- /dev/null +++ b/code/ui/blocks/src/examples/ControlsParameters.stories.tsx @@ -0,0 +1,55 @@ +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 Controls', + 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; + +export default meta; +type Story = StoryObj; + +/** + * This is the primary mode for the button + * + * _this description was written as a comment above the story_ + */ +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' } } }, +}; diff --git a/code/ui/blocks/src/examples/ControlsParameters.tsx b/code/ui/blocks/src/examples/ControlsParameters.tsx new file mode 100644 index 000000000000..8d32189898d6 --- /dev/null +++ b/code/ui/blocks/src/examples/ControlsParameters.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +type PropTypes = { a?: string; b: string }; + +export const ControlsParameters = ({ a = 'a', b }: PropTypes) =>
Example story
; From b0386b983d80be8b2a0a56522d4742aee1b47f3f Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 19 Jan 2023 17:18:37 +1100 Subject: [PATCH 2/4] Use Controls in DocsPage --- code/ui/blocks/src/blocks/ArgTypes.tsx | 2 +- code/ui/blocks/src/blocks/Controls.tsx | 2 +- code/ui/blocks/src/blocks/DocsPage.tsx | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/code/ui/blocks/src/blocks/ArgTypes.tsx b/code/ui/blocks/src/blocks/ArgTypes.tsx index 3d5f44d68bb2..009d66cd8198 100644 --- a/code/ui/blocks/src/blocks/ArgTypes.tsx +++ b/code/ui/blocks/src/blocks/ArgTypes.tsx @@ -18,7 +18,7 @@ type ArgTypesParameters = { }; type ArgTypesProps = ArgTypesParameters & { - of: Renderer['component'] | ModuleExports; + of?: Renderer['component'] | ModuleExports; }; function extractComponentArgTypes( component: Renderer['component'], diff --git a/code/ui/blocks/src/blocks/Controls.tsx b/code/ui/blocks/src/blocks/Controls.tsx index 3bd42f7d8fa5..14c07c549cde 100644 --- a/code/ui/blocks/src/blocks/Controls.tsx +++ b/code/ui/blocks/src/blocks/Controls.tsx @@ -23,7 +23,7 @@ type ControlsParameters = { }; type ControlsProps = ControlsParameters & { - of: Renderer['component'] | ModuleExports; + of?: Renderer['component'] | ModuleExports; }; const useArgs = ( diff --git a/code/ui/blocks/src/blocks/DocsPage.tsx b/code/ui/blocks/src/blocks/DocsPage.tsx index a59c595d2c2e..bee87f66bcc9 100644 --- a/code/ui/blocks/src/blocks/DocsPage.tsx +++ b/code/ui/blocks/src/blocks/DocsPage.tsx @@ -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 = () => ( @@ -14,7 +13,7 @@ export const DocsPage: FC = () => ( - + ); From c04ae7d64be6451ad113fe680d74975e09f4eacf Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 19 Jan 2023 19:37:15 +1100 Subject: [PATCH 3/4] Remove unused file --- code/ui/blocks/src/argsHelpers.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 code/ui/blocks/src/argsHelpers.ts diff --git a/code/ui/blocks/src/argsHelpers.ts b/code/ui/blocks/src/argsHelpers.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From 17849d4459bb13b848795bd523d0cd8242ca656d Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 19 Jan 2023 19:58:27 +1100 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Jeppe Reinhold --- code/ui/blocks/src/blocks/Controls.tsx | 12 ++++++------ .../src/examples/ControlsParameters.stories.tsx | 7 +------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/code/ui/blocks/src/blocks/Controls.tsx b/code/ui/blocks/src/blocks/Controls.tsx index 14c07c549cde..390b2f2e3d66 100644 --- a/code/ui/blocks/src/blocks/Controls.tsx +++ b/code/ui/blocks/src/blocks/Controls.tsx @@ -35,13 +35,13 @@ const useArgs = ( const [args, setArgs] = useState(storyContext.args); useEffect(() => { - const cb = (changed: { storyId: string; args: Args }) => { + const onArgsUpdated = (changed: { storyId: string; args: Args }) => { if (changed.storyId === storyId) { setArgs(changed.args); } }; - context.channel.on(STORY_ARGS_UPDATED, cb); - return () => context.channel.off(STORY_ARGS_UPDATED, cb); + 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 }), @@ -59,11 +59,11 @@ const useGlobals = (story: PreparedStory, context: DocsContextProps): [Globals] const [globals, setGlobals] = useState(storyContext.globals); useEffect(() => { - const cb = (changed: { globals: Globals }) => { + const onGlobalsUpdated = (changed: { globals: Globals }) => { setGlobals(changed.globals); }; - context.channel.on(GLOBALS_UPDATED, cb); - return () => context.channel.off(GLOBALS_UPDATED, cb); + context.channel.on(GLOBALS_UPDATED, onGlobalsUpdated); + return () => context.channel.off(GLOBALS_UPDATED, onGlobalsUpdated); }, [context.channel]); return [globals]; diff --git a/code/ui/blocks/src/examples/ControlsParameters.stories.tsx b/code/ui/blocks/src/examples/ControlsParameters.stories.tsx index 48228d359460..cee0141cb61f 100644 --- a/code/ui/blocks/src/examples/ControlsParameters.stories.tsx +++ b/code/ui/blocks/src/examples/ControlsParameters.stories.tsx @@ -5,7 +5,7 @@ import { ControlsParameters } from './ControlsParameters'; * Reference stories to be used by the Controls stories */ const meta = { - title: 'Example/Stories for Controls', + title: 'Example/Stories for the Controls Block', component: ControlsParameters, args: { b: 'b' }, argTypes: { @@ -22,11 +22,6 @@ const meta = { export default meta; type Story = StoryObj; -/** - * This is the primary mode for the button - * - * _this description was written as a comment above the story_ - */ export const NoParameters: Story = { argTypes: { // @ts-expect-error Story type is trying to force us to use real props as args