-
-
Notifications
You must be signed in to change notification settings - Fork 9.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10028 from storybookjs/feature/addon-toolbars
Addon-Toolbars: Global args support in the toolbar
- Loading branch information
Showing
18 changed files
with
449 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./dist/preset'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './dist/register'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | ||
})} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function managerEntries(entry: any[] = []) { | ||
return [...entry, require.resolve('../register')]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 />, | ||
}) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.