Merge pull request #10479 from storybookjs/remove/addon-context

Addon-contexts: Move to deprecated-addons repo
This commit is contained in:
Norbert de Langen 2020-04-23 18:21:46 +02:00 committed by GitHub
commit da223892b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 5 additions and 2234 deletions

View File

@ -5,7 +5,6 @@
| [a11y](addons/a11y) | + | | + | + | + | + | + | + | + | + | + | + |
| [actions](addons/actions) | + | +\* | + | + | + | + | + | + | + | + | + | + |
| [backgrounds](addons/backgrounds) | + | \* | + | + | + | + | + | + | + | + | + | + |
| [contexts](addons/contexts) | + | | + | | | | | | | | + | + |
| [cssresources](addons/cssresources) | + | | + | + | + | + | + | + | + | + | + | + |
| [design assets](addons/design-assets) | + | | + | + | + | + | + | + | + | + | + | + |
| [docs](addons/docs) | + | | + | + | + | + | + | + | + | + | + | + |

View File

@ -137,7 +137,6 @@ For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Sl
| [a11y](addons/a11y/) | Test components for user accessibility in Storybook |
| [actions](addons/actions/) | Log actions as users interact with components in the Storybook UI |
| [backgrounds](addons/backgrounds/) | Let users choose backgrounds in the Storybook UI |
| [contexts](addons/contexts/) | Interactively inject component contexts for stories in the Storybook UI |
| [cssresources](addons/cssresources/) | Dynamically add/remove css resources to the component iframe |
| [design assets](addons/design-assets/) | View images, videos, weblinks alongside your story |
| [docs](addons/docs/) | Add high quality documentation to your components |

View File

