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

Core: Hoist 'control.options', validate them in core and introduce 'control.labels' #14169

Merged
merged 12 commits into from
Mar 9, 2021
28 changes: 28 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Deprecated implicit PostCSS loader](#deprecated-implicit-postcss-loader)
- [Deprecated default PostCSS plugins](#deprecated-default-postcss-plugins)
- [Deprecated showRoots config option](#deprecated-showroots-config-option)
- [Deprecated control.options](#deprecated-controloptions)
- [From version 6.0.x to 6.1.0](#from-version-60x-to-610)
- [Addon-backgrounds preset](#addon-backgrounds-preset)
- [Single story hoisting](#single-story-hoisting)
Expand Down Expand Up @@ -246,6 +247,33 @@ addons.setConfig({

The top-level `showRoots` option will be removed in Storybook 7.0.

#### Deprecated control.options

Possible `options` for a radio/check/select controls has been moved up to the argType level, and no longer accepts an object. Instead, you should specify `options` as an array. You can use `control.labels` to customize labels. Additionally, you can use a `mapping` to deal with complex values.

```js
argTypes: {
answer:
options: ['yes', 'no'],
mapping: {
yes: <Check />,
no: <Cross />,
},
control: {
type: 'radio',
labels: {
yes: 'да',
no: 'нет',
}
}
}
}
```

Keys in `control.labels` as well as in `mapping` should match the values in `options`. Neither object has to be exhaustive, in case of a missing property, the option value will be used directly.

If you are currently using an object as value for `control.options`, be aware that the key and value are reversed in `control.labels`.

## From version 6.0.x to 6.1.0

### Addon-backgrounds preset
Expand Down
1 change: 0 additions & 1 deletion addons/controls/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"@storybook/addons": "6.2.0-beta.11",
"@storybook/api": "6.2.0-beta.11",
"@storybook/client-api": "6.2.0-beta.11",
"@storybook/client-logger": "6.2.0-beta.11",
"@storybook/components": "6.2.0-beta.11",
"@storybook/node-logger": "6.2.0-beta.11",
"@storybook/theming": "6.2.0-beta.11",
Expand Down
18 changes: 1 addition & 17 deletions addons/controls/src/ControlsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React, { FC, useEffect } from 'react';
import dedent from 'ts-dedent';
import React, { FC } from 'react';
import { useArgs, useArgTypes, useParameter } from '@storybook/api';
import { once } from '@storybook/client-logger';
import { ArgsTable, NoControlsWarning } from '@storybook/components';

import { PARAM_KEY } from './constants';
Expand All @@ -20,20 +18,6 @@ export const ControlsPanel: FC = () => {
{}
);

useEffect(() => {
if (
Object.values(rows).some(({ control: { options = {} } = {} }) =>
Object.values(options).some((v) => !['boolean', 'number', 'string'].includes(typeof v))
)
) {
once.warn(dedent`
Only primitives are supported as values in control options. Use a 'mapping' for complex values.

More info: https://storybook.js.org/docs/react/writing-stories/args#mapping-to-complex-arg-values
`);
}
}, [rows]);

const hasControls = Object.values(rows).some((arg) => arg?.control);
const showWarning = !(hasControls && isArgsStory) && !hideNoControlsWarning;

Expand Down
72 changes: 21 additions & 51 deletions docs/essentials/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,18 @@ This replaces the input with a radio group for a more intuitive experience.

For a few types, Controls will automatically infer them by using [regex](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp). You can change the matchers for a regex that suits you better.

| Data Type | Default regex | Description |
| :---------- | :----------: | :--------------------------------------------------------------------- |
| **color** | `/(background\|color)$/i` | will display a color picker UI for the args that match it |
| **date** | `/Date$/` | will display a date picker UI for the args that match it |
| Data type | Default regex | Description |
| :---------: | :-----------------------: | :-------------------------------------------------------: |
| **color** | `/(background\|color)$/i` | Will display a color picker UI for the args that match it |
| **date** | `/Date$/` | Will display a date picker UI for the args that match it |


To do so, use the `matchers` property in `controls` parameter:
<!-- prettier-ignore-start -->

<CodeSnippets
paths={[
'storybook-addon-controls-custom-matchers.js.mdx',
'common/storybook-addon-controls-custom-matchers.js.mdx',
]}
/>

Expand Down Expand Up @@ -128,9 +128,13 @@ By default, Storybook will add controls for all args that:

- Appear in the list of args for your story.

You can determine the control by using `argTypes` in each case.
Using `argTypes`, you can change the display and behavior of each control.

As they can be complex cases:
### Dealing with complex values

You'll notice when dealing with non-primitive values, you'll run into some limitations. The most obvious issue is that not every value can be represented as part of the `args` param in the URL, losing the ability to share and deeplink to such a state. Beyond that, complex values such as JSX cannot be synchronized between the manager (e.g. Controls addon) and the preview (your story).

One way to deal with this is to use primitive values (e.g. strings) as arg values, and using a story template to convert these values to their complex counterpart before rendering. This isn't the nicest way to do it (see below), but certainly the most flexible.

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

Expand All @@ -147,35 +151,20 @@ As they can be complex cases:

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

Use customized options:

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

<CodeSnippets
paths={[
'common/component-story-custom-args-complex-object.js.mdx',
]}
/>

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

Or even with certain types of elements, such as icons:
Unless you need the flexibility of a function, an easier way to map primitives to complex values before rendering is to define a `mapping`. Additionally, you can specify `control.labels` to configure custom labels for your checkbox, radio or select input.

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

<CodeSnippets
paths={[
'react/component-story-custom-args-icons.js.mdx',
'react/component-story-custom-args-icons.ts.mdx',
'react/component-story-custom-args-icons.mdx.mdx',
'vue/component-story-custom-args-icons.2.js.mdx',
'vue/component-story-custom-args-icons.3.js.mdx',
'angular/component-story-custom-args-icons.ts.mdx',
'common/component-story-custom-args-mapping.js.mdx',
]}
/>

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

Note that both `mapping` and `control.labels` don't have to be exhaustive. If the currently selected option is not listed, it will be used verbatim.

## Configuration

The Controls addon can be configured in two ways:
Expand All @@ -197,35 +186,16 @@ Here is the full list of available controls you can use:
| **number** | number | a numeric text box input | min, max, step |
| | range | a range slider input | min, max, step |
| **object** | object | json editor text input | - |
| **enum** | radio | radio buttons input | options |
| | inline-radio | inline radio buttons input | options |
| | check | multi-select checkbox input | options |
| | inline-check | multi-select inline checkbox input | options |
| | select | select dropdown input | options |
| | multi-select | multi-select dropdown input | options |
| **enum** | radio | radio buttons input | - |
| | inline-radio | inline radio buttons input | - |
| | check | multi-select checkbox input | - |
| | inline-check | multi-select inline checkbox input | - |
| | select | select dropdown input | - |
| | multi-select | multi-select dropdown input | - |
| **string** | text | simple text input | - |
| | color | color picker input that assumes strings are color values | - |
| | date | date picker input | - |

If you need to customize a control to use a enum data type in your story, for instance the `inline-radio` you can do it like so:

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

<CodeSnippets
paths={[
'common/widget-story-controls-enum.js.mdx',
'common/widget-story-controls-enum.mdx.mdx',
]}
/>

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

<div class="aside">
If you don't provide a specific one, it defaults to:
- a radio type for enums with 5 or less elements
- a select control type with more than 5 elements
</div>

If you need to customize a control for a number data type in your story, you can do it like so:

<!-- prettier-ignore-start -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,11 @@ export default {
// creates specific argTypes with options
argTypes: {
propertyA: {
control: {
type: 'select',
options: ['Item One', 'Item Two', 'Item Three']
},
options: ['Item One', 'Item Two', 'Item Three'],
control: { type: 'select' } // automatically inferred when 'options' is defined
},
propertyB: {
control: {
type: 'select',
options: ['Another Item One', 'Another Item Two', 'Another Item Three']
},
options: ['Another Item One', 'Another Item Two', 'Another Item Three']
},
},
} as Meta;
Expand Down

This file was deleted.

27 changes: 27 additions & 0 deletions docs/snippets/common/component-story-custom-args-mapping.js.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
```js
// YourComponent.stories.js

import startCase from 'lodash/startCase';

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

export default {
component: YourComponent,
title: 'My Story with Icons',
argTypes: {
icon: {
// 'options' is an array of serializable values
options: Object.keys(icons),
// 'mapping' maps serializable option values to complex arg values (which can be anything)
mapping: icons,
control: {
// type 'select' is automatically inferred when 'options' is defined
type: 'select',
// 'labels' maps option values to string labels
labels: Object.fromEntries(Object.keys(icons).map(key => [key, startCase(key)]))
}
},
},
};
```
16 changes: 0 additions & 16 deletions docs/snippets/common/widget-story-controls-enum.js.mdx

This file was deleted.

16 changes: 0 additions & 16 deletions docs/snippets/common/widget-story-controls-enum.mdx.mdx

This file was deleted.

13 changes: 4 additions & 9 deletions docs/snippets/react/component-story-custom-args-complex.js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,11 @@ export default {
//👇 Creates specific argTypes with options
argTypes: {
propertyA: {
control: {
type: 'select',
options: ['Item One', 'Item Two', 'Item Three'],
},
options: ['Item One', 'Item Two', 'Item Three'],
control: { type: 'select' } // automatically inferred when 'options' is defined
},
propertyB: {
control: {
type: 'select',
options: ['Another Item One', 'Another Item Two', 'Another Item Three'],
},
options: ['Another Item One', 'Another Item Two', 'Another Item Three'],
},
},
};
Expand All @@ -36,4 +31,4 @@ const Template = ({ propertyA, propertyB, ...rest }) => {

return <YourComponent someProperty={someFunctionResult} {...rest} />;
};
```
```
28 changes: 11 additions & 17 deletions docs/snippets/react/component-story-custom-args-complex.mdx.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,18 @@ export const Template = ({propertyA,propertyB,...rest})=>{
name="A complex case with a function"
argTypes={{
propertyA: {
control: {
type: 'select',
options: [
'Item One',
'Item Two',
'Item Three'
],
},
options: [
'Item One',
'Item Two',
'Item Three'
],
},
propertyB: {
control: {
type: 'select',
options: [
'Another Item One',
'Another Item Two',
'Another Item Three'
],
},
options: [
'Another Item One',
'Another Item Two',
'Another Item Three'
],
},
}}
args={{
Expand All @@ -50,4 +44,4 @@ export const Template = ({propertyA,propertyB,...rest})=>{
{Template.bind({})}
</Story>
</Canvas>
```
```
Loading