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) => {