@ -1,254 +0,0 @@
# Storybook Addon Contexts
**Storybook Addon Contexts** is an addon for driving your components under dynamic contexts in
[Storybook](https://storybook.js.org/).
## 💡 Why you need this?
Real world users expects your application being customizable, that is why often your components are **polymorphic**:
they need to adapt themselves under different contextual environments. Imagine your components can speak
Chinese, English, or even French, and they change their skin tone under dark or light theme. Yeah, you want to make
sure a component looks great in all scenarios.
A good practice to write maintainable components is separate the presentation and its business logic. Storybook is
a great place for exercising the visualization and interaction of your components, which may depend on some contexts.
Often enough, you will find it become very tedious to wrap each component deeply with its contextual environments
before you can really write the main story. You even start to write extra components or factory functions just to
make your life easier. How about changing the context of your story dynamically?! There was simply no good way so
you ended up writing stories like an accountant.
That is why you need this. An elegant way to wrap your component stories and change their contextual environment
directly and dynamically in Storybook UI! Kind of like a dependency injection, eh! The best bit is **you define it
once then apply it everywhere**.
## ✅ Features
1. Define a single global file for managing contextual environments (a.k.a. containers) for all of your stories
declaratively. No more repetitive setups or noisy wrapping, making your stories more focused and readable.
2. Support dynamic contextual props switching from Storybook toolbar at runtime. You can slice into
different environments (e.g. languages or themes ) to understand how your component is going to respond.
3. Library agnostic: no presumption on what kind of components you want to wrap around your stories. You can even
use it to bridge with your favorite routing, state-management solutions, or even your own
[React Context](https://reactjs.org/docs/context.html) provider.
4. Offer chainable and granular configurations. It is even possible to fine-tune at per story level.
5. Visual regression friendly. You can use this addon to drive the same story under different contexts to smoke
test important visual states.
## 🧰 Requirements
Make sure the version of your Storybook is above v5. For the full list of the current supported frameworks, see
[Addon / Framework Support Table](../../ADDONS_SUPPORT.md).
## 🎬 Getting started
To get it started, add this package into your project:
```bash
yarn add -D @storybook/addon-contexts
```
within `.storybook/main.js`:
```js
module.exports = {
addons: ['@storybook/addon-contexts']
}
```
To load your contextual setups for your stories globally, add the following lines into `preview.js` file (you should
see it near your `addon.js` file):
```js
import { addDecorator } from '@storybook/[framework]';
import { withContexts } from '@storybook/addon-contexts/[framework]';
import { contexts } from './configs/contexts'; // we will define the contextual setups later in API section
addDecorator(withContexts(contexts));
```
Alternatively, like other addons, you can use this addon only for a given set of stories:
```js
import { withContexts } from '@storybook/addon-contexts/[framework]';
import { contexts } from './configs/contexts';
export default {
title: 'Component With Contexts',
decorators: [withContexts(contexts)],
};
```
Finally, you may want to create new contextual environments or disable default setups at the story level. To create a new contextual environment at the story level:
```js
export const defaultView = () => <div />; // sample story in CSF format
defaultView.story = {
parameters: {
contexts: [{ /* contextual environment defined using the API below */ }]
}
};
```
To disable a default setup at the story level:
```js
export const defaultView = () => <div />;
defaultView.story = {
parameters: {
contexts: [
{
title: '[title of contextual environment defined in contexts.js]'
options: { disable: true }
}
]
}
};
```
To override the default option for a default setup at the story level, see [this suggestion](https://discordapp.com/channels/486522875931656193/501692020226654208/687359410577604732).
## ⚙️ Setups
### Overview
It is recommended to have a separate file for managing your contextual environment setups. Let's add a file named
`contexts.js` first. Before diving into API details, here is an overview on the landscape. For example (in React),
to inject component theming contexts to both `styled-components` and `material-ui` theme providers in stories:
```js
export const contexts = [
{
icon: 'box', // a icon displayed in the Storybook toolbar to control contextual props
title: 'Themes', // an unique name of a contextual environment
components: [
// an array of components that is going to be injected to wrap stories
/* Styled-components ThemeProvider, */
/* Material-ui ThemeProvider, */
],
params: [
// an array of params contains a set of predefined `props` for `components`
{ name: 'Light Theme', props: { theme /* : your light theme */ } },
{ name: 'Dark Theme', props: { theme /* : your dark theme */ }, default: true },
],
options: {
deep: true, // pass the `props` deeply into all wrapping components
disable: false, // disable this contextual environment completely
cancelable: false, // allow this contextual environment to be opt-out optionally in toolbar
},
},
/* ... */ // multiple contexts setups are supported
];
```
---
### APIs
#### `withContexts(contexts) : function`
A decorating function for wrapping your stories under your predefined `contexts`. This means multiple contextual
environments are supported. They are going to be loaded layer by layer and wrapped in a descending oder (top -> down
-> story). The `contexts` is an array of objects that should have the following properties:
---
#### `icon : string?`
(default `undefined`)
An icon displayed in the Storybook toolbar to control contextual props. This addon allows you to define an icon for
each contextual environment individually. Take a look at the currently supported
[icon lists](https://storybooks-official.netlify.com/?path=/story/basics-icon--labels) from the official Storybook
story. You must define an icon first if you want to take advantage of switching props dynamically in your Storybook
toolbar.
---
#### `title : string`
(required)
A unique name of a contextual environment; if duplicate names are provided, the latter is going to be ignored.
---
#### `components : (Component|string)[]`
(required)
An array of components that is going to be injected to wrap stories. This means this addon allows multiple wrapping
components to coexist. The wrapping sequence is from the left to right (parent -> children -> story). This nested
wrapping behaviour can be useful in some cases; for instance, in the above example, we are wrapping stories under
`styled-components` and `material-ui` theme providers. Also, you can use this addon to wrap any valid HTML tags.
---
#### `params : object[] | undefined`
(default: `undefined`)
An array of params contains a set of predefined `props` for `components`. This object has the following properties:
#### `params.name : string`
(required)
A unique name for representing the props.
#### `params.props : object | null:`
(required)
The `props` that are accepted by the wrapping component(s).
#### `params.default : true?`
(default: `undefined`)
Set to `true` if you want to use this param initially. Only the first one marked as default is identified.
---
#### `options`
A set of options offers more granular control over the defined contextual environment. These properties can be
overridden at the story level:
#### `options.deep : boolean?`
(default: `false`)
Pass the `props` deeply into all wrapping components. Useful when you want them all to be passed with the same props.
#### `options.disable : boolean?`
(default: `false`)
Disable this contextual environment completely. Useful when you want to opt-out this context from a given story.
#### `options.cancelable : boolean?`
(default: `false`)
Allow this contextual environment to be opt-out optionally in toolbar. When set to `true`, an **Off** option will
be shown at first in the toolbar menu in your Storybook.
## 📔 Notes
1. You can use this addon to inject any valid components, that is why `icon` and `params` can be optional.
2. As mentioned, extra contextual environment setups can be added at the story level. Please make sure they are
passed via the second argument as `{ contexts: [{ /* extra contexts */ }}`.
3. Additional `params` can be "appended" into an existing setup at the story level too (make sure it goes with the
correct `title`); however, they are never be able to overridden the default setups. So it is important to have
non-colliding names.
4. The addon will persist the selected params (the addon state) between stories at run-time (similar to other
addons). If the active params were gone after story switching, it falls back to the default then the first. As a
rule of thumb, whenever collisions are possible, the first always wins.
5. Query parameters are supported for pre-selecting contexts param, which comes in handy for visual regression testing.
You can do this by appending `&contexts=[name of contexts]=[name of param]` in the URL under iframe mode. Use `,`
to separate multiple contexts (e.g. `&contexts=Theme=Forests,Language=Fr`).
## 📖 License
MIT

View File

@ -1,74 +0,0 @@
{
"name": "@storybook/addon-contexts",
"version": "6.0.0-alpha.42",
"description": "Storybook Addon Contexts",
"keywords": [
"preact",
"react",
"storybook",
"vue"
],
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "addons/contexts"
},
"license": "MIT",
"author": "Leo Y. Li",
"main": "dist/register.js",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts",
"ts3.5/**/*"
],
"scripts": {
"dev:check-types": "tsc --noEmit",
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-alpha.42",
"@storybook/api": "6.0.0-alpha.42",
"@storybook/components": "6.0.0-alpha.42",
"@storybook/core-events": "6.0.0-alpha.42",
"core-js": "^3.0.1",
"global": "^4.3.2",
"qs": "^6.6.0",
"regenerator-runtime": "^0.13.3"
},
"devDependencies": {
"@types/enzyme": "^3.10.5",
"enzyme": "^3.11.0"
},
"peerDependencies": {
"preact": "*",
"qs": "*",
"rax": "*",
"react": "*",
"react-dom": "*",
"vue": "*"
},
"peerDependenciesMeta": {
"preact": {
"optional": true
},
"rax": {
"optional": true
},
"vue": {
"optional": true
}
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"typesVersions": {
"<=3.5": {
"*": [
"ts3.5/*"
]
}
}
}

View File

@ -1,4 +0,0 @@
import { withContexts } from './dist/preview/frameworks/preact';
export { withContexts };
export default withContexts;

View File

@ -1,4 +0,0 @@
import { withContexts } from './dist/preview/frameworks/rax';
export { withContexts };
export default withContexts;

View File

@ -1,4 +0,0 @@
import { withContexts } from './dist/preview/frameworks/react';
export { withContexts };
export default withContexts;

View File

@ -1 +0,0 @@
export * from './dist/register';

View File

@ -1,37 +0,0 @@
import { makeDecorator, StoryWrapper } from '@storybook/addons';
import { ContextsPreviewAPI } from './preview/ContextsPreviewAPI';
import { ID, PARAM } from './shared/constants';
import { AddonSetting, AnyFunctionReturns, ContextNode, PropsMap } from './shared/types.d';
/**
* This file serves a idiomatic facade of a Storybook decorator.
*
* Wrapper function get called whenever the Storybook rerender the view. This reflow logic is
* framework agnostic; on the other hand, the framework specific bindings are the implementation
* details hidden behind the passed `render` function.
*
* Here, we need a dedicated singleton as a state manager for preview (the addon API, in vanilla)
* who is also knowing how to communicate with the Storybook manager (in React) via the Storybook
* event system.
*
* @param {Render} render - framework specific bindings
*/
export type Render<T> = (...args: [ContextNode[], PropsMap, AnyFunctionReturns<T>]) => T;
type CreateAddonDecorator = <T>(render: Render<T>) => (contexts: AddonSetting[]) => unknown;
export const createAddonDecorator: CreateAddonDecorator = (render) => {
const wrapper: StoryWrapper = (getStory, context, settings: any) => {
const { getContextNodes, getSelectionState, getPropsMap } = ContextsPreviewAPI();
const nodes = getContextNodes(settings);
const state = getSelectionState();
const props = getPropsMap(nodes, state);
return render(nodes, props, () => getStory(context));
};
return makeDecorator({
name: ID,
parameterName: PARAM,
skipIfNoParametersOrOptions: true,
wrapper,
});
};

View File

@ -1,34 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useChannel } from '@storybook/api';
import { ToolBar } from './components/ToolBar';
import { deserialize, serialize } from '../shared/serializers';
import { PARAM, REBOOT_MANAGER, UPDATE_MANAGER, UPDATE_PREVIEW } from '../shared/constants';
import { FCNoChildren, ManagerAPI } from '../shared/types.d';
/**
* A smart component for handling manager-preview interactions.
*/
type ContextsManager = FCNoChildren<{
api: ManagerAPI;
}>;
export const ContextsManager: ContextsManager = ({ api }) => {
const [nodes, setNodes] = useState([]);
const [state, setState] = useState(deserialize(api.getQueryParam(PARAM)));
const setSelected = useCallback(
(nodeId, name) => setState((obj) => ({ ...obj, [nodeId]: name })),
[]
);
// from preview
const emit = useChannel({
[UPDATE_MANAGER]: (newNodes) => setNodes(newNodes || []),
});
// to preview
useEffect(() => emit(REBOOT_MANAGER), []);
useEffect(() => emit(UPDATE_PREVIEW, state), [state]);
useEffect(() => api.setQueryParams({ [PARAM]: serialize(state) }), [state]);
return <ToolBar nodes={nodes} state={state || {}} setSelected={setSelected} />;
};

View File

@ -1,103 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { ToolBar } from './ToolBar';
describe('Tests on addon-contexts component: ToolBar', () => {
it('should render nothing if receive an empty contextNodes', () => {
// when
const result = shallow(<ToolBar nodes={[]} state={{}} setSelected={jest.fn} />);
// then
expect(result).toMatchInlineSnapshot(`""`);
});
it('should spawn ToolBarControl based on the given contextNodes', () => {
// given
const someContextNodes = [
{
components: ['span'],
icon: 'box' as const,
nodeId: 'Some Context A',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: '', props: {} }],
title: 'Some Context A',
},
{
components: ['div'],
icon: 'box' as const,
nodeId: 'Some Context B',
options: { cancelable: true, deep: false, disable: false },
params: [
{ name: 'Some Param X', props: {} },
{ name: 'Some Param Y', props: {} },
],
title: 'Some Context B',
},
];
const someSelectionState = {
'Some Context B': 'Some Param Y',
};
// when
const result = shallow(
<ToolBar nodes={someContextNodes} state={someSelectionState} setSelected={jest.fn} />
);
// then
expect(result).toMatchInlineSnapshot(`
<Fragment>
<Separator />
<ToolBarControl
icon="box"
key="Some Context A"
nodeId="Some Context A"
options={
Object {
"cancelable": false,
"deep": false,
"disable": false,
}
}
params={
Array [
Object {
"name": "",
"props": Object {},
},
]
}
selected=""
setSelected={[Function]}
title="Some Context A"
/>
<ToolBarControl
icon="box"
key="Some Context B"
nodeId="Some Context B"
options={
Object {
"cancelable": true,
"deep": false,
"disable": false,
}
}
params={
Array [
Object {
"name": "Some Param X",
"props": Object {},
},
Object {
"name": "Some Param Y",
"props": Object {},
},
]
}
selected="Some Param Y"
setSelected={[Function]}
title="Some Context B"
/>
</Fragment>
`);
});
});

View File

