Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controls: Add conditional controls #17536

Merged
merged 11 commits into from
Mar 23, 2022
17 changes: 17 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion docs/essentials/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,34 @@ paths={[

</div>

### 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.
tmeasday marked this conversation as resolved.
Show resolved Hide resolved

Consider a collection of "advanced" settings that are only visible when the user toggles an "advanced" toggle.

<!-- prettier-ignore-start -->

<CodeSnippets
paths={[
'common/component-story-conditional-controls-toggle.js.mdx',
]}
/>

<!-- prettier-ignore-end -->

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.

<!-- prettier-ignore-start -->

<CodeSnippets
paths={[
'common/component-story-conditional-controls-mutual-exclusion.js.mdx',
]}
/>

<!-- prettier-ignore-end -->

## Hide NoControls warning

If you don't plan to handle the control args inside your Story, you can remove the warning with:
Expand Down Expand Up @@ -348,4 +376,4 @@ Consider the following snippet to force required args first:
]}
/>

<!-- prettier-ignore-end -->
<!-- prettier-ignore-end -->
Original file line number Diff line number Diff line change
@@ -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',
},
},
};
```
Original file line number Diff line number Diff line change
@@ -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' },
yannbf marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, users have to opt into the advanced controls in this case right?

// below are only included when advanced is true
margin: { control: 'number', includeIf: 'advanced' },
padding: { control: 'number', includeIf: 'advanced' },
cornerRadius: { control: 'number', includeIf: 'advanced' },
},
};
```
12 changes: 5 additions & 7 deletions docs/snippets/common/component-story-disable-controls.js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
};
```
```
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import { YourComponent } from './YourComponent'
component={YourComponent}
argTypes={{
foo:{
table:{
disable: true,
}
excludeIf: true,
}
}} />
```
```
35 changes: 35 additions & 0 deletions examples/official-storybook/stories/addon-controls.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 2 additions & 0 deletions lib/api/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export interface ArgType {
name?: string;
description?: string;
defaultValue?: any;
includeIf?: boolean | string;
excludeIf?: boolean | string;
[key: string]: any;
}

Expand Down
18 changes: 17 additions & 1 deletion lib/components/src/blocks/ArgsTable/ArgsTable.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.
yannbf marked this conversation as resolved.
Show resolved Hide resolved

https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#conditional-controls
`
);

export const TableWrapper = styled.table<{
compact?: boolean;
inAddonPanel?: boolean;
Expand Down Expand Up @@ -397,8 +409,12 @@ export const ArgsTable: FC<ArgsTableProps> = (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)),
shilman marked this conversation as resolved.
Show resolved Hide resolved
sort
);

Expand Down
2 changes: 2 additions & 0 deletions lib/components/src/blocks/ArgsTable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface ArgType {
name?: string;
description?: string;
defaultValue?: any;
includeIf?: boolean | string;
excludeIf?: boolean | string;
[key: string]: any;
}

Expand Down
13 changes: 10 additions & 3 deletions lib/store/src/csf/prepareStory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
StoryContext,
AnyFramework,
StrictArgTypes,
includeConditionalArg,
} from '@storybook/csf';

import {
Expand Down Expand Up @@ -165,12 +166,18 @@ export function prepareStory<TFramework extends AnyFramework>(
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);
tmeasday marked this conversation as resolved.
Show resolved Hide resolved

const includedContext = { ...context, args: includedArgs };
const { passArgsFirst: renderTimePassArgsFirst = true } = context.parameters;
return renderTimePassArgsFirst
? (render as ArgsStoryFn<TFramework>)(mappedContext.args, mappedContext)
: (render as LegacyStoryFn<TFramework>)(mappedContext);
? (render as ArgsStoryFn<TFramework>)(includedContext.args, includedContext)
: (render as LegacyStoryFn<TFramework>)(includedContext);
};
const decoratedStoryFn = applyHooks<TFramework>(applyDecorators)(undecoratedStoryFn, decorators);
const unboundStoryFn = (context: StoryContext<TFramework>) => {
Expand Down