Skip to content

Commit

Permalink
Merge pull request #10028 from storybookjs/feature/addon-toolbars
Browse files Browse the repository at this point in the history
Addon-Toolbars: Global args support in the toolbar
  • Loading branch information
shilman committed Mar 13, 2020
2 parents fca0da0 + 7d75ee5 commit e0c42a9
Show file tree
Hide file tree
Showing 18 changed files with 449 additions and 1 deletion.
177 changes: 177 additions & 0 deletions addons/toolbars/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<center>
<img src="./docs/hero.gif" width="100%" />
</center>

<h1>Storybook Addon Toolbars</h1>

The Toolbars addon controls global story rendering options from Storybook's toolbar UI. It's a general purpose addon that can be used to:

- set a theme for your components
- set your components' internationalization (i18n) locale
- configure just about anything in Storybook that makes use of a global variable

Toolbars is implemented using Storybook Args (SB6.0+): dynamic variables that trigger a story re-render when they are set.

- [Get started](#get-started)
- [Installation](#installation)
- [Configure menu UI](#configure-menu-ui)
- [Create a decorator](#create-a-decorator)
- [Advanced usage](#advanced-usage)
- [Advanced menu configuration](#advanced-menu-configuration)
- [Consuming global args from within a story](#consuming-global-args-from-within-a-story)
- [FAQs](#faqs)
- [How does this compare to `addon-contexts`?](#how-does-this-compare-to-addon-contexts)

## Get started

To get started with `addon-toolbars`: (1) [install the addon](#installation), (2) [configure the menu UI](#configure-menu-ui), and (3) [Create a decorator to implement custom logic](#create-a-decorator).

### Installation

First, install the package:

```sh
npm install @storybook/addon-toolbars -D # or yarn
```

Then add it to your `.storybook/main.js` config:

```js
module.exports = {
addons: ['@storybook/addon-toolbars'],
};
```

### Configure menu UI

Addon-toolbars has a simple, declarative syntax for configuring toolbar menus. You can add toolbars by adding `globalArgTypes` with a `toolbar` annotation, in `.storybook/preview.js`:

```js
export const globalArgTypes = {
theme: {
name: 'Theme'
description: 'Global theme for components',
defaultValue: 'light',
toolbar: { icon: 'box', options: ['light','dark', 'medium'] },
}
}
```

You should see a dropdown in your toolbar with options `light`, `dark`, and `medium`.

### Create a decorator

Now, let's wire it up! We can consume our new `theme` global arg in a decorator using the `context.globalArgs.theme` value.

For example, suppose you are using`styled-components`. You can add a theme provider decorator to your `.storybook/preview.js` config:

```js
const styledComponentsThemeDecorator = (storyFn, { globalArgs: { theme } }) => (
<ThemeProvider {...getTheme(theme)}>{storyFn()}</ThemeProvider>
);

export const decorators = [styledComponentsThemeDecorator];
```

## Advanced usage

The previous section shows the common case. There are two advanced use cases: (1) [advanced menu configurations](#advanced-menu-configuration), (2) [consuming global args inside a story](#consuming-global-args-from-within-a-story).

### Advanced menu configuration

The default menu configuration is simple: everything's a string! However, the Toolbars addon also support configuration options to tweak the appearance of the menu:

```ts
type MenuItem {
/**
* The string value of the menu that gets set in the global args
*/
value: string,
/**
* The main text of the title
*/
title: string,
/**
* A string that gets shown in left side of the menu, if set
*/
left?: string,
/**
* A string that gets shown in right side of the menu, if set
*/
right?: string,
/**
* An icon that gets shown in the toolbar if this item is selected
*/
icon?: icon,
}
```

Thus if you want to show right-justified flags for an internationalization locale, you might set up the following configuration in `.storybook/preview.js`:

```js
export const globalArgTypes = {
locale: {
name: 'Locale',
description: 'Internationalization locale',
defaultValue: 'en',
toolbar: {
icon: 'globe',
items: [
{ value: 'en', right: '🇺🇸', title: 'English' },
{ value: 'fr', right: '🇫🇷', title: 'Français' },
{ value: 'es', right: '🇪🇸', title: 'Español' },
{ value: 'zh', right: '🇨🇳', title: '中文' },
{ value: 'kr', right: '🇰🇷', title: '한국어' },
],
}
},
},
};
```

### Consuming global args from within a story

The recommended usage, as shown in the examples above, is to consume global args from within a decorator and implement a global setting that applies to all stories. But sometimes it's useful to use toolbar options inside individual stories.

Storybook's `globalArgs` are available via the story context:

```js
const getCaptionForLocale = (locale) => {
switch(locale) {
case 'es': return 'Hola!';
case 'fr': return 'Bonjour!';
case 'kr': return '안녕하세요!';
case 'zh': return '你好!';
default:
return 'Hello!',
}
}

export const StoryWithLocale = ({ globalArgs: { locale } }) => {
const caption = getCaptionForLocale(locale);
return <>{caption}</>
};
```

**NOTE:** In Storybook 6.0, if you set the global option `passArgsFirst`, the story context is passes as the second argument:

```js
export const StoryWithLocale = (args, { globalArgs: { locale } }) => {
const caption = getCaptionForLocale(locale);
return <>{caption}</>;
};
```

## FAQs

### How does this compare to `addon-contexts`?

`Addon-toolbars` is the successor to `addon-contexts`, which provided convenient global toolbars in Storybook's toolbar.

The primary difference between the two packages is that `addon-toolbars` makes use of Storybook's new **Story Args** feature, which has the following advantages:

- **Standardization**. Args are built into Storybook in 6.x. Since `addon-toolbars` is based on args, you don't need to learn any addon-specific APIs to use it.

- **Ergonomics**. Global args are easy to consume [in stories](#consuming-global-args-from-within-a-story), in [Storybook Docs](https://github.com/storybookjs/storybook/tree/master/addons/docs), or even in other addons.

* **Framework compatibility**. Args are completely framework-independent, so `addon-toolbars` is compatible with React, Vue, Angular, etc. out of the box with no framework logic needed in the addon.
Binary file added addons/toolbars/docs/hero.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions addons/toolbars/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@storybook/addon-toolbars",
"version": "6.0.0-alpha.26",
"description": "Storybook Addon Controls",
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "addons/toolbars"
},
"license": "MIT",
"main": "dist/register.js",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-alpha.26",
"@storybook/api": "6.0.0-alpha.26",
"@storybook/client-api": "6.0.0-alpha.26",
"@storybook/components": "6.0.0-alpha.26"
},
"peerDependencies": {
"react": "*"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions addons/toolbars/preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/preset');
1 change: 1 addition & 0 deletions addons/toolbars/register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dist/register';
47 changes: 47 additions & 0 deletions addons/toolbars/src/components/MenuToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { FC } from 'react';
import { useGlobalArgs } from '@storybook/api';
import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components';
import { NormalizedToolbarArgType } from '../types';

export type MenuToolbarProps = NormalizedToolbarArgType & { id: string };

export const MenuToolbar: FC<MenuToolbarProps> = ({
id,
name,
description,
toolbar: { icon, items },
}) => {
const [globalArgs, updateGlobalArgs] = useGlobalArgs();
const selectedValue = globalArgs[id];
const active = selectedValue != null;
const selectedItem = active && items.find(item => item.value === selectedValue);

return (
<WithTooltip
placement="top"
trigger="click"
tooltip={({ onHide }) => {
const links = items.map(item => {
const { value, left, title, right } = item;
return {
id: value,
left,
title,
right,
active: selectedValue === value,
onClick: () => {
updateGlobalArgs({ [id]: value });
onHide();
},
};
});
return <TooltipLinkList links={links} />;
}}
closeOnClick
>
<IconButton key={name} active={active} title={description}>
<Icons icon={(selectedItem && selectedItem.icon) || icon} />
</IconButton>
</WithTooltip>
);
};
37 changes: 37 additions & 0 deletions addons/toolbars/src/components/ToolbarManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { FC } from 'react';
import { useGlobalArgTypes } from '@storybook/api';
import { Separator } from '@storybook/components';

import { ToolbarArgType } from '../types';
import { MenuToolbar } from './MenuToolbar';

const normalize = (key: string, argType: ToolbarArgType) => ({
...argType,
name: argType.name || key,
description: argType.description || key,
toolbar: {
...argType.toolbar,
items: argType.toolbar.items.map(item =>
typeof item === 'string' ? { value: item, title: item } : item
),
},
});

/**
* A smart component for handling manager-preview interactions.
*/
export const ToolbarManager: FC = () => {
const globalArgTypes = useGlobalArgTypes();
const keys = Object.keys(globalArgTypes).filter(key => !!globalArgTypes[key].toolbar);
if (!keys.length) return null;

return (
<>
<Separator />
{keys.map(key => {
const normalizedConfig = normalize(key, globalArgTypes[key] as ToolbarArgType);
return <MenuToolbar key={key} id={key} {...normalizedConfig} />;
})}
</>
);
};
2 changes: 2 additions & 0 deletions addons/toolbars/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ID = 'addon-toolbars' as const;
export const PARAM = 'toolbars' as const;
3 changes: 3 additions & 0 deletions addons/toolbars/src/preset/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function managerEntries(entry: any[] = []) {
return [...entry, require.resolve('../register')];
}
13 changes: 13 additions & 0 deletions addons/toolbars/src/register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import addons, { types } from '@storybook/addons';
import { ToolbarManager } from './components/ToolbarManager';
import { ID } from './constants';

addons.register(ID, api =>
addons.add(ID, {
title: ID,
type: types.TOOL,
match: ({ viewMode }) => viewMode === 'story',
render: () => <ToolbarManager />,
})
);
27 changes: 27 additions & 0 deletions addons/toolbars/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IconsProps } from '@storybook/components';
import { ArgType } from '@storybook/api';

export interface ToolbarItem {
value: string;
icon?: IconsProps['icon'];
left?: string;
right?: string;
title?: string;
}

export interface NormalizedToolbarConfig {
icon?: IconsProps['icon'];
items: ToolbarItem[];
}

export type NormalizedToolbarArgType = ArgType & {
toolbar: NormalizedToolbarConfig;
};

export type ToolbarConfig = NormalizedToolbarConfig & {
items: string[] | ToolbarItem[];
};

export type ToolbarArgType = ArgType & {
toolbar: ToolbarConfig;
};
9 changes: 9 additions & 0 deletions addons/toolbars/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env", "jest"]
},
"include": ["src/**/*"],
"exclude": ["src/**.test.ts"]
}
1 change: 1 addition & 0 deletions examples/official-storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
'@storybook/addon-viewport',
'@storybook/addon-graphql',
'@storybook/addon-contexts',
'@storybook/addon-toolbars',
'@storybook/addon-queryparams',
],
webpackFinal: async (config, { configType }) => ({
Expand Down
1 change: 1 addition & 0 deletions examples/official-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@storybook/addon-storyshots": "6.0.0-alpha.26",
"@storybook/addon-storyshots-puppeteer": "6.0.0-alpha.26",
"@storybook/addon-storysource": "6.0.0-alpha.26",
"@storybook/addon-toolbars": "6.0.0-alpha.26",
"@storybook/addon-viewport": "6.0.0-alpha.26",
"@storybook/addons": "6.0.0-alpha.26",
"@storybook/components": "6.0.0-alpha.26",
Expand Down
Loading

0 comments on commit e0c42a9

Please sign in to comment.