@ -1,26 +0,0 @@
import React, { ComponentProps, memo } from 'react';
import { Separator } from '@storybook/components';
import { ToolBarControl } from './ToolBarControl';
import { ContextNode, FCNoChildren, SelectionState } from '../../shared/types.d';
type ToolBar = FCNoChildren<{
nodes: ContextNode[];
state: SelectionState;
setSelected: ComponentProps<typeof ToolBarControl>['setSelected'];
}>;
export const ToolBar: ToolBar = memo(({ nodes, state, setSelected }) =>
nodes.length ? (
<>
<Separator />
{nodes.map(({ components, ...forwardProps }) => (
<ToolBarControl
{...forwardProps}
setSelected={setSelected}
selected={state[forwardProps.nodeId] || ''}
key={forwardProps.nodeId}
/>
))}
</>
) : null
);

View File

@ -1,102 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { ToolBarControl } from './ToolBarControl';
import { OPT_OUT } from '../../shared/constants';
describe('Tests on addon-contexts component: ToolBarControl', () => {
// given
const someBasicProps = {
icon: 'box' as const,
nodeId: 'Some Context',
options: { cancelable: true, deep: false, disable: false },
params: [
{ name: 'A', props: {} },
{ name: 'B', props: {} },
],
title: 'Some Context',
selected: '',
setSelected: jest.fn,
};
it('should control menu: set as inactive if being out-out (if cancelable)', () => {
// when
const result = shallow(<ToolBarControl {...someBasicProps} selected={OPT_OUT} />);
// then
expect(result.props().active).toBe(false);
});
it('should control menu: valid "selected" to give "activeName"', () => {
// given
const selected = 'C';
const anotherSelected = 'B';
// when
const result = shallow(<ToolBarControl {...someBasicProps} selected={selected} />);
const anotherResult = shallow(
<ToolBarControl {...someBasicProps} selected={anotherSelected} />
);
// then
expect(result.props().optionsProps.activeName).not.toBe(selected);
expect(anotherResult.props().optionsProps.activeName).toBe(anotherSelected);
});
it('should control menu: fallback "activeName" to the default param', () => {
// given
const name = 'C';
const params = [...someBasicProps.params, { name, props: {}, default: true }];
// when
const result = shallow(<ToolBarControl {...someBasicProps} params={params} />);
// then
expect(result.props().optionsProps.activeName).toBe(name);
});
it('should control menu: fallback "activeName" to the first (if default not found)', () => {
// when
const result = shallow(<ToolBarControl {...someBasicProps} />);
// then
expect(result.props().optionsProps.activeName).toBe(someBasicProps.params[0].name);
});
it('should render nothing if being disabled', () => {
// given
const options = { ...someBasicProps.options, disable: true };
// when
const result = shallow(<ToolBarControl {...someBasicProps} options={options} />);
// then
expect(result).toMatchInlineSnapshot(`""`);
});
it('should document the shallowly rendered result', () => {
// when
const result = shallow(<ToolBarControl {...someBasicProps} />);
// then
expect(result).toMatchInlineSnapshot(`
<ToolBarMenu
active={true}
expanded={false}
icon="box"
optionsProps={
Object {
"activeName": "A",
"list": Array [
"__OPT_OUT__",
"A",
"B",
],
"onSelectOption": [Function],
}
}
setExpanded={[Function]}
title="Some Context"
/>
`);
});
});

View File

@ -1,53 +0,0 @@
import React, { useState } from 'react';
import { ToolBarMenu } from './ToolBarMenu';
import { OPT_OUT } from '../../shared/constants';
import { ContextNode, FCNoChildren } from '../../shared/types.d';
type ToolBarControl = FCNoChildren<
Omit<
ContextNode & {
selected: string;
setSelected: (nodeId: string, name: string) => void;
},
'components'
>
>;
export const ToolBarControl: ToolBarControl = ({
nodeId,
icon,
title,
params,
options,
selected,
setSelected,
}) => {
const [expanded, setExpanded] = useState(false);
const paramNames = params.map(({ name }) => name);
const activeName =
// validate the integrity of the selected name
([...paramNames, options.cancelable && OPT_OUT].includes(selected) && selected) ||
// fallback to default
(params.find((param) => !!param.default) || { name: null }).name ||
// fallback to the first
params[0].name;
const list = options.cancelable ? [OPT_OUT, ...paramNames] : paramNames;
const props = {
title,
active: activeName !== OPT_OUT,
expanded,
setExpanded,
optionsProps: {
activeName,
list,
onSelectOption: (name: string) => () => {
setExpanded(false);
setSelected(nodeId, name);
},
},
};
return Array.isArray(list) && list.length && !options.disable ? (
<ToolBarMenu icon={icon} {...props} />
) : null;
};

View File

@ -1,109 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { ToolBarMenu } from './ToolBarMenu';
describe('Tests on addon-contexts component: ToolBarMenu', () => {
it('should glue `@storybook/ui` components to produce a context menu', () => {
// given
const someProps = {
icon: 'globe' as const,
title: 'Some Context',
active: true,
expanded: false,
setExpanded: jest.fn,
optionsProps: {
activeName: 'A',
list: ['A', 'B'],
onSelectOption: jest.fn,
},
};
// when
const result = shallow(<ToolBarMenu {...someProps} />);
// then
expect(result).toMatchInlineSnapshot(`
<WithTooltipPure
closeOnClick={true}
hasChrome={true}
modifiers={Object {}}
onVisibilityChange={[Function]}
placement="top"
svg={false}
tooltip={
<ToolBarMenuOptions
activeName="A"
list={
Array [
"A",
"B",
]
}
onSelectOption={[Function]}
/>
}
tooltipShown={false}
trigger="click"
>
<IconButton
active={true}
title="Some Context"
>
<Icons
icon="globe"
/>
</IconButton>
</WithTooltipPure>
`);
});
it('should render TabButton with title if the icon is given', () => {
// given
const someProps = {
title: 'Some Context',
active: true,
expanded: false,
setExpanded: jest.fn,
optionsProps: {
activeName: 'A',
list: ['A', 'B'],
onSelectOption: jest.fn,
},
};
// when
const result = shallow(<ToolBarMenu {...someProps} />);
// then
expect(result).toMatchInlineSnapshot(`
<WithTooltipPure
closeOnClick={true}
hasChrome={true}
modifiers={Object {}}
onVisibilityChange={[Function]}
placement="top"
svg={false}
tooltip={
<ToolBarMenuOptions
activeName="A"
list={
Array [
"A",
"B",
]
}
onSelectOption={[Function]}
/>
}
tooltipShown={false}
trigger="click"
>
<TabButton
active={true}
>
Some Context
</TabButton>
</WithTooltipPure>
`);
});
});

View File

@ -1,39 +0,0 @@
import React, { ComponentProps } from 'react';
import { Icons, IconButton, WithTooltipPure, TabButton } from '@storybook/components';
import { ToolBarMenuOptions } from './ToolBarMenuOptions';
import { ContextNode, FCNoChildren } from '../../shared/types.d';
type ToolBarMenu = FCNoChildren<{
icon?: ComponentProps<typeof Icons>['icon'] | '' | void;
title: ContextNode['title'];
active: boolean;
expanded: boolean;
setExpanded: (state: boolean) => void;
optionsProps: ComponentProps<typeof ToolBarMenuOptions>;
}>;
export const ToolBarMenu: ToolBarMenu = ({
icon,
title,
active,
expanded,
setExpanded,
optionsProps,
}) => (
<WithTooltipPure
closeOnClick
trigger="click"
placement="top"
tooltipShown={expanded}
onVisibilityChange={setExpanded}
tooltip={<ToolBarMenuOptions {...optionsProps} />}
>
{icon ? (
<IconButton active={active} title={title}>
<Icons icon={icon} />
</IconButton>
) : (
<TabButton active={active}>{title}</TabButton>
)}
</WithTooltipPure>
);

View File

