Addons: Disable option for addon tab (#6923)

feature request: disable option for addon tab
This commit is contained in:
Michael Shilman 2019-07-04 12:35:23 +08:00 committed by GitHub
commit b9d2ba2611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 317 additions and 13 deletions

View File

@ -2,7 +2,7 @@ import React, { Fragment, FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { addons, types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import { ColorBlindness } from './components/ColorBlindness';
import { A11YPanel } from './components/A11YPanel';
@ -94,6 +94,7 @@ addons.register(ADDON_ID, api => {
title: 'Accessibility',
type: types.PANEL,
render: ({ active, key }) => <A11YPanel key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
addons.add(PANEL_ID, {

View File

@ -1,3 +1,4 @@
export const PARAM_KEY = 'actions';
export const ADDON_ID = 'storybook/actions';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const EVENT_ID = `${ADDON_ID}/action-event`;

View File

@ -1,13 +1,14 @@
import React from 'react';
import addons from '@storybook/addons';
import ActionLogger from './containers/ActionLogger';
import { ADDON_ID, PANEL_ID } from '.';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
export function register() {
addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'Actions',
render: ({ active, key }) => <ActionLogger key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { addons, types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import { CssResourcePanel } from './css-resource-panel';
addons.register(ADDON_ID, api => {
@ -10,5 +10,6 @@ addons.register(ADDON_ID, api => {
type: types.PANEL,
title: 'CSS resources',
render: ({ active }) => <CssResourcePanel key={PANEL_ID} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -2,7 +2,7 @@ import React from 'react';
import { addons, types } from '@storybook/addons';
import { AddonPanel } from '@storybook/components';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import { Panel } from './panel';
addons.register(ADDON_ID, () => {
@ -14,5 +14,6 @@ addons.register(ADDON_ID, () => {
<Panel />
</AddonPanel>
),
paramKey: PARAM_KEY,
});
});

View File

@ -1,3 +1,5 @@
export const PARAM_KEY = 'events';
export const ADDON_ID = 'storybook/events';
export const PANEL_ID = `${ADDON_ID}/panel`;

View File

@ -2,7 +2,7 @@ import React from 'react';
import addons from '@storybook/addons';
import Panel from './components/Panel';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
export function register() {
addons.register(ADDON_ID, api => {
@ -10,6 +10,7 @@ export function register() {
title: 'Events',
// eslint-disable-next-line react/prop-types
render: ({ active, key }) => <Panel key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});
}

View File

@ -1,7 +1,7 @@
import { addons, types } from '@storybook/addons';
import GQL from './manager';
import { ADDON_ID } from '.';
import { ADDON_ID, PARAM_KEY } from '.';
export const register = () => {
addons.register(ADDON_ID, () => {
@ -11,6 +11,7 @@ export const register = () => {
route: ({ storyId }) => `/graphql/${storyId}`,
match: ({ viewMode }) => viewMode === 'graphql',
render: GQL,
paramKey: PARAM_KEY,
});
});
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import addons from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './shared';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared';
import Panel from './components/Panel';
@ -8,5 +8,6 @@ addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'tests',
render: ({ active, key }) => <Panel key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -1,4 +1,5 @@
// addons, panels and events get unique names using a prefix
export const PARAM_KEY = 'test';
export const ADDON_ID = 'storybookjs/test';
export const PANEL_ID = `${ADDON_ID}/panel`;

View File

@ -1,12 +1,13 @@
import React from 'react';
import addons from '@storybook/addons';
import Panel from './components/Panel';
import { ADDON_ID, PANEL_ID } from './shared';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared';
addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'Knobs',
// eslint-disable-next-line react/prop-types
render: ({ active, key }) => <Panel api={api} key={key} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -1,4 +1,5 @@
// addons, panels and events get unique names using a prefix
export const PARAM_KEY = 'knobs';
export const ADDON_ID = 'storybookjs/knobs';
export const PANEL_ID = `${ADDON_ID}/panel`;

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import addons, { types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './shared';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared';
// TODO: fix eslint in tslint (igor said he fixed it, should ask him)
import Panel from './Panel';
@ -14,6 +14,7 @@ export default function register(type: types) {
route: ({ storyId }) => `/info/${storyId}`, // todo add type
match: ({ viewMode }) => viewMode === 'info', // todo add type
render: ({ active }) => <Panel api={api} active={active} />,
paramKey: PARAM_KEY,
});
});
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import addons from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from '@storybook/addon-actions';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from '@storybook/addon-actions';
import ActionLogger from './containers/ActionLogger';
export function register() {
@ -8,6 +8,7 @@ export function register() {
addons.addPanel(PANEL_ID, {
title: 'Actions',
render: ({ active, key }) => <ActionLogger key={key} active={active} />,
paramKey: PARAM_KEY,
});
});
}

View File

@ -1,3 +1,4 @@
export const PARAM_KEY = 'background';
export const ADDON_ID = 'storybook-addon-background';
export const PANEL_ID = `${ADDON_ID}/background-panel`;

View File

@ -1,7 +1,7 @@
import React from 'react';
import addons from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import BackgroundPanel from './BackgroundPanel';
addons.register(ADDON_ID, api => {
@ -10,5 +10,6 @@ addons.register(ADDON_ID, api => {
title: 'Backgrounds',
// eslint-disable-next-line react/prop-types
render: ({ active }) => <BackgroundPanel channel={channel} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -9,6 +9,7 @@ export function register() {
title: 'Knobs',
// eslint-disable-next-line react/prop-types
render: ({ active, key }) => <Panel key={key} channel={channel} active={active} />,
paramKey: 'knobs',
});
});
}

View File

@ -49,5 +49,6 @@ addons.register('storybook/notes', api => {
addons.addPanel('storybook/notes/panel', {
title: 'Notes',
render: ({ active, key }) => <Notes key={key} channel={channel} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -65,6 +65,29 @@ Then you'll be able to see those notes when you are viewing the story.
![Stories with notes](../static/stories-with-notes.png)
## Disable the addon
You can disable an addon panel for a story by adding a `disabled` parameter.
```js
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Button from './Button';
storiesOf('Button', module).add(
'with some emoji',
() => (
<Button onClick={action('clicked')}>
<span role="img" aria-label="so cool">
😀 😎 👍 💯
</span>
</Button>
),
{ notes: { disabled: true } }
);
```
## Global Configuration
Sometimes you might want to configure an addon globally, as in the case of collocating stories with components, or just simply to keep your stories file cleaner. To do that, you can add your decorators to a config file, typically in `.storybook/config.js`. Here's an example of how you might do that.

View File

@ -89,6 +89,7 @@ addons.register(ADDON_ID, api => {
type: types.PANEL,
title,
render,
paramKey: PARAM_KEY,
});
});
```
@ -246,6 +247,34 @@ storiesOf('Button', module)
});
```
### Disabling an addon panel
It's possible to disable an addon panel for a particular story.
To offer that capability, you need to pass the paramKey when you register the panel
```js
addons.register(ADDON_ID, () => {
addons.add(PANEL_ID, {
type: types.PANEL,
title: 'My Addon',
render: () => <div>Addon tab content</div>,
paramKey: 'myAddon',
});
});
```
While adding a story, you can then pass a `disabled` parameter.
```js
storiesOf('Button', module)
.add('with text', () => <Button>Hello Button</Button>, {
myAddon: {
disabled: true,
},
});
```
## Styling your addon
We use [emotion](https://emotion.sh) for styling, AND we provide a theme which can be set by the user!

View File

@ -24,6 +24,7 @@ export interface Addon {
route?: (routeOptions: RouteOptions) => string;
match?: (matchOptions: MatchOptions) => boolean;
render: (renderOptions: RenderOptions) => ReactElement<any>;
paramKey?: string;
}
export type Loader = (api: API) => void;

View File

@ -1,6 +1,6 @@
import { ReactElement } from 'react';
import { Module, State } from '../index';
import { Module } from '../index';
import { Options } from '../store';
export enum types {
@ -31,6 +31,7 @@ export interface Addon {
route?: (routeOptions: RouteOptions) => string;
match?: (matchOptions: MatchOptions) => boolean;
render: (renderOptions: RenderOptions) => ReactElement<any>;
paramKey?: string;
}
export interface Collection {
[key: string]: Addon;
@ -42,9 +43,16 @@ interface Panels {
type StateMerger<S> = (input: S) => S;
interface StoryInput {
parameters: {
[parameterName: string]: any;
};
}
export interface SubAPI {
getElements: (type: Types) => Collection;
getPanels: () => Collection;
getStoryPanels: () => Collection;
getSelectedPanel: () => string;
setSelectedPanel: (panelName: string) => void;
setAddonState<S>(
@ -72,6 +80,28 @@ export default ({ provider, store }: Module) => {
const api: SubAPI = {
getElements: type => provider.getElements(type),
getPanels: () => api.getElements(types.PANEL),
getStoryPanels: () => {
const allPanels = api.getPanels();
const { storyId, storiesHash } = store.getState();
const storyInput = storyId && (storiesHash[storyId] as StoryInput);
if (!allPanels || !storyInput) {
return allPanels;
}
const { parameters } = storyInput;
const filteredPanels: Collection = {};
Object.entries(allPanels).forEach(([id, panel]) => {
const { paramKey } = panel;
if (paramKey && parameters[paramKey] && parameters[paramKey].disabled) {
return;
}
filteredPanels[id] = panel;
});
return filteredPanels;
},
getSelectedPanel: () => {
const { selectedPanel } = store.getState();
return ensurePanel(api.getPanels(), selectedPanel, selectedPanel);

View File

@ -0,0 +1,159 @@
import initAddons, { types } from '../modules/addons';
const PANELS = {
a11y: {
title: 'Accessibility',
paramKey: 'a11y',
},
actions: {
title: 'Actions',
paramKey: 'actions',
},
knobs: {
title: 'Knobs',
paramKey: 'knobs',
},
};
const provider = {
getElements(type) {
if (type === types.PANEL) {
return PANELS;
}
return null;
},
};
const store = {
getState: () => ({
selectedPanel: '',
}),
setState: jest.fn(),
};
describe('Addons API', () => {
describe('#getElements', () => {
it('should return provider elements', () => {
// given
const { api } = initAddons({ provider, store });
// when
const panels = api.getElements(types.PANEL);
// then
expect(panels).toBe(PANELS);
});
});
describe('#getPanels', () => {
it('should return provider panels', () => {
// given
const { api } = initAddons({ provider, store });
// when
const panels = api.getPanels();
// then
expect(panels).toBe(PANELS);
});
});
describe('#getStoryPanels', () => {
it('should return all panels by default', () => {
// given
const { api } = initAddons({ provider, store });
// when
const filteredPanels = api.getStoryPanels();
// then
expect(filteredPanels).toBe(PANELS);
});
it('should filter disabled addons', () => {
// given
const storyId = 'story 1';
const storeWithStory = {
getState: () => ({
storyId,
storiesHash: {
[storyId]: {
parameters: {
a11y: { disabled: true },
},
},
},
}),
setState: jest.fn(),
};
const { api } = initAddons({ provider, store: storeWithStory });
// when
const filteredPanels = api.getStoryPanels();
// then
expect(filteredPanels).toEqual({
actions: PANELS.actions,
knobs: PANELS.knobs,
});
});
});
describe('#getSelectedPanel', () => {
it('should return provider panels', () => {
// given
const storeWithSelectedPanel = {
getState: () => ({
selectedPanel: 'actions',
}),
setState: jest.fn(),
};
const { api } = initAddons({ provider, store: storeWithSelectedPanel });
// when
const selectedPanel = api.getSelectedPanel();
// then
expect(selectedPanel).toBe('actions');
});
it('should return first panel when selected is not a panel', () => {
// given
const storeWithSelectedPanel = {
getState: () => ({
selectedPanel: 'unknown',
}),
setState: jest.fn(),
};
const { api } = initAddons({ provider, store: storeWithSelectedPanel });
// when
const selectedPanel = api.getSelectedPanel();
// then
expect(selectedPanel).toBe('a11y');
});
});
describe('#setSelectedPanel', () => {
it('should set value inn store', () => {
// given
const setState = jest.fn();
const storeWithSelectedPanel = {
getState: () => ({
selectedPanel: 'actions',
}),
setState,
};
const { api } = initAddons({ provider, store: storeWithSelectedPanel });
expect(setState).not.toHaveBeenCalled();
// when
api.setSelectedPanel('knobs');
// then
expect(setState).toHaveBeenCalledWith({ selectedPanel: 'knobs' }, { persistence: 'session' });
});
});
});

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots UI|Addon Panel default 1`] = `
<div>
By default all addon panels are rendered
</div>
`;
exports[`Storyshots UI|Addon Panel disable panel 1`] = `
<div>
This story disables Actions and Accessibility panels
<pre>
storiesOf('UI|Addon Panel', module)
.add(
'my story',
&lt;MyComponent /&gt;,
{ a11y: { disable: true }, actions: { disable: true } }
);
</pre>
</div>
`;

View File

@ -11,7 +11,7 @@ const createPanelActions = memoize(1)(api => ({
}));
const mapper = ({ state, api }) => ({
panels: api.getPanels(),
panels: api.getStoryPanels(),
selectedPanel: api.getSelectedPanel(),
panelPosition: state.layout.panelPosition,
actions: createPanelActions(api),

View File

@ -0,0 +1,22 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
storiesOf('UI|Addon Panel', module)
.add('default', () => <div>By default all addon panels are rendered</div>)
.add(
'disable panel',
() => (
<div>
This story disables Actions and Accessibility panels
<pre>
{`storiesOf('UI|Addon Panel', module)
.add(
'my story',
<MyComponent />,
{ a11y: { disable: true }, actions: { disable: true } }
);`}
</pre>
</div>
),
{ a11y: { disabled: true }, actions: { disabled: true } }
);