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

Addon-controls: Conditional arg disabling #13890

Closed
wants to merge 12 commits into from
19 changes: 19 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<h1>Migration</h1>

- [From version 6.2.x to 6.3.0](#from-version-62x-to-630)
- [6.3 Deprecations](#63-deprecations)
- [Improved args disabling](#improved-args-disabling)
- [From version 6.1.x to 6.2.0](#from-version-61x-to-620)
- [MDX pattern tweaked](#mdx-pattern-tweaked)
- [6.2 Angular overhaul](#62-angular-overhaul)
Expand Down Expand Up @@ -151,6 +154,22 @@
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)

## From version 6.2.x to 6.3.0

### 6.3 Deprecations

#### Improved args disabling

We've simplified disabling arg display in 6.3 by hoisting the `table.disable` property. It is now the `disable` property instead.

```js
// before
const argTypes = { foo: { table: { disable: true } } };

// after
const argTypes = { foo: { disable: true } };
```

## From version 6.1.x to 6.2.0

### MDX pattern tweaked
Expand Down
2 changes: 1 addition & 1 deletion addons/controls/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export default {

export const CustomControls = (args) => <Button {...args} />;
CustomControls.argTypes = {
borderWidth: { table: { disable: true } },
borderWidth: { disable: true },
label: { control: { disable: true } },
};
```
Expand Down
5 changes: 3 additions & 2 deletions addons/docs/docs/multiframework.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface ArgType {
name?: string;
description?: string;
defaultValue?: any;
disable?: boolean | string;
[key: string]: any;
}

Expand Down Expand Up @@ -106,7 +107,7 @@ The input is the story function and the story context (id, parameters, args, etc

## Dynamic source rendering

With the release of Storybook 6.0, we've improved how stories are rendered in the [Source doc block](https://storybook.js.org/docs/react/writing-docs/doc-blocks#source). One of such improvements is the `dynamic` source type, which renders a snippet based on the output the story function.
With the release of Storybook 6.0, we've improved how stories are rendered in the [Source doc block](https://storybook.js.org/docs/react/writing-docs/doc-blocks#source). One of such improvements is the `dynamic` source type, which renders a snippet based on the output the story function.

This dynamic rendering is framework-specific, meaning it needs a custom implementation for each framework.

Expand Down Expand Up @@ -151,7 +152,7 @@ import { jsxDecorator } from './jsxDecorator';
export const decorators = [jsxDecorator];
```

This configures the `jsxDecorator` to be run on every story.
This configures the `jsxDecorator` to be run on every story.

<div class="aside">
To learn more and see how it's implemented in context, check out <a href="https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/react/jsxDecorator.tsx">the code</a> .
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
```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 hidden when advanced is "false"
margin: { control: 'number', disable: '!advanced' },
padding: { control: 'number', disable: '!advanced' },
cornerRadius: { control: 'number', disable: '!advanced' },
},
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
```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', disable: 'icon' },
image: {
control: { type: 'select', options: ['foo.jpg', 'bar.jpg'] },
disable: 'label'
},
},
};
```
46 changes: 37 additions & 9 deletions docs/essentials/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,7 @@ For `color` controls, you can specify an array of `presetColors`, either on the

export const parameters = {
controls: {
presetColors: [
{ color: '#ff4785', title: 'Coral' },
'rgba(0, 159, 183, 1)',
'#fe4a49',
]
presetColors: [{ color: '#ff4785', title: 'Coral' }, 'rgba(0, 159, 183, 1)', '#fe4a49'],
},
};
```
Expand Down Expand Up @@ -284,19 +280,51 @@ Resulting in the following change in Storybook UI:

The previous example also removed the prop documentation from the table. In some cases this is fine, however sometimes you might want to still render the prop documentation but without a control. The following example illustrates how:

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

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

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

<div class="aside">

As with other Storybook properties, such as [decorators](../writing-stories/decorators.md) the same principle can also be applied at a story-level for more granular cases.

</div>

### Conditional disabling

In some cases, it's useful to be able to conditionally disable a control based on the value of another control. Controls supports basic versions of these use cases with the `disable` option, 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.

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

<CodeSnippets
paths={[
'common/component-story-controls-conditional-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-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
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
```js
// YourComponent.stories.js | YourComponent.stories.ts

import { YourComponent } from './your-component'
import { YourComponent } from './your-component';

export default {
component: YourComponent,
title:'My Story',
argTypes:{
title: 'My Story',
argTypes: {
// foo is the property we want to remove from the UI
foo:{
control:false
}
}
foo: {
control: false,
},
},
};
```
```
18 changes: 8 additions & 10 deletions docs/snippets/common/component-story-disable-controls.js.mdx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
```js
// YourComponent.stories.js | YourComponent.stories.ts

import { YourComponent } from './your-component'
import { YourComponent } from './your-component';

export default {
component: YourComponent,
title:'My Story',
argTypes:{
title: 'My Story',
argTypes: {
// foo is the property we want to remove from the UI
foo:{
table:{
disable:true
}
}
}
foo: {
disable: true,
},
},
};
```
```
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import { YourComponent } from './your-component'
<Meta
title='My Story'
argTypes={{
foo:{
table:{
disable:true
}
foo: {
disable: true
}
}} />

Expand All @@ -22,4 +20,4 @@ export const Template = (args) => <YourComponent {...args} />
{Template.bind({})}
</Story>
</Canvas>
```
```
1 change: 1 addition & 0 deletions examples/angular-cli/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
{
files: ['./src/stories/addon-jest.stories.ts'],
rules: {
'@typescript-eslint/ban-ts-comment': 'warn',
'import/no-useless-path-segments': ignore,
},
settings: {
Expand Down
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',
disable: true,
},
mutuallyExclusiveA: { control: 'text', disable: 'mutuallyExclusiveB' },
mutuallyExclusiveB: { control: 'text', disable: 'mutuallyExclusiveA' },
colorMode: {
control: 'boolean',
},
dynamicText: {
disable: 'colorMode',
control: 'text',
},
dynamicColor: {
disable: '!colorMode',
control: 'color',
},
advanced: {
control: 'boolean',
},
margin: {
control: 'number',
disable: '!advanced',
},
padding: {
control: 'number',
disable: '!advanced',
},
cornerRadius: {
control: 'number',
disable: '!advanced',
},
someText: { control: 'text' },
subText: { control: 'text', disable: '!someText' },
anotherText: { control: 'text', disable: '!someText' },
},
parameters: {
chromatic: { disable: true },
Expand Down
1 change: 1 addition & 0 deletions lib/addons/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface ArgType {
name?: string;
description?: string;
defaultValue?: any;
disable?: boolean | string;
[key: string]: any;
}

Expand Down
1 change: 1 addition & 0 deletions lib/api/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export interface ArgType {
name?: string;
description?: string;
defaultValue?: any;
disable?: boolean | string;
[key: string]: any;
}

Expand Down
23 changes: 23 additions & 0 deletions lib/client-api/src/story_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,29 @@ describe('preview.story_store', () => {
);
});

it('disabled changes arg values that are passed to the story in the context', () => {
const storyFn = jest.fn();
const store = new StoryStore({ channel });
addStoryToStore(store, 'a', '1', storyFn, {
argTypes: {
truthy: {},
falsey: { disable: false },
one: { disable: true },
two: { disable: 'truthy' },
three: { disable: '!truthy' },
four: { disable: 'falsey' },
five: { disable: '!falsey' },
},
args: { truthy: 'hello', falsey: undefined, one: 1, two: 2, three: 3, four: 4, five: 5 },
});
store.getRawStory('a', '1').storyFn();

expect(storyFn).toHaveBeenCalledWith(
{ truthy: 'hello', falsey: undefined, three: 3, four: 4 },
expect.objectContaining({ args: { truthy: 'hello', falsey: undefined, three: 3, four: 4 } })
);
});

it('updateStoryArgs emits STORY_ARGS_UPDATED', () => {
const onArgsChangedChannel = jest.fn();
const testChannel = mockChannel();
Expand Down
33 changes: 28 additions & 5 deletions lib/client-api/src/story_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ const toExtracted = <T>(obj: T) =>
return Object.assign(acc, { [key]: value });
}, {});

const shouldDisable = (disable: boolean | string | undefined, args: Args) => {
if (disable === true) return true;
if (typeof disable === 'string' && disable.length > 0) {
if (disable[0] === '!') {
return !args[disable.substr(1)];
}
return !!args[disable];
}
return !!disable;
};

export default class StoryStore {
_error?: ErrorLike;

Expand Down Expand Up @@ -386,13 +397,25 @@ export default class StoryStore {
const finalStoryFn = (context: StoryContext) => {
const { args = {}, argTypes = {}, parameters } = context;
const { passArgsFirst = true } = parameters;

const mappedArgs = Object.entries(args).reduce((acc, [key, val]) => {
const { mapping } = argTypes[key] || {};
acc[key] = mapping && val in mapping ? mapping[val] : val;
return acc;
}, {} as Args);

const enabledArgs = Object.entries(mappedArgs).reduce((acc, [key, val]) => {
const { disable } = argTypes[key] || {};
const disabled = shouldDisable(disable, mappedArgs);
if (!disabled) {
acc[key] = val;
}
return acc;
}, {} as Args);

const mapped = {
...context,
args: Object.entries(args).reduce((acc, [key, val]) => {
const { mapping } = argTypes[key] || {};
acc[key] = mapping && val in mapping ? mapping[val] : val;
return acc;
}, {} as Args),
args: enabledArgs,
};
return passArgsFirst
? (original as ArgsStoryFn)(mapped.args, mapped)
Expand Down
Loading