@ -1,51 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { ToolBarMenuOptions } from './ToolBarMenuOptions';
import { OPT_OUT } from '../../shared/constants';
describe('Tests on addon-contexts component: ToolBarMenuOptions', () => {
it('should glue TooltipLinkList and set the active item correspondingly', () => {
// given
const list = [OPT_OUT, 'A', 'B'];
const activeName = 'B';
// when
const result = shallow(
<ToolBarMenuOptions activeName={activeName} list={list} onSelectOption={jest.fn} />
);
// then
expect(result.props().links.length).toBe(list.length);
expect(result.props().links.find((link: any) => link.title === activeName).active).toBe(true);
expect(result).toMatchInlineSnapshot(`
<TooltipLinkList
LinkWrapper={null}
links={
Array [
Object {
"active": false,
"id": "__OPT_OUT__",
"key": "__OPT_OUT__",
"onClick": [MockFunction],
"title": "Off",
},
Object {
"active": false,
"id": "A",
"key": "A",
"onClick": [MockFunction],
"title": "A",
},
Object {
"active": true,
"id": "B",
"key": "B",
"onClick": [MockFunction],
"title": "B",
},
]
}
/>
`);
});
});

View File

@ -1,22 +0,0 @@
import * as React from 'react';
import { TooltipLinkList } from '@storybook/components';
import { OPT_OUT } from '../../shared/constants';
import { FCNoChildren } from '../../shared/types.d';
type ToolBarMenuOptions = FCNoChildren<{
activeName: string;
list: string[];
onSelectOption: (name: string) => () => void;
}>;
export const ToolBarMenuOptions: ToolBarMenuOptions = ({ activeName, list, onSelectOption }) => (
<TooltipLinkList
links={list.map((name) => ({
key: name,
id: name,
title: name !== OPT_OUT ? name : 'Off',
active: name === activeName,
onClick: onSelectOption(name),
}))}
/>
);

View File

@ -1,81 +0,0 @@
import addons from '@storybook/addons';
import { window } from 'global';
import { parse } from 'qs';
import { getContextNodes, getPropsMap, getRendererFrom, singleton } from './libs';
import { deserialize } from '../shared/serializers';
import {
PARAM,
REBOOT_MANAGER,
UPDATE_PREVIEW,
UPDATE_MANAGER,
FORCE_RE_RENDER,
SET_CURRENT_STORY,
} from '../shared/constants';
import { ContextNode, PropsMap, SelectionState } from '../shared/types.d';
/**
* A singleton for handling preview-manager and one-time-only side-effects.
*/
export const ContextsPreviewAPI = singleton(() => {
const channel = addons.getChannel();
let contextsNodesMemo: ContextNode[] | null = null;
let selectionState: SelectionState = {};
/**
* URL query param can be used to predetermine the contexts a story should render,
* which is useful for performing image snapshot testing or URL sharing.
*/
if (window && window.location) {
selectionState = deserialize(parse(window.location.search)[PARAM]) || {};
}
/**
* (Vue specific)
* Vue will inject getter/setter watchers on the first rendering of the addon,
* which is why we have to keep an internal reference and use `Object.assign` to notify the watcher.
*/
const reactivePropsMap = {};
const updateReactiveSystem = (propsMap: PropsMap) => Object.assign(reactivePropsMap, propsMap);
/**
* Preview-manager communications.
*/
// from manager
channel.on(UPDATE_PREVIEW, (state) => {
if (state) {
selectionState = state;
channel.emit(FORCE_RE_RENDER);
}
});
channel.on(REBOOT_MANAGER, () => {
channel.emit(UPDATE_MANAGER, contextsNodesMemo);
});
channel.on(SET_CURRENT_STORY, () => {
// trash the memorization since the story-level setting may change (diffing it is much expensive)
contextsNodesMemo = null;
});
// to manager
const getContextNodesWithSideEffects: typeof getContextNodes = (...arg) => {
if (contextsNodesMemo === null) {
contextsNodesMemo = getContextNodes(...arg);
channel.emit(UPDATE_MANAGER, contextsNodesMemo);
}
return contextsNodesMemo;
};
/**
* @Public
* Exposed interfaces
*/
return {
// methods get called on Storybook event lifecycle
getContextNodes: getContextNodesWithSideEffects,
getSelectionState: () => selectionState,
getPropsMap,
// methods for processing framework specific bindings
getRendererFrom,
updateReactiveSystem,
};
});

View File

@ -1,14 +0,0 @@
import { h, VNode } from 'preact';
import { createAddonDecorator, Render } from '../../index';
import { ContextsPreviewAPI } from '../ContextsPreviewAPI';
/**
* This is the framework specific bindings for Preact.
* '@storybook/preact' expects the returning object from a decorator to be a 'Preact vNode'.
*/
export const renderPreact: Render<VNode> = (contextNodes, propsMap, getStoryVNode) => {
const { getRendererFrom } = ContextsPreviewAPI();
return getRendererFrom(h)(contextNodes, propsMap, getStoryVNode);
};
export const withContexts = createAddonDecorator(renderPreact);

View File

@ -1,14 +0,0 @@
import { createElement, RaxElement } from 'rax';
import { createAddonDecorator, Render } from '../../index';
import { ContextsPreviewAPI } from '../ContextsPreviewAPI';
/**
* This is the framework specific bindings for Rax.
* '@storybook/rax' expects the returning object from a decorator to be a 'Rax Element' (vNode).
*/
export const renderRax: Render<RaxElement<any>> = (contextNodes, propsMap, getStoryVNode) => {
const { getRendererFrom } = ContextsPreviewAPI();
return getRendererFrom(createElement)(contextNodes, propsMap, getStoryVNode);
};
export const withContexts = createAddonDecorator(renderRax);

View File

@ -1,14 +0,0 @@
import { createElement, ReactElement } from 'react';
import { createAddonDecorator, Render } from '../../index';
import { ContextsPreviewAPI } from '../ContextsPreviewAPI';
/**
* This is the framework specific bindings for React.
* '@storybook/react' expects the returning object from a decorator to be a 'React Element' (vNode).
*/
export const renderReact: Render<ReactElement> = (contextNodes, propsMap, getStoryVNode) => {
const { getRendererFrom } = ContextsPreviewAPI();
return getRendererFrom(createElement)(contextNodes, propsMap, getStoryVNode);
};
export const withContexts = createAddonDecorator(renderReact);

View File

@ -1,28 +0,0 @@
import Vue from 'vue';
import { createAddonDecorator, Render } from '../../index';
import { ContextsPreviewAPI } from '../ContextsPreviewAPI';
import { ID } from '../../shared/constants';
/**
* This is the framework specific bindings for Vue.
* '@storybook/vue' expects the returning object from a decorator to be a 'VueComponent'.
*/
export const renderVue: Render<Vue.Component> = (contextNodes, propsMap, getStoryComponent) => {
const { getRendererFrom, updateReactiveSystem } = ContextsPreviewAPI();
const reactiveProps = updateReactiveSystem(propsMap);
return Vue.extend({
name: ID,
data: () => reactiveProps, // deepscan-disable-line
render: (createElement) =>
getRendererFrom((Component, props, children) => {
const { key, ref, style, classNames, ...rest } = props || Object();
const contextData =
Component instanceof Object
? { key, ref, style, class: classNames, props: rest } // component as a Vue object
: { key, ref, style, class: classNames, attrs: rest }; // component as a HTML tag string
return createElement(Component, contextData, [children]);
})(contextNodes, reactiveProps, () => createElement(getStoryComponent())),
});
};
export const withContexts = createAddonDecorator(renderVue);

View File

