diff --git a/MIGRATION.md b/MIGRATION.md index 52cb910ef59..30e349770d4 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,6 +2,8 @@ - [From version 6.4.x to 6.5.0](#from-version-64x-to-650) - [CSF3 auto-title redundant filename](#csf3-auto-title-redundant-filename) + - [6.5 Deprecations](#65-deprecations) + - [Improved args disabling](#improved-args-disabling) - [From version 6.3.x to 6.4.0](#from-version-63x-to-640) - [Automigrate](#automigrate) - [CRA5 upgrade](#cra5-upgrade) @@ -207,6 +209,21 @@ Since CSF3 is experimental, we are introducing this technically breaking change export default { title: 'Atoms/Button/Button' }; ``` +### 6.5 Deprecations + +#### Improved args disabling + +We've simplified disabling arg display in 6.5 by replacing the `table.disable` property with `includeIf`/`excludeIf` property: + +```js +// before +const argTypes = { foo: { table: { disable: true } } }; +// after +const argTypes = { foo: { includeIf: false } }; +``` + +In addition to being one less level of nesting in the ArgType declaration, `includeIf`/`excludeIf` can also accept the name of another arg (a string) can conditionally include/exclude the arg based on the runtime value of the other arg. + ## From version 6.3.x to 6.4.0 ### Automigrate diff --git a/docs/essentials/controls.md b/docs/essentials/controls.md index 9ee5f664cfb..8b0a64aceca 100644 --- a/docs/essentials/controls.md +++ b/docs/essentials/controls.md @@ -300,6 +300,34 @@ paths={[ +### Conditional controls + +In some cases, it's useful to be able to conditionally exclude a control based on the value of another control. Controls supports basic versions of these use cases with the `enableIf` and `disableIf` options, which can take a boolean value, or a string which can refer to the value of another arg. + +Consider a collection of "advanced" settings that are only visible when the user toggles an "advanced" toggle. + + + + + + + +Or consider a constraint where if the user sets one control value, it doesn't make sense for the user to be able to set another value. + + + + + + + ## Hide NoControls warning If you don't plan to handle the control args inside your Story, you can remove the warning with: @@ -348,4 +376,4 @@ Consider the following snippet to force required args first: ]} /> - \ No newline at end of file + diff --git a/docs/snippets/common/component-story-conditional-controls-mutual-exclusion.js.mdx b/docs/snippets/common/component-story-conditional-controls-mutual-exclusion.js.mdx new file mode 100644 index 00000000000..f489e88effa --- /dev/null +++ b/docs/snippets/common/component-story-conditional-controls-mutual-exclusion.js.mdx @@ -0,0 +1,16 @@ +```js +// Button.stories.js +import { Button } from './Button'; +export default { + component: Button, + title: 'Button', + argTypes: { + // button can be passed a label or an image, not both + label: { control: 'text', excludeIf: 'image' }, + image: { + control: { type: 'select', options: ['foo.jpg', 'bar.jpg'] }, + excludeIf: 'label', + }, + }, +}; +``` diff --git a/docs/snippets/common/component-story-conditional-controls-toggle.js.mdx b/docs/snippets/common/component-story-conditional-controls-toggle.js.mdx new file mode 100644 index 00000000000..5a96b0970dd --- /dev/null +++ b/docs/snippets/common/component-story-conditional-controls-toggle.js.mdx @@ -0,0 +1,16 @@ +```js +// Button.stories.js +import { Button } from './Button'; +export default { + component: Button, + title: 'Button', + argTypes: { + label: { control: 'text' }, // always shows + advanced: { control: 'boolean' }, + // below are only included when advanced is true + margin: { control: 'number', includeIf: 'advanced' }, + padding: { control: 'number', includeIf: 'advanced' }, + cornerRadius: { control: 'number', includeIf: 'advanced' }, + }, +}; +``` diff --git a/docs/snippets/common/component-story-disable-controls.js.mdx b/docs/snippets/common/component-story-disable-controls.js.mdx index 4bbcdaccf3a..bb883817f8c 100644 --- a/docs/snippets/common/component-story-disable-controls.js.mdx +++ b/docs/snippets/common/component-story-disable-controls.js.mdx @@ -5,18 +5,16 @@ import { YourComponent } from './YourComponent'; export default { /* 👇 The title prop is optional. - * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading - * to learn how to generate automatic titles - */ + * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ title: 'YourComponent', component: YourComponent, argTypes: { // foo is the property we want to remove from the UI foo: { - table: { - disable: true, - }, + excludeIf: true, }, }, }; -``` \ No newline at end of file +``` diff --git a/docs/snippets/common/component-story-disable-controls.mdx.mdx b/docs/snippets/common/component-story-disable-controls.mdx.mdx index 04f50ba86a6..29c1e4bc2f6 100644 --- a/docs/snippets/common/component-story-disable-controls.mdx.mdx +++ b/docs/snippets/common/component-story-disable-controls.mdx.mdx @@ -10,9 +10,7 @@ import { YourComponent } from './YourComponent' component={YourComponent} argTypes={{ foo:{ - table:{ - disable: true, - } + excludeIf: true, } }} /> -``` \ No newline at end of file +``` diff --git a/examples/official-storybook/stories/addon-controls.stories.tsx b/examples/official-storybook/stories/addon-controls.stories.tsx index ec83aa20c37..26c81633094 100644 --- a/examples/official-storybook/stories/addon-controls.stories.tsx +++ b/examples/official-storybook/stories/addon-controls.stories.tsx @@ -28,6 +28,41 @@ export default { ], }, }, + staticDisable: { + name: 'Static disabled', + excludeIf: true, + }, + mutuallyExclusiveA: { control: 'text', excludeIf: 'mutuallyExclusiveB' }, + mutuallyExclusiveB: { control: 'text', excludeIf: 'mutuallyExclusiveA' }, + colorMode: { + control: 'boolean', + }, + dynamicText: { + excludeIf: 'colorMode', + control: 'text', + }, + dynamicColor: { + includeIf: 'colorMode', + control: 'color', + }, + advanced: { + control: 'boolean', + }, + margin: { + control: 'number', + includeIf: 'advanced', + }, + padding: { + control: 'number', + includeIf: 'advanced', + }, + cornerRadius: { + control: 'number', + includeIf: 'advanced', + }, + someText: { control: 'text' }, + subText: { control: 'text', includeIf: 'someText' }, + anotherText: { control: 'text', includeIf: 'someText' }, }, parameters: { chromatic: { disable: true }, diff --git a/lib/api/src/index.tsx b/lib/api/src/index.tsx index 61b6a0d23b9..69ed88e0f59 100644 --- a/lib/api/src/index.tsx +++ b/lib/api/src/index.tsx @@ -116,6 +116,8 @@ export interface ArgType { name?: string; description?: string; defaultValue?: any; + includeIf?: boolean | string; + excludeIf?: boolean | string; [key: string]: any; } diff --git a/lib/components/src/blocks/ArgsTable/ArgsTable.tsx b/lib/components/src/blocks/ArgsTable/ArgsTable.tsx index faabe0e11c2..88be908a09f 100644 --- a/lib/components/src/blocks/ArgsTable/ArgsTable.tsx +++ b/lib/components/src/blocks/ArgsTable/ArgsTable.tsx @@ -1,7 +1,10 @@ import React, { FC } from 'react'; +import dedent from 'ts-dedent'; +import deprecate from 'util-deprecate'; import pickBy from 'lodash/pickBy'; import { styled, ignoreSsrWarning } from '@storybook/theming'; import { opacify, transparentize, darken, lighten } from 'polished'; +import { includeConditionalArg } from '@storybook/csf'; import { Icons } from '../../icon/icon'; import { ArgRow } from './ArgRow'; import { SectionRow } from './SectionRow'; @@ -10,6 +13,15 @@ import { EmptyBlock } from '../EmptyBlock'; import { Link } from '../../typography/link/link'; import { ResetWrapper } from '../../typography/ResetWrapper'; +const warnTableDisableDeprecated = deprecate( + () => {}, + dedent` + Use 'show' or 'hide' instead of 'table.disable' to disable ArgsTable rows. + + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#conditional-controls + ` +); + export const TableWrapper = styled.table<{ compact?: boolean; inAddonPanel?: boolean; @@ -397,8 +409,12 @@ export const ArgsTable: FC = (props) => { const isLoading = 'isLoading' in props; const { rows, args } = 'rows' in props ? props : argsTableLoadingData; + if (Object.values(rows).some((row) => row?.table?.disable)) { + warnTableDisableDeprecated(); + } + const groups = groupRows( - pickBy(rows, (row) => !row?.table?.disable), + pickBy(rows, (row) => !row?.table?.disable && includeConditionalArg(row, args)), sort ); diff --git a/lib/components/src/blocks/ArgsTable/types.ts b/lib/components/src/blocks/ArgsTable/types.ts index 4b8a3fa4941..a99065beeed 100644 --- a/lib/components/src/blocks/ArgsTable/types.ts +++ b/lib/components/src/blocks/ArgsTable/types.ts @@ -32,6 +32,8 @@ export interface ArgType { name?: string; description?: string; defaultValue?: any; + includeIf?: boolean | string; + excludeIf?: boolean | string; [key: string]: any; } diff --git a/lib/store/src/csf/prepareStory.ts b/lib/store/src/csf/prepareStory.ts index fe5cd8b862d..2b54f74c699 100644 --- a/lib/store/src/csf/prepareStory.ts +++ b/lib/store/src/csf/prepareStory.ts @@ -11,6 +11,7 @@ import { StoryContext, AnyFramework, StrictArgTypes, + includeConditionalArg, } from '@storybook/csf'; import { @@ -165,12 +166,18 @@ export function prepareStory( acc[key] = mapping && val in mapping ? mapping[val] : val; return acc; }, {} as Args); - const mappedContext = { ...context, args: mappedArgs }; + const includedArgs = Object.entries(mappedArgs).reduce((acc, [key, val]) => { + const argType = context.argTypes[key] || {}; + if (includeConditionalArg(argType, mappedArgs)) acc[key] = val; + return acc; + }, {} as Args); + + const includedContext = { ...context, args: includedArgs }; const { passArgsFirst: renderTimePassArgsFirst = true } = context.parameters; return renderTimePassArgsFirst - ? (render as ArgsStoryFn)(mappedContext.args, mappedContext) - : (render as LegacyStoryFn)(mappedContext); + ? (render as ArgsStoryFn)(includedContext.args, includedContext) + : (render as LegacyStoryFn)(includedContext); }; const decoratedStoryFn = applyHooks(applyDecorators)(undecoratedStoryFn, decorators); const unboundStoryFn = (context: StoryContext) => {