diff --git a/addons/a11y/src/register.tsx b/addons/a11y/src/register.tsx
index a0995ab4fcee..9b360ec97d4c 100644
--- a/addons/a11y/src/register.tsx
+++ b/addons/a11y/src/register.tsx
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
addons.add(PANEL_ID, {
diff --git a/addons/actions/src/constants.ts b/addons/actions/src/constants.ts
index 1bc018e8d9ba..9bc6f11ef5c4 100644
--- a/addons/actions/src/constants.ts
+++ b/addons/actions/src/constants.ts
@@ -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`;
diff --git a/addons/actions/src/manager.tsx b/addons/actions/src/manager.tsx
index be1e6f8e1261..d77272be077b 100644
--- a/addons/actions/src/manager.tsx
+++ b/addons/actions/src/manager.tsx
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
}
diff --git a/addons/cssresources/src/register.tsx b/addons/cssresources/src/register.tsx
index 4c893c5ccc92..4366ea61801a 100644
--- a/addons/cssresources/src/register.tsx
+++ b/addons/cssresources/src/register.tsx
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
diff --git a/addons/design-assets/src/register.tsx b/addons/design-assets/src/register.tsx
index 61fda7a988db..fb903ec514e5 100644
--- a/addons/design-assets/src/register.tsx
+++ b/addons/design-assets/src/register.tsx
@@ -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, () => {
),
+ paramKey: PARAM_KEY,
});
});
diff --git a/addons/events/src/constants.js b/addons/events/src/constants.js
index cf57a2570904..3d765994293b 100644
--- a/addons/events/src/constants.js
+++ b/addons/events/src/constants.js
@@ -1,3 +1,5 @@
+export const PARAM_KEY = 'events';
+
export const ADDON_ID = 'storybook/events';
export const PANEL_ID = `${ADDON_ID}/panel`;
diff --git a/addons/events/src/manager.js b/addons/events/src/manager.js
index 6d8bea634686..d7cab5bfc338 100644
--- a/addons/events/src/manager.js
+++ b/addons/events/src/manager.js
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
}
diff --git a/addons/graphql/src/register.ts b/addons/graphql/src/register.ts
index d69b5af63619..68d41a5dd20e 100644
--- a/addons/graphql/src/register.ts
+++ b/addons/graphql/src/register.ts
@@ -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,
});
});
};
diff --git a/addons/jest/src/register.tsx b/addons/jest/src/register.tsx
index b3058d321dd6..a8d3b2c97bad 100644
--- a/addons/jest/src/register.tsx
+++ b/addons/jest/src/register.tsx
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
diff --git a/addons/jest/src/shared.ts b/addons/jest/src/shared.ts
index 08438c671482..17e74d304d48 100644
--- a/addons/jest/src/shared.ts
+++ b/addons/jest/src/shared.ts
@@ -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`;
diff --git a/addons/knobs/src/register.js b/addons/knobs/src/register.js
index e8eeddb512c1..2808815b3958 100644
--- a/addons/knobs/src/register.js
+++ b/addons/knobs/src/register.js
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
diff --git a/addons/knobs/src/shared.js b/addons/knobs/src/shared.js
index 64aa6af2d923..267862d05d10 100644
--- a/addons/knobs/src/shared.js
+++ b/addons/knobs/src/shared.js
@@ -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`;
diff --git a/addons/notes/src/register.tsx b/addons/notes/src/register.tsx
index 17d3c8d4189f..0e56ab83ae1a 100644
--- a/addons/notes/src/register.tsx
+++ b/addons/notes/src/register.tsx
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
}
diff --git a/addons/ondevice-actions/src/index.tsx b/addons/ondevice-actions/src/index.tsx
index 76602d22ae9c..63b71d68a676 100644
--- a/addons/ondevice-actions/src/index.tsx
+++ b/addons/ondevice-actions/src/index.tsx
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
}
diff --git a/addons/ondevice-backgrounds/src/constants.js b/addons/ondevice-backgrounds/src/constants.js
index 7e0fcc42dae1..4c8f09259143 100644
--- a/addons/ondevice-backgrounds/src/constants.js
+++ b/addons/ondevice-backgrounds/src/constants.js
@@ -1,3 +1,4 @@
+export const PARAM_KEY = 'background';
export const ADDON_ID = 'storybook-addon-background';
export const PANEL_ID = `${ADDON_ID}/background-panel`;
diff --git a/addons/ondevice-backgrounds/src/register.js b/addons/ondevice-backgrounds/src/register.js
index 0bd614430dae..2714689abb25 100644
--- a/addons/ondevice-backgrounds/src/register.js
+++ b/addons/ondevice-backgrounds/src/register.js
@@ -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 }) => ,
+ paramKey: PARAM_KEY,
});
});
diff --git a/addons/ondevice-knobs/src/index.js b/addons/ondevice-knobs/src/index.js
index 8286083ad1ed..9e46af4a4071 100644
--- a/addons/ondevice-knobs/src/index.js
+++ b/addons/ondevice-knobs/src/index.js
@@ -9,6 +9,7 @@ export function register() {
title: 'Knobs',
// eslint-disable-next-line react/prop-types
render: ({ active, key }) => ,
+ paramKey: 'knobs',
});
});
}
diff --git a/addons/ondevice-notes/src/register.js b/addons/ondevice-notes/src/register.js
index b68ccc3a6d52..c3850db2047a 100644
--- a/addons/ondevice-notes/src/register.js
+++ b/addons/ondevice-notes/src/register.js
@@ -49,5 +49,6 @@ addons.register('storybook/notes', api => {
addons.addPanel('storybook/notes/panel', {
title: 'Notes',
render: ({ active, key }) => ,
+ paramKey: PARAM_KEY,
});
});
diff --git a/docs/src/pages/addons/using-addons/index.md b/docs/src/pages/addons/using-addons/index.md
index 42cec71d9e4c..cbfcb08e0caf 100644
--- a/docs/src/pages/addons/using-addons/index.md
+++ b/docs/src/pages/addons/using-addons/index.md
@@ -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',
+ () => (
+
+ ),
+ { 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.
diff --git a/docs/src/pages/addons/writing-addons/index.md b/docs/src/pages/addons/writing-addons/index.md
index 6e4d5338bff1..8e3380cd92ed 100644
--- a/docs/src/pages/addons/writing-addons/index.md
+++ b/docs/src/pages/addons/writing-addons/index.md
@@ -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: () =>
Addon tab content
,
+ paramKey: 'myAddon',
+ });
+});
+```
+
+While adding a story, you can then pass a `disabled` parameter.
+
+```js
+storiesOf('Button', module)
+ .add('with text', () => , {
+ 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!
diff --git a/lib/addons/src/index.ts b/lib/addons/src/index.ts
index 046b59d403cc..5e4843cbaf17 100644
--- a/lib/addons/src/index.ts
+++ b/lib/addons/src/index.ts
@@ -24,6 +24,7 @@ export interface Addon {
route?: (routeOptions: RouteOptions) => string;
match?: (matchOptions: MatchOptions) => boolean;
render: (renderOptions: RenderOptions) => ReactElement;
+ paramKey?: string;
}
export type Loader = (api: API) => void;
diff --git a/lib/api/src/modules/addons.ts b/lib/api/src/modules/addons.ts
index efc6a1758b3f..ce95b9765bde 100644
--- a/lib/api/src/modules/addons.ts
+++ b/lib/api/src/modules/addons.ts
@@ -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;
+ paramKey?: string;
}
export interface Collection {
[key: string]: Addon;
@@ -42,9 +43,16 @@ interface Panels {
type StateMerger = (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(
@@ -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);
diff --git a/lib/api/src/tests/addons.test.js b/lib/api/src/tests/addons.test.js
new file mode 100644
index 000000000000..c17c485518ec
--- /dev/null
+++ b/lib/api/src/tests/addons.test.js
@@ -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' });
+ });
+ });
+});
diff --git a/lib/ui/src/containers/__snapshots__/panel.stories.storyshot b/lib/ui/src/containers/__snapshots__/panel.stories.storyshot
new file mode 100644
index 000000000000..9ec192c6ca77
--- /dev/null
+++ b/lib/ui/src/containers/__snapshots__/panel.stories.storyshot
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots UI|Addon Panel default 1`] = `
+
+ By default all addon panels are rendered
+
+`;
+
+exports[`Storyshots UI|Addon Panel disable panel 1`] = `
+
+ This story disables Actions and Accessibility panels
+
+ storiesOf('UI|Addon Panel', module)
+ .add(
+ 'my story',
+ <MyComponent />,
+ { a11y: { disable: true }, actions: { disable: true } }
+ );
+
+
+`;
diff --git a/lib/ui/src/containers/panel.js b/lib/ui/src/containers/panel.js
index de4d62f233f2..88821840822d 100644
--- a/lib/ui/src/containers/panel.js
+++ b/lib/ui/src/containers/panel.js
@@ -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),
diff --git a/lib/ui/src/containers/panel.stories.js b/lib/ui/src/containers/panel.stories.js
new file mode 100644
index 000000000000..f920d6639ffb
--- /dev/null
+++ b/lib/ui/src/containers/panel.stories.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+
+storiesOf('UI|Addon Panel', module)
+ .add('default', () => By default all addon panels are rendered
)
+ .add(
+ 'disable panel',
+ () => (
+
+ This story disables Actions and Accessibility panels
+
+ {`storiesOf('UI|Addon Panel', module)
+ .add(
+ 'my story',
+ ,
+ { a11y: { disable: true }, actions: { disable: true } }
+ );`}
+
+
+ ),
+ { a11y: { disabled: true }, actions: { disabled: true } }
+ );