@ -1,59 +0,0 @@
import { memorize, singleton } from './decorators';
describe('Test on functional helpers: memorize', () => {
it('should memorize the calculated result', () => {
// given
const someFn = jest.fn((x) => [x]);
const someFnMemo = memorize(someFn);
// when
const resultA = someFnMemo(1);
const resultB = someFnMemo(2);
const resultC = someFnMemo(1);
// then
expect(someFn).toHaveBeenCalledTimes(2);
expect(resultA).toEqual(someFn(1));
expect(resultA).not.toEqual(resultB);
expect(resultA).toBe(resultC);
expect(resultB).not.toEqual(resultC);
});
it('should memorize based on the second argument', () => {
// given
const someFn = jest.fn((x, y) => [x, y]);
const someFnMemo = memorize(someFn, (x, y) => y);
// when
const resultA = someFnMemo(1, 2);
const resultB = someFnMemo(2, 2);
const resultC = someFnMemo(1, 3);
// then
expect(someFn).toHaveBeenCalledTimes(2);
expect(resultA).toEqual(someFn(1, 2));
expect(resultA).toBe(resultB);
expect(resultA).not.toEqual(resultC);
expect(resultB).not.toEqual(resultC);
});
});
describe('Test on functional helpers: singleton', () => {
it('should make a function singleton', () => {
// given
const someFn = jest.fn((x, y, z) => [x, y, z]);
const someFnSingleton = singleton(someFn);
// when
const resultA = someFnSingleton(1, 2, 3);
const resultB = someFnSingleton(4, 5, 6);
const resultC = someFnSingleton(7, 8, 9);
// then
expect(someFn).toHaveBeenCalledTimes(1);
expect(resultA).toEqual(someFn(1, 2, 3));
expect(resultA).toBe(resultB);
expect(resultA).toBe(resultC);
expect(resultB).toBe(resultC);
});
});

View File

@ -1,26 +0,0 @@
/**
* Memorize the calculated result of a function by an ES6 Map;
* the default is to memorize its the first argument;
* @return the memorized version of a function.
*/
type memorize = <T, U extends any[]>(
fn: (...args: U) => T,
resolver?: (...args: U) => unknown
) => (...args: U) => T;
export const memorize: memorize = (fn, resolver) => {
const memo = new Map();
return (...arg) => {
const key = resolver ? resolver(...arg) : arg[0];
return memo.get(key) || memo.set(key, fn(...arg)).get(key);
};
};
/**
* Enforce a given function can only be executed once;
* the returned value is cached for resolving the subsequent calls.
* @return the singleton version of a function.
*/
type singleton = <T, U extends any[]>(fn: (...args: U) => T) => (...args: U) => T;
export const singleton: singleton = (fn) => memorize(fn, () => 'singleton');

View File

@ -1,139 +0,0 @@
import { _getMergedSettings, getContextNodes } from './getContextNodes';
describe('Test on the merging result of a pair of settings', () => {
it('should retain the basic structure even receiving empty objects', () => {
// when
const result = _getMergedSettings({}, {});
// then
expect(result).toEqual({
components: [],
icon: '',
nodeId: '',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: '', props: {} }],
title: '',
});
});
it('should correctly merge two settings', () => {
// given
const someTopLevelSettings = {
icon: 'box' as const,
title: 'Some Context',
components: ['div'],
params: [
{ name: 'T1', props: {} },
{ name: 'T2', props: {} },
],
options: {
cancelable: true,
disable: true,
},
};
const someStoryLevelSettings = {
icon: 'box' as const,
title: 'Some Context',
components: ['span'],
params: [
{ name: 'S1', props: {} },
{ name: 'S2', props: {} },
],
options: {
deep: true,
disable: false,
},
};
// when
const result = _getMergedSettings(someTopLevelSettings, someStoryLevelSettings);
// then
expect(result).toEqual({
// topLevel over storyLevel
nodeId: someTopLevelSettings.title,
icon: someTopLevelSettings.icon,
title: someTopLevelSettings.title,
components: someTopLevelSettings.components,
// storyLevel appends to topLevel
params: [...someTopLevelSettings.params, ...someStoryLevelSettings.params],
// storyLevel over topLevel
options: {
cancelable: someTopLevelSettings.options.cancelable,
deep: someStoryLevelSettings.options.deep,
disable: someStoryLevelSettings.options.disable,
},
});
});
});
describe('Test on reconciliation of settings', () => {
it('should have a stable array ordering after normalization', () => {
// when
const result = getContextNodes({
// from the topLevel
options: [
{
icon: 'box',
title: 'Some Context',
components: ['div'],
params: [{ name: 'T1', props: {} }],
},
{
icon: 'box',
title: 'Another Context',
components: ['div'],
params: [{ name: 'T2', props: {} }],
},
],
// from the storyLevel
parameters: [
{
icon: 'box',
title: 'Other Contexts',
components: ['span'],
params: [{ name: 'S1', props: {} }],
},
{
icon: 'box',
title: 'Some Context',
components: ['p'],
params: [{ name: 'S2', props: {}, default: true }],
},
],
});
// then
expect(result).toEqual([
{
components: ['div'],
icon: 'box',
nodeId: 'Some Context',
options: { cancelable: false, deep: false, disable: false },
params: [
{ name: 'T1', props: {} },
{ name: 'S2', props: {}, default: true },
],
title: 'Some Context',
},
{
components: ['div'],
icon: 'box',
nodeId: 'Another Context',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'T2', props: {} }],
title: 'Another Context',
},
{
components: ['span'],
icon: 'box',
nodeId: 'Other Contexts',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'S1', props: {} }],
title: 'Other Contexts',
},
]);
});
});

View File

@ -1,53 +0,0 @@
/* eslint-disable no-underscore-dangle */
import { AddonSetting, ContextNode, WrapperSettings } from '../../shared/types.d';
/**
* @private
* Merge the top-level (global options) and the story-level (parameters) from a pair of setting;
* @return the normalized definition for a contextual environment (i.e. a contextNode).
*/
type _getMergedSettings = (
topLevel: Partial<AddonSetting>,
storyLevel: Partial<AddonSetting>
) => ContextNode;
export const _getMergedSettings: _getMergedSettings = (topLevel, storyLevel) => ({
// strip out special characters reserved for serializing
nodeId: (topLevel.title || storyLevel.title || '').replace(/[,+]/g, ''),
icon: topLevel.icon || storyLevel.icon || '',
title: topLevel.title || storyLevel.title || '',
components: topLevel.components || storyLevel.components || [],
params:
topLevel.params || storyLevel.params
? [...(topLevel.params || []), ...(storyLevel.params || [])].filter(Boolean)
: [{ name: '', props: {} }],
options: {
deep: false,
disable: false,
cancelable: false,
...topLevel.options,
...storyLevel.options,
},
});
/**
* @nosideeffects
* Pair up settings for merging normalizations to produce the contextual definitions (i.e. contextNodes);
* it guarantee the adding order can be respected but not duplicated.
*/
type getContextNodes = (settings: WrapperSettings) => ContextNode[];
export const getContextNodes: getContextNodes = ({ options, parameters }) => {
const titles = [...(options || []), ...(parameters || [])]
.filter(Boolean)
.map(({ title }) => title);
return Array.from(new Set(titles))
.filter(Boolean)
.map((title) =>
_getMergedSettings(
(options && options.find((option) => option.title === title)) || {},
(parameters && parameters.find((param) => param.title === title)) || {}
)
);
};

View File

@ -1,88 +0,0 @@
import { _getPropsByParamName, getPropsMap } from './getPropsMap';
import { OPT_OUT } from '../../shared/constants';
describe('Test on behaviors from collecting the propsMap', () => {
const someParams = [
{ name: 'A', props: {} },
{ name: 'B', props: {} },
];
it('should return "null" when params in 0 length', () => {
const result = _getPropsByParamName([]);
expect(result).toBe(null);
});
it('should return "OPT_OUT" token when the context being opted out', () => {
const result = _getPropsByParamName(someParams, OPT_OUT, { cancelable: true });
expect(result).toBe(OPT_OUT);
});
it('should return the props from params when the name existed', () => {
const target = {};
const result = _getPropsByParamName([...someParams, { name: 'C', props: target }], 'C');
expect(result).toBe(target);
});
it('should otherwise fallback to default props in params for a bad name', () => {
const target = {};
const result = _getPropsByParamName(
[...someParams, { name: 'C', props: target, default: true }],
'X'
);
expect(result).toBe(target);
});
it('should otherwise fallback to the first props in params for a bad name, if no marked default props', () => {
const result = _getPropsByParamName(someParams, 'A');
expect(result).toBe(someParams[0].props);
});
});
describe('Test on the integrity of the method to get the propMaps', () => {
it('should return the correct propsMap from the specified selectionState', () => {
// given
const someContextNodes = [
{
components: ['div'],
icon: 'box' as const,
nodeId: 'Some Context',
options: { cancelable: false, deep: false, disable: false },
params: [
{ name: 'A1', props: { a: 1 } },
{ name: 'A2', props: { a: 2 }, default: true },
],
title: 'Some Context',
},
{
components: ['div'],
icon: 'box' as const,
nodeId: 'Another Context',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'B', props: { b: 1 } }],
title: 'Another Context',
},
{
components: ['span'],
icon: 'box' as const,
nodeId: 'Other Contexts',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'C', props: { c: 1 } }],
title: 'Other Contexts',
},
];
const someSelectionState = {
'Some Context': 'A1',
'Another Context': OPT_OUT, // an inconsistent but possible state being introduced via query param
};
// when
const result = getPropsMap(someContextNodes, someSelectionState);
// then
expect(result).toEqual({
'Some Context': { a: 1 },
'Another Context': { b: 1 }, // not equal to `OPT_OUT` due to the context is not cancelable
'Other Contexts': { c: 1 },
});
});
});

View File

@ -1,41 +0,0 @@
/* eslint-disable no-underscore-dangle */
import { OPT_OUT } from '../../shared/constants';
import { ContextNode, GenericProp, PropsMap, SelectionState } from '../../shared/types.d';
/**
* @private
* Extract the activated props by name from a given contextual params.
*/
type _getPropsByParamName = (
params: ContextNode['params'],
name?: string,
options?: Partial<ContextNode['options']>
) => GenericProp | typeof OPT_OUT;
export const _getPropsByParamName: _getPropsByParamName = (params, name = '', options = {}) => {
const { props = null } =
// when opt-out context
(options.cancelable && name === OPT_OUT && { props: OPT_OUT }) ||
// when menu option get selected
(name && params.find((param) => param.name === name)) ||
// when being initialized
params.find((param) => !!param.default) ||
// fallback to the first
params[0] ||
// fallback for destructuring
{};
return props;
};
/**
* @nosideeffects
* Collect the propsMap from Nodes based on a controlled state tracker.
*/
type getPropsMap = (contextNodes: ContextNode[], selectionState: SelectionState) => PropsMap;
export const getPropsMap: getPropsMap = (contextNodes, selectionState) =>
contextNodes.reduce((agg, { nodeId, params, options }) => {
// eslint-disable-next-line no-param-reassign
agg[nodeId] = _getPropsByParamName(params, selectionState[nodeId], options);
return agg;
}, Object());

View File

@ -1,110 +0,0 @@
import { _getAggregatedWrap, getRendererFrom } from './getRendererFrom';
import { OPT_OUT } from '../../shared/constants';
// mocks
const h = jest.fn();
const spiedAggregator = _getAggregatedWrap(h);
beforeEach(() => {
h.mockReset();
});
// tests
describe('Test on aggregation of a single context', () => {
const fakeTag = 'fakeTag';
const fakeComponent = () => '';
it('should skip wrapping when being set to disable', () => {
// given
const testedProps = {};
const testedOption = { disable: true };
// when
spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)();
// then
expect(h).toHaveBeenCalledTimes(0);
});
it('should skip wrapping when props is marked as "OPT_OUT"', () => {
// given
const testedProps = OPT_OUT;
const testedOption = { cancelable: true };
// when
spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)();
// then
expect(h).toHaveBeenCalledTimes(0);
});
it('should wrap components in the stacking order', () => {
// given
const testedProps = {};
const testedOption = {};
// when
spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)();
// then
expect(h).toHaveBeenCalledTimes(2);
expect(h.mock.calls[0][0]).toBe(fakeComponent);
expect(h.mock.calls[1][0]).toBe(fakeTag);
});
it('should NOT pass props deeply by default', () => {
// given
const testedProps = {};
const testedOption = {};
// when
spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)();
// then
expect(h.mock.calls[0][1]).toBe(null);
expect(h.mock.calls[1][1]).toBe(testedProps);
});
it('should pass props deeply', () => {
const testedProps = {};
const testedOption = { deep: true };
spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)();
expect(h.mock.calls[0][1]).toBe(testedProps);
expect(h.mock.calls[1][1]).toBe(testedProps);
});
});
describe('Test on aggregation of contexts', () => {
it('should aggregate contexts in the stacking order', () => {
// given
const someContextNodes = [
{
components: ['div'],
icon: 'box' as const,
nodeId: 'Some Context',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'A', props: {} }],
title: 'Some Context',
},
{
components: ['span'],
icon: 'box' as const,
nodeId: 'Another Context',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'B', props: {} }],
title: 'Another Context',
},
];
const propsMap = {
'Some Context': {},
'Another Context': {},
};
// when
getRendererFrom(h)(someContextNodes, propsMap, () => {});
// then
expect(h.mock.calls[0][0]).toBe(someContextNodes[1].components[0]);
expect(h.mock.calls[1][0]).toBe(someContextNodes[0].components[0]);
});
});

View File

@ -1,66 +0,0 @@
/* eslint-disable no-underscore-dangle */
import { OPT_OUT } from '../../shared/constants';
import {
AddonOptions,
AnyFunctionReturns,
ContextNode,
GenericProp,
PropsMap,
} from '../../shared/types.d';
/**
* @private
* Aggregate component vNodes with activated props in a descending order,
* based on the given options in the contextual environment setup.
*
* @param {function} h - the associated `createElement` vNode creator from the framework
*/
type _getAggregatedWrap = <T>(
h: AnyFunctionReturns<T>
) => (
components: ContextNode['components'],
props: GenericProp | typeof OPT_OUT,
options: AddonOptions
) => AnyFunctionReturns<T>;
export const _getAggregatedWrap: _getAggregatedWrap = (h) => (components, props, options) => (
vNode
) => {
const last = components.length - 1;
const isSkipped =
// when set to disable
options.disable ||
// when opt-out context
(options.cancelable && props === OPT_OUT);
return isSkipped
? vNode
: components
// shallow clone the array since .reverse() is not pure
.concat()
// reverse the array to get the correct wrapping sequence (i.e. left(right))
.reverse()
.reduce((acc, C, index) => h(C, options.deep || index === last ? props : null, acc), vNode);
};
/**
* @nosideeffects
* Aggregate aggregated-components among all contextual nodes in a descending order;
* this is the core of this addon, which is based on the general virtual DOM implementation.
*
* @param {function} h - the associated `createElement` vNode creator from the framework
*/
type getRendererFrom = <T>(
h: AnyFunctionReturns<T>
) => (contextNodes: ContextNode[], propsMap: PropsMap, getStoryVNode: AnyFunctionReturns<T>) => T;
export const getRendererFrom: getRendererFrom = (h) => (contextNodes, propsMap, getStoryVNode) =>
contextNodes
// map over contextual nodes to get the wrapping function
.map(({ nodeId, components, options }) =>
_getAggregatedWrap(h)(components, propsMap[nodeId], options)
)
// reverse the array to get the correct wrapping sequence (i.e. top(down))
.reverse()
// stitch everything to get the final vNode
.reduce((vNode, wrap) => wrap(vNode), getStoryVNode());

View File

@ -1,4 +0,0 @@
export { memorize, singleton } from './decorators';
export { getContextNodes } from './getContextNodes';
export { getPropsMap } from './getPropsMap';
export { getRendererFrom } from './getRendererFrom';

View File

@ -1,13 +0,0 @@
import { createElement } from 'react';
import addons, { types } from '@storybook/addons';
import { ContextsManager } from './manager/ContextsManager';
import { ID } from './shared/constants';
addons.register(ID, (api) =>
addons.add(ID, {
title: ID,
type: types.TOOL,
match: ({ viewMode }) => viewMode === 'story',
render: () => createElement(ContextsManager, { api }),
})
);

View File

@ -1 +0,0 @@
declare module 'global';

View File

@ -1,7 +0,0 @@
/**
* Preact v8.4.2 shipped with global polluted JSX typing, which breaks the React components typing under Manager
*/
declare module 'preact' {
declare type VNode = any;
declare const h: any = () => {};
}

View File

@ -1,17 +0,0 @@
export { FORCE_RE_RENDER, SET_CURRENT_STORY } from '@storybook/core-events';
// configs
export const ID = 'addon-contexts' as const;
export const PARAM = 'contexts' as const;
// tokens
/**
* OPT_OUT is a token for skipping a context, dundering the string to avoid name collisions;
* ES6 Symbol is not available due to stringify used in Storybook event system via the channel.
*/
export const OPT_OUT = '__OPT_OUT__' as const;
// events
export const REBOOT_MANAGER = `${ID}/REBOOT_MANAGER`;
export const UPDATE_MANAGER = `${ID}/UPDATE_MANAGER`;
export const UPDATE_PREVIEW = `${ID}/UPDATE_PREVIEW`;

View File

@ -1,21 +0,0 @@
import { deserialize, serialize } from './serializers';
describe('Test on serializers', () => {
// given
const someContextsQueryParam = 'CSS Themes=Forests,Languages=Fr';
const someSelectionState = {
'CSS Themes': 'Forests',
Languages: 'Fr',
};
it('Should deserialize a string representation into the represented selection state', () => {
expect(deserialize('')).toEqual(null);
expect(deserialize('An invalid string=')).toEqual(null);
expect(deserialize(someContextsQueryParam)).toEqual(someSelectionState);
});
it('Should serialize selection state into its string representation', () => {
expect(serialize(null)).toEqual(null);
expect(serialize(someSelectionState)).toEqual(someContextsQueryParam);
});
});

View File

@ -1,29 +0,0 @@
import { SelectionState } from './types.d';
/**
* Deserialize URL query param into the specified selection state.
*/
type deserialize = (param?: string) => SelectionState | null;
export const deserialize: deserialize = (param) =>
!param
? null
: param
.split(/,+/g)
.map((str) => str.split(/=+/g))
.reduce<SelectionState | null>(
(acc, [nodeId, name]) => (nodeId && name ? { ...acc, [nodeId]: name } : acc),
null
);
/**
* Serialize the selection state in its string representation.
*/
type serialize = (state: ReturnType<deserialize>) => string | null;
export const serialize: serialize = (state) =>
!state
? null
: Object.entries(state)
.map((tuple) => tuple.join('='))
.join(',');

View File

@ -1,49 +0,0 @@
import { ComponentProps, FunctionComponent } from 'react';
import { Icons } from '@storybook/components';
export { API as ManagerAPI } from '@storybook/api';
// helpers
export declare type AnyFunctionReturns<T> = (...arg: any[]) => T;
export declare type FCNoChildren<P> = FunctionComponent<{ children?: never } & P>;
export declare type GenericProp = null | {
readonly [key: string]: unknown;
};
// interfaces
export declare interface AddonOptions {
deep?: boolean;
disable?: boolean;
cancelable?: boolean;
}
export declare interface AddonSetting {
icon?: ComponentProps<typeof Icons>['icon'] | '';
title: string;
components?: unknown[];
params?: {
name: string;
props: GenericProp;
default?: boolean;
}[];
options?: AddonOptions;
}
export declare interface ContextNode extends Required<AddonSetting> {
nodeId: string;
options: Required<AddonOptions>;
}
export declare interface SelectionState {
readonly [key: string]: string | undefined;
}
export declare interface PropsMap {
readonly [key: string]: GenericProp;
}
export declare interface WrapperSettings {
options?: AddonSetting[];
// `parameters` can be set to `false` to disable the addon
parameters?: AddonSetting[] | false;
}

View File

@ -1,15 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"preact": ["src/shared/@mock-types/_preact.d.ts"]
},
"strictNullChecks": true,
"removeComments": true,
"rootDir": "./src",
"types": ["webpack-env", "jest"]
},
"include": ["src/**/*"],
"exclude": ["src/register.ts", "src/**/*.test.ts"]
}

View File

@ -1,4 +0,0 @@
import { withContexts } from './dist/preview/frameworks/vue';
export { withContexts };
export default withContexts;

View File

@ -22,13 +22,10 @@ describe('Navigation', () => {
});
describe('Routing', () => {
before(() => {
visitExample('official-storybook');
});
it('should navigate to story addons-a11y-basebutton--default', () => {
cy.get('#addons-a11y-basebutton--label').click();
visitExample('official-storybook');
cy.get('#addons-a11y-basebutton--label').click();
cy.url().should('include', 'path=/story/addons-a11y-basebutton--label');
});

View File

@ -38,18 +38,15 @@ Cypress.Commands.add(
);
Cypress.Commands.add('preview', {}, () => {
return cy.get(`#storybook-preview-iframe`).then({ timeout: 10000 }, (iframe) => {
return cy.get(`#storybook-preview-iframe`).then({ timeout: 20000 }, (iframe) => {
const content = iframe[0].contentDocument;
const element = content !== null ? content.documentElement : null;
return cy
.wrap(iframe)
.get(iframe, { timeout: 20000 })
.should(() => {
expect(element).not.null;
if (element !== null) {
expect(element.querySelector('#root > *')).not.null;
}
expect(element.querySelector('#root')).not.null;
})
.then(() => {
return element.querySelector('#root');

View File

@ -20,7 +20,6 @@ module.exports = {
'@storybook/addon-jest',
'@storybook/addon-viewport',
'@storybook/addon-graphql',
'@storybook/addon-contexts',
'@storybook/addon-toolbars',
'@storybook/addon-queryparams',
],

View File

@ -17,7 +17,6 @@
"@storybook/addon-a11y": "6.0.0-alpha.42",
"@storybook/addon-actions": "6.0.0-alpha.42",
"@storybook/addon-backgrounds": "6.0.0-alpha.42",
"@storybook/addon-contexts": "6.0.0-alpha.42",
"@storybook/addon-cssresources": "6.0.0-alpha.42",
"@storybook/addon-design-assets": "6.0.0-alpha.42",
"@storybook/addon-docs": "6.0.0-alpha.42",

View File

@ -1,101 +0,0 @@
import React from 'react';
import { withContexts } from '@storybook/addon-contexts/react';
// Example A: Simple CSS Theming
const topLevelContexts = [
{
title: 'CSS Themes',
components: ['div'],
params: [
{
name: 'Desert',
props: {
style: { color: 'brown', background: '#F4A261', height: '100vh', padding: '10px' },
},
},
{
name: 'Ocean',
props: {
style: { color: 'white', background: '#173F5F', height: '100vh', padding: '10px' },
},
default: true,
},
],
},
];
const storyLevelContexts = [
{
title: 'CSS Themes',
params: [
{
name: 'Forest',
props: {
style: { color: 'teal', background: '#00b894', height: '100vh', padding: '10px' },
},
},
],
},
];
export default {
title: 'Addons/Contexts',
decorators: [withContexts(topLevelContexts)],
};
export const SimpleCssTheming = () => (
<>I'm a children of the injected 'div' (where provides a theming context).</>
);
SimpleCssTheming.story = {
name: 'Simple CSS Theming',
parameters: {
contexts: storyLevelContexts,
},
};
// Example B: Language (React Contexts API)
const NaiveIntlContext = React.createContext({
locale: 'unknown',
greeting: 'NULL',
});
export const Languages = () => (
<NaiveIntlContext.Consumer>
{({ locale, greeting }) => `Your locale is "${locale}", so I say "${greeting}"!`}
</NaiveIntlContext.Consumer>
);
Languages.story = {
parameters: {
contexts: [
{
icon: 'globe',
title: 'Languages',
components: [NaiveIntlContext.Provider],
params: [
{
name: 'English',
props: {
value: { locale: 'en', greeting: 'Hello' },
},
},
{
name: 'French',
props: {
value: { locale: 'fr', greeting: 'Bonjour' },
},
},
{
name: 'Chinese',
props: {
value: { locale: 'cn', greeting: '你好' },
},
},
],
options: {
cancelable: true,
},
},
],
},
};

View File

@ -9,7 +9,6 @@ module.exports = {
'@storybook/addon-knobs',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
'@storybook/addon-contexts',
'@storybook/addon-a11y',
],
webpackFinal: (config) => {

View File

@ -18,7 +18,6 @@
"@storybook/addon-a11y": "6.0.0-alpha.42",
"@storybook/addon-actions": "6.0.0-alpha.42",
"@storybook/addon-backgrounds": "6.0.0-alpha.42",
"@storybook/addon-contexts": "6.0.0-alpha.42",
"@storybook/addon-knobs": "6.0.0-alpha.42",
"@storybook/addon-links": "6.0.0-alpha.42",
"@storybook/addon-storyshots": "6.0.0-alpha.42",

View File

@ -1,18 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons/Contexts Simple CSS Theming 1`] = `
<div
style={
Object {
"background": "#173F5F",
"color": "white",
"height": "100vh",
"padding": "10px",
}
}
>
<div>
I'm a children of the injected 'div' (where provides a theming context).
</div>
</div>
`;

View File

@ -1,58 +0,0 @@
/** @jsx h */
import { h } from 'preact';
import { withContexts } from '@storybook/addon-contexts/preact';
// Example A: Simple CSS Theming
const topLevelContexts = [
{
icon: 'sidebaralt',
title: 'CSS Themes',
components: ['div'],
params: [
{
name: 'Desert',
props: {
style: { color: 'brown', background: '#F4A261', height: '100vh', padding: '10px' },
},
},
{
name: 'Ocean',
props: {
style: { color: 'white', background: '#173F5F', height: '100vh', padding: '10px' },
},
default: true,
},
],
},
];
const storyLevelContexts = [
{
title: 'CSS Themes',
params: [
{
name: 'Forest',
props: {
style: { color: 'teal', background: '#00b894', height: '100vh', padding: '10px' },
},
},
],
},
];
export default {
title: 'Addons/Contexts',
decorators: [withContexts(topLevelContexts)],
};
export const SimpleCssTheming = () => (
<div>I'm a children of the injected 'div' (where provides a theming context).</div>
);
SimpleCssTheming.story = {
name: 'Simple CSS Theming',
parameters: {
contexts: storyLevelContexts,
},
};

View File

@ -1,57 +0,0 @@
import { createElement } from 'rax';
import { withContexts } from '@storybook/addon-contexts/rax';
// Example A: Simple CSS Theming
const topLevelContexts = [
{
icon: 'sidebaralt',
title: 'CSS Themes',
components: ['div'],
params: [
{
name: 'Desert',
props: {
style: { color: 'brown', background: '#F4A261', height: '100vh', padding: '10px' },
},
},
{
name: 'Ocean',
props: {
style: { color: 'white', background: '#173F5F', height: '100vh', padding: '10px' },
},
default: true,
},
],
},
];
const storyLevelContexts = [
{
title: 'CSS Themes',
params: [
{
name: 'Forest',
props: {
style: { color: 'teal', background: '#00b894', height: '100vh', padding: '10px' },
},
},
],
},
];
export default {
title: 'Addons/Contexts',
decorators: [withContexts(topLevelContexts)],
};
export const SimpleCssTheming = () => (
<div>I'm a children of the injected 'div' (where provides a theming context).</div>
);
SimpleCssTheming.story = {
name: 'Simple CSS Theming',
parameters: {
contexts: storyLevelContexts,
},
};

View File

@ -9,6 +9,5 @@ module.exports = {
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
'@storybook/addon-a11y',
'@storybook/addon-contexts',
],
};

View File

@ -17,7 +17,6 @@
"@storybook/addon-a11y": "6.0.0-alpha.42",
"@storybook/addon-actions": "6.0.0-alpha.42",
"@storybook/addon-backgrounds": "6.0.0-alpha.42",
"@storybook/addon-contexts": "6.0.0-alpha.42",
"@storybook/addon-docs": "6.0.0-alpha.42",
"@storybook/addon-knobs": "6.0.0-alpha.42",
"@storybook/addon-links": "6.0.0-alpha.42",

View File

@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addon/Contexts Languages 1`] = `
<div
style="color: white; background: rgb(23, 63, 95); height: 100vh; padding: 10px;"
>
<div>
Your locale is unknown, so I say NULL!
</div>
</div>
`;
exports[`Storyshots Addon/Contexts Simple CSS Theming 1`] = `
<div
style="color: white; background: rgb(23, 63, 95); height: 100vh; padding: 10px;"
>
<span>
I'm a children of the injected 'div' (where provides a theming context).
</span>
</div>
`;

View File

@ -1,146 +0,0 @@
import { withContexts } from '@storybook/addon-contexts/vue';
// Example A: Simple CSS Theming
const topLevelContexts = [
{
icon: 'sidebaralt',
title: 'CSS Themes',
components: ['div'],
params: [
{
name: 'Desert',
props: {
style: { color: 'brown', background: '#F4A261', height: '100vh', padding: '10px' },
},
},
{
name: 'Ocean',
props: {
style: { color: 'white', background: '#173F5F', height: '100vh', padding: '10px' },
},
default: true,
},
],
},
];
const storyLevelContexts = [
{
title: 'CSS Themes',
params: [
{
name: 'Forest',
props: {
style: { color: 'teal', background: '#00b894', height: '100vh', padding: '10px' },
},
},
],
},
];
export default {
title: 'Addon/Contexts',
decorators: [withContexts(topLevelContexts)],
};
export const SimpleCssTheming = () => ({
template: "<span>I'm a children of the injected 'div' (where provides a theming context).</span>",
});
SimpleCssTheming.story = {
name: 'Simple CSS Theming',
parameters: {
contexts: storyLevelContexts,
},
};
// Example B: Language (Vue provide/inject API)
const createContext = (initialValue) => {
const uid = `_${Date.now()}${Math.random()}`;
return {
Provider: {
name: `Context.Provider`,
props: ['value'],
provide() {
return this.value === undefined
? undefined
: {
[uid]: () => this.value,
};
},
template: `
<div>
<slot />
</div>
`,
},
Consumer: {
name: `Context.Consumer`,
inject: {
[uid]: {
default: () => () =>
initialValue instanceof Object ? { ...initialValue } : { value: initialValue },
},
},
computed: {
value() {
return this[uid]();
},
},
template: `
<div>
<slot v-bind="value" />
</div>
`,
},
};
};
const NaiveIntlContext = createContext({
locale: 'unknown',
greeting: 'NULL',
});
export const Languages = () => ({
components: { 'NaiveIntlContext.Consumer': NaiveIntlContext.Consumer },
template: `
<NaiveIntlContext.Consumer v-slot="{ locale, greeting }">
Your locale is {{ locale }}, so I say {{ greeting }}!
</NaiveIntlContext.Consumer>
`,
});
Languages.story = {
parameters: {
contexts: [
{
icon: 'globe',
title: 'Languages',
components: [NaiveIntlContext.Provider],
params: [
{
name: 'English',
props: {
value: { locale: 'en', greeting: 'Hello' },
},
},
{
name: 'French',
props: {
value: { locale: 'fr', greeting: 'Bonjour' },
},
},
{
name: 'Chinese',
props: {
value: { locale: 'cn', greeting: '你好' },
},
},
],
options: {
cancelable: true,
},
},
],
},
};

View File

@ -82,7 +82,6 @@ Let's throw in a crazy table, because why not?
| [actions](addons/actions) | + | + | + | + | + | + | + | + | + | + | + |
| [backgrounds](addons/backgrounds) | + | \* | + | + | + | + | + | + | + | + | + |
| [centered](addons/centered) | + | | + | + | + | + | | + | | + | + |
| [contexts](addons/contexts) | + | | + | | | | | | | | + |
## Code

View File

@ -82,7 +82,6 @@ Let's throw in a crazy table, because why not?
| [actions](addons/actions) | + | + | + | + | + | + | + | + | + | + | + |
| [backgrounds](addons/backgrounds) | + | \* | + | + | + | + | + | + | + | + | + |
| [centered](addons/centered) | + | | + | + | + | + | | + | | + | + |
| [contexts](addons/contexts) | + | | + | | | | | | | | + |
## Code