Merge branch 'master' of github.com:storybooks/storybook into addmarkosupport

This commit is contained in:
Neville Mehta 2018-05-06 21:13:31 -07:00
commit d8e0673d31
143 changed files with 2341 additions and 187 deletions

2
.github/CODEOWNERS vendored
View File

@ -3,7 +3,7 @@
/addons/a11y/ @jbovenschen
/addons/actions/ @rhalff
/addons/background/ @ndelangen @hypnosphi
/addons/backgrounds/ @ndelangen @hypnosphi
/addons/centered/ @kazupon
/addons/events/ @z4o4z @ndelangen
/addons/graphql/ @mnmtanish

View File

@ -12,7 +12,7 @@ enum class StorybookApp(val appName: String, val exampleDir: String, val merged:
ANGULAR("Angular", "angular-cli"),
POLYMER("Polymer", "polymer-cli"),
MITHRIL("Mithril", "mithril-kitchen-sink"),
HTML("HTML", "html-kitchen-sink", false);
HTML("HTML", "html-kitchen-sink");
val lowerName = appName.toLowerCase()

View File

@ -1,19 +1,19 @@
## Addon / Framework Support Table
| |[React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [Marko](app/marko)|
| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
|[a11y](addons/a11y) |+| | | | | | |
|[actions](addons/actions) |+|+|+|+|+|+|+|
|[background](addons/background) |+| | | | |+| |
|[centered](addons/centered) |+| |+| | |+| |
|[events](addons/events) |+| | | | | | |
|[graphql](addons/graphql) |+| | | | | | |
|[info](addons/info) |+| | | | | | |
|[jest](addons/jest) |+| | | | | | |
|[knobs](addons/knobs) |+|+|+|+|+|+|+|
|[links](addons/links) |+|+|+|+|+|+| |
|[notes](addons/notes) |+| |+|+|+|+| |
|[options](addons/options) |+|+|+|+|+|+|+|
|[storyshots](addons/storyshots) |+|+|+|+| | | |
|[storysource](addons/storysource)|+| |+|+|+|+| |
|[viewport](addons/viewport) |+| |+|+|+|+| |
| |[React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [HTML](app/html)| [Marko](app/marko)|
| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
|[a11y](addons/a11y) |+| | | | | |+| |
|[actions](addons/actions) |+|+|+|+|+|+|+|+|
|[backgrounds](addons/backgrounds) |+| | | | |+|+| |
|[centered](addons/centered) |+| |+| | |+|+| |
|[events](addons/events) |+| | | | | |+| |
|[graphql](addons/graphql) |+| | | | | | | |
|[info](addons/info) |+| | | | | | | |
|[jest](addons/jest) |+| | | | | |+| |
|[knobs](addons/knobs) |+|+|+|+|+|+|+|+|
|[links](addons/links) |+|+|+|+|+|+|+| |
|[notes](addons/notes) |+| |+|+|+|+|+| |
|[options](addons/options) |+|+|+|+|+|+|+| |
|[storyshots](addons/storyshots) |+|+|+|+| | |+| |
|[storysource](addons/storysource)|+| |+|+|+|+|+|+|
|[viewport](addons/viewport) |+| |+|+|+|+|+| |

View File

@ -72,9 +72,10 @@ For additional help, join us [in our Slack](https://now-examples-slackin-rrirkqo
- [React Native](app/react-native)
- [Vue](app/vue)
- [Angular](app/angular)
- [Polymer](app/polymer) <sup>release candidate</sup>
- [Polymer](app/polymer)
- [Mithril](app/mithril) <sup>alpha</sup>
- [Marko](app/marko) <sup>alpha</sup>
- [HTML](app/html) <sup>alpha</sup>
### Sub Projects
@ -85,7 +86,7 @@ For additional help, join us [in our Slack](https://now-examples-slackin-rrirkqo
- [a11y](addons/a11y/) - Test components for user accessibility in Storybook
- [actions](addons/actions/) - Log actions as users interact with components in the Storybook UI
- [background](addons/background/) - Let users choose backgrounds in the Storybook UI
- [backgrounds](addons/backgrounds/) - Let users choose backgrounds in the Storybook UI
- [centered](addons/centered/) - Center the alignment of your components within the Storybook UI
- [events](addons/events/) - Interactively fire events to components that respond to EventEmitter
- [graphql](addons/graphql/) - Query a GraphQL server within Storybook stories
@ -112,6 +113,7 @@ See [Addon / Framework Support Table](ADDONS_SUPPORT.md)
- [Polymer](https://storybooks-polymer.netlify.com/)
- [Mithril](https://storybooks-mithril.netlify.com/)
- [Marko](https://storybooks-marko.netlify.com/)
- [HTML](https://storybooks-html.netlify.com/)
### 3.4
- [React Official](https://release-3-4--storybooks-official.netlify.com)
@ -226,3 +228,7 @@ Become a sponsor and get your logo on our README on Github with a link to your s
<a href="https://opencollective.com/storybook/sponsor/27/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/storybook/sponsor/28/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/storybook/sponsor/29/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/29/avatar.svg"></a>
## License
[MIT](https://github.com/storybooks/storybook/blob/master/LICENSE)

1
addons/a11y/html.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/html');

View File

@ -28,10 +28,12 @@
"@storybook/addons": "4.0.0-alpha.4",
"@storybook/client-logger": "4.0.0-alpha.4",
"@storybook/components": "4.0.0-alpha.4",
"@storybook/core-events": "4.0.0-alpha.4",
"axe-core": "^3.0.2",
"babel-runtime": "^6.26.0",
"glamor": "^2.20.40",
"glamorous": "^4.12.5",
"global": "^4.3.2",
"prop-types": "^15.6.1"
},
"peerDependencies": {

View File

@ -1,6 +1,8 @@
import React, { Component } from 'react';
import addons from '@storybook/addons';
import { CHECK_EVENT_ID } from '../shared';
import Tabs from './Tabs';
import Report from './Report';
@ -26,11 +28,11 @@ class Panel extends Component {
}
componentDidMount() {
this.channel.on('addon:a11y:check', this.onUpdate);
this.channel.on(CHECK_EVENT_ID, this.onUpdate);
}
componentWillUnmount() {
this.channel.removeListener('addon:a11y:check', this.onUpdate);
this.channel.removeListener(CHECK_EVENT_ID, this.onUpdate);
}
onUpdate({ passes, violations }) {

View File

@ -1,6 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import addons from '@storybook/addons';
import { RERUN_EVENT_ID } from '../../shared';
import RerunButton from './RerunButton';
import Item from './Item';
@ -20,7 +23,7 @@ const styles = {
function onRerunClick() {
const channel = addons.getChannel();
channel.emit('addon:a11y:rerun');
channel.emit(RERUN_EVENT_ID);
}
const Report = ({ items, empty, passes }) => (

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import axe from 'axe-core';
import { logger } from '@storybook/client-logger';
import { CHECK_EVENT_ID, RERUN_EVENT_ID } from '../shared';
class WrapStory extends Component {
static propTypes = {
context: PropTypes.shape({}),
@ -25,13 +27,13 @@ class WrapStory extends Component {
componentDidMount() {
const { channel } = this.props;
channel.on('addon:a11y:rerun', this.runA11yCheck);
channel.on(RERUN_EVENT_ID, this.runA11yCheck);
this.runA11yCheck();
}
componentWillUnmount() {
const { channel } = this.props;
channel.removeListener('addon:a11y:rerun', this.runA11yCheck);
channel.removeListener(RERUN_EVENT_ID, this.runA11yCheck);
}
/* eslint-disable react/no-find-dom-node */
@ -42,7 +44,7 @@ class WrapStory extends Component {
if (wrapper !== null) {
axe.reset();
axe.configure(axeOptions);
axe.run(wrapper).then(results => channel.emit('addon:a11y:check', results), logger.error);
axe.run(wrapper).then(results => channel.emit(CHECK_EVENT_ID, results), logger.error);
}
}

35
addons/a11y/src/html.js Normal file
View File

@ -0,0 +1,35 @@
import { document, setTimeout } from 'global';
import axe from 'axe-core';
import addons from '@storybook/addons';
import Events from '@storybook/core-events';
import { logger } from '@storybook/client-logger';
import { CHECK_EVENT_ID, RERUN_EVENT_ID } from './shared';
let axeOptions = {};
export const configureA11y = (options = {}) => {
axeOptions = options;
};
const runA11yCheck = () => {
const channel = addons.getChannel();
const wrapper = document.getElementById('root');
axe.reset();
axe.configure(axeOptions);
axe.run(wrapper).then(results => channel.emit(CHECK_EVENT_ID, results), logger.error);
};
const a11ySubscription = () => {
const channel = addons.getChannel();
channel.on(RERUN_EVENT_ID, runA11yCheck);
return () => channel.removeListener(RERUN_EVENT_ID, runA11yCheck);
};
export const checkA11y = story => {
addons.getChannel().emit(Events.REGISTER_SUBSCRIPTION, a11ySubscription);
// We need to wait for rendering
setTimeout(runA11yCheck, 0);
return story();
};

View File

@ -1,6 +1,7 @@
// addons, panels and events get unique names using a prefix
const ADDON_ID = '@storybook/addon-a11y';
const PANEL_ID = `${ADDON_ID}/panel`;
const EVENT_ID = `${ADDON_ID}/event`;
const CHECK_EVENT_ID = `${ADDON_ID}/check`;
const RERUN_EVENT_ID = `${ADDON_ID}/rerun`;
export { ADDON_ID, PANEL_ID, EVENT_ID };
export { ADDON_ID, PANEL_ID, CHECK_EVENT_ID, RERUN_EVENT_ID };

View File

@ -55,10 +55,10 @@ import { actions } from '@storybook/addon-actions';
import Button from './button';
// This will lead to { onClick: action('onClick'), ... }
const eventsFromNames = actions('onClick', 'onDoubleClick');
const eventsFromNames = actions('onClick', 'onMouseOver');
// This will lead to { onClick: action('clicked'), ... }
const eventsFromObject = actions({ onClick: 'clicked', onDoubleClick: 'double clicked' });
const eventsFromObject = actions({ onClick: 'clicked', onMouseOver: 'hovered' });
storiesOf('Button', module)
.add('default view', () => <Button {...eventsFromNames}>Hello World!</Button>)
@ -123,3 +123,22 @@ action('my-action', {
|`depth`|Number|Configures the transfered depth of any logged objects.|`10`|
|`clearOnStoryChange`|Boolean|Flag whether to clear the action logger when switching away from the current story.|`true`|
|`limit`|Number|Limits the number of items logged in the action logger|`50`|
## withActions decorator
You can define action handles in a declarative way using `withActions` decorators. It accepts the same arguments as [`actions`](#multiple-actions)
Keys have `'<eventName> <selector>'` format, e.g. `'click .btn'`. Selector is optional. This can be used with any framework but is especially useful for `@storybook/html`.
```js
import { storiesOf } from '@storybook/html';
import { withActions } from '@storybook/addon-actions';
storiesOf('button', module)
// Log mousovers on entire story and clicks on .btn
.addDecorator(withActions('mouseover', 'click .btn'))
.add('with actions', () => `
<div>
Clicks on this button will be logged: <button class="btn" type="button">Button</button>
</div>
`);
```

View File

@ -22,11 +22,13 @@
"dependencies": {
"@storybook/addons": "4.0.0-alpha.4",
"@storybook/components": "4.0.0-alpha.4",
"@storybook/core-events": "4.0.0-alpha.4",
"babel-runtime": "^6.26.0",
"deep-equal": "^1.0.1",
"glamor": "^2.20.40",
"glamorous": "^4.12.5",
"global": "^4.3.2",
"lodash.isequal": "^4.5.0",
"make-error": "^1.3.4",
"prop-types": "^15.6.1",
"react-inspector": "^2.3.0",

View File

@ -1,8 +1,15 @@
import { action, actions, decorate, configureActions, decorateAction } from './preview';
import {
action,
actions,
decorate,
configureActions,
decorateAction,
withActions,
} from './preview';
// addons, panels and events get unique names using a prefix
export const ADDON_ID = 'storybook/actions';
export const PANEL_ID = `${ADDON_ID}/actions-panel`;
export const EVENT_ID = `${ADDON_ID}/action-event`;
export { action, actions, decorate, configureActions, decorateAction };
export { action, actions, decorate, configureActions, decorateAction, withActions };

View File

@ -1,5 +1,6 @@
import action from './action';
import actions from './actions';
import { createDecorator } from './withActions';
function applyDecorators(decorators, actionCallback) {
return (..._args) => {
@ -17,15 +18,17 @@ export function decorateAction(decorators) {
export function decorate(decorators) {
const decorated = decorateAction(decorators);
const decoratedActions = (...args) => {
const rawActions = actions(...args);
const actionsObject = {};
Object.keys(rawActions).forEach(name => {
actionsObject[name] = applyDecorators(decorators, rawActions[name]);
});
return actionsObject;
};
return {
action: decorated,
actions: (...args) => {
const rawActions = actions(...args);
const decoratedActions = {};
Object.keys(rawActions).forEach(name => {
decoratedActions[name] = applyDecorators(decorators, rawActions[name]);
});
return decoratedActions;
},
actions: decoratedActions,
withActions: createDecorator(decoratedActions),
};
}

View File

@ -2,3 +2,4 @@ export { default as action } from './action';
export { default as actions } from './actions';
export { configureActions } from './configureActions';
export { decorateAction, decorate } from './decorateAction';
export { default as withActions } from './withActions';

View File

@ -0,0 +1,64 @@
// Based on http://backbonejs.org/docs/backbone.html#section-164
import { document, Element } from 'global';
import isEqual from 'lodash.isequal';
import addons from '@storybook/addons';
import Events from '@storybook/core-events';
import actions from './actions';
let lastSubscription;
let lastArgs;
const delegateEventSplitter = /^(\S+)\s*(.*)$/;
const isIE = Element != null && !Element.prototype.matches;
const matchesMethod = isIE ? 'msMatchesSelector' : 'matches';
const root = document && document.getElementById('root');
const hasMatchInAncestry = (element, selector) => {
if (element[matchesMethod](selector)) {
return true;
}
const parent = element.parentElement;
if (!parent) {
return false;
}
return hasMatchInAncestry(parent, selector);
};
const createHandlers = (actionsFn, ...args) => {
const actionsObject = actionsFn(...args);
return Object.entries(actionsObject).map(([key, action]) => {
// eslint-disable-next-line no-unused-vars
const [_, eventName, selector] = key.match(delegateEventSplitter);
return {
eventName,
handler: e => {
if (!selector || hasMatchInAncestry(e.target, selector)) {
action(e);
}
},
};
});
};
const actionsSubscription = (...args) => {
if (!isEqual(args, lastArgs)) {
lastArgs = args;
const handlers = createHandlers(...args);
lastSubscription = () => {
handlers.forEach(({ eventName, handler }) => root.addEventListener(eventName, handler));
return () =>
handlers.forEach(({ eventName, handler }) => root.removeEventListener(eventName, handler));
};
}
return lastSubscription;
};
export const createDecorator = actionsFn => (...args) => story => {
addons.getChannel().emit(Events.REGISTER_SUBSCRIPTION, actionsSubscription(actionsFn, ...args));
return story();
};
export default createDecorator(actions);

View File

@ -0,0 +1 @@
module.exports = require('./dist/html');

View File

@ -25,6 +25,7 @@
},
"dependencies": {
"@storybook/addons": "4.0.0-alpha.4",
"@storybook/core-events": "4.0.0-alpha.4",
"babel-runtime": "^6.26.0",
"global": "^4.3.2",
"prop-types": "^15.6.1",

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import addons from '@storybook/addons';
import Events from './events';
import Swatch from './Swatch';
const storybookIframe = 'storybook-preview-iframe';
@ -88,7 +89,7 @@ export default class BackgroundPanel extends Component {
const { api } = this.props;
this.channel.on('background-set', backgrounds => {
this.channel.on(Events.SET, backgrounds => {
this.setState({ backgrounds });
const currentBackground = api.getQueryParam('background');
@ -100,7 +101,7 @@ export default class BackgroundPanel extends Component {
}
});
this.channel.on('background-unset', () => {
this.channel.on(Events.UNSET, () => {
this.setState({ backgrounds: [] });
this.updateIframe('none');
});

View File

@ -3,6 +3,7 @@ import { shallow, mount } from 'enzyme';
import EventEmitter from 'events';
import BackgroundPanel from '../BackgroundPanel';
import Events from '../events';
const backgrounds = [
{ name: 'black', value: '#000000' },
@ -49,7 +50,7 @@ describe('Background Panel', () => {
it('should set the query string', () => {
const SpiedChannel = new EventEmitter();
mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />);
SpiedChannel.emit('background-set', backgrounds);
SpiedChannel.emit(Events.SET, backgrounds);
expect(mockedApi.getQueryParam).toBeCalledWith('background');
});
@ -57,7 +58,7 @@ describe('Background Panel', () => {
it('should not unset the query string', () => {
const SpiedChannel = new EventEmitter();
mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />);
SpiedChannel.emit('background-unset', []);
SpiedChannel.emit(Events.UNSET, []);
expect(mockedApi.setQueryParams).not.toHaveBeenCalled();
});
@ -65,7 +66,7 @@ describe('Background Panel', () => {
it('should accept colors through channel and render the correct swatches with a default swatch', () => {
const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />);
SpiedChannel.emit('background-set', backgrounds);
SpiedChannel.emit(Events.SET, backgrounds);
expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds);
});
@ -75,7 +76,7 @@ describe('Background Panel', () => {
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />);
const [head, ...tail] = backgrounds;
const localBgs = [{ ...head, default: true }, ...tail];
SpiedChannel.emit('background-set', localBgs);
SpiedChannel.emit(Events.SET, localBgs);
expect(backgroundPanel.state('backgrounds')).toEqual(localBgs);
backgroundPanel.setState({ backgrounds: localBgs }); // force re-render
@ -93,7 +94,7 @@ describe('Background Panel', () => {
SpiedChannel.on('background', bg => {
expect(bg).toBe(second.value);
});
SpiedChannel.emit('background-set', localBgs);
SpiedChannel.emit(Events.SET, localBgs);
expect(backgroundPanel.state('backgrounds')).toEqual(localBgs);
backgroundPanel.setState({ backgrounds: localBgs }); // force re-render
@ -106,12 +107,12 @@ describe('Background Panel', () => {
it('should unset all swatches on receiving the background-unset message', () => {
const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />);
SpiedChannel.emit('background-set', backgrounds);
SpiedChannel.emit(Events.SET, backgrounds);
expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds);
backgroundPanel.setState({ backgrounds }); // force re-render
SpiedChannel.emit('background-unset');
SpiedChannel.emit(Events.UNSET);
expect(backgroundPanel.state('backgrounds')).toHaveLength(0);
});

View File

@ -1,9 +1,9 @@
import React from 'react';
import { shallow } from 'enzyme';
import EventEmitter from 'events';
import { BackgroundDecorator } from '../index';
const EventEmitter = require('events');
import Events from '../events';
const testStory = () => () => <p>Hello World!</p>;
@ -23,7 +23,7 @@ describe('Background Decorator', () => {
);
const spy = jest.fn();
SpiedChannel.on('background-unset', spy);
SpiedChannel.on(Events.UNSET, spy);
backgroundDecorator.unmount();
@ -33,7 +33,7 @@ describe('Background Decorator', () => {
it('should send background-set event when the component mounts', () => {
const SpiedChannel = new EventEmitter();
const spy = jest.fn();
SpiedChannel.on('background-set', spy);
SpiedChannel.on(Events.SET, spy);
shallow(<BackgroundDecorator story={testStory} channel={SpiedChannel} />);

View File

@ -1,6 +1,8 @@
import Vue from 'vue';
import { vueHandler } from '../vue';
import Events from '../events';
describe('Vue handler', () => {
it('Returns a component with a created function', () => {
const testChannel = { emit: jest.fn() };
@ -37,6 +39,6 @@ describe('Vue handler', () => {
new Vue(vueHandler(testChannel, testBackground)(testStory, testContext)).$mount();
expect(testChannel.emit).toHaveBeenCalledTimes(1);
expect(testChannel.emit).toHaveBeenCalledWith('background-set', expect.any(Array));
expect(testChannel.emit).toHaveBeenCalledWith(Events.SET, expect.any(Array));
});
});

View File

@ -0,0 +1,6 @@
const ADDON_ID = 'backgrounds';
export default {
SET: `${ADDON_ID}:set`,
UNSET: `${ADDON_ID}:unset`,
};

View File

@ -0,0 +1,17 @@
import addons from '@storybook/addons';
import CoreEvents from '@storybook/core-events';
import Events from './events';
const subscription = () => () => addons.getChannel().emit(Events.UNSET);
let prevBackgrounds;
export default backgrounds => story => {
if (prevBackgrounds !== backgrounds) {
addons.getChannel().emit(Events.SET, backgrounds);
prevBackgrounds = backgrounds;
}
addons.getChannel().emit(CoreEvents.REGISTER_SUBSCRIPTION, subscription);
return story();
};

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import addons from '@storybook/addons';
import Events from './events';
export class BackgroundDecorator extends React.Component {
constructor(props) {
super(props);
@ -21,11 +23,11 @@ export class BackgroundDecorator extends React.Component {
}
componentDidMount() {
this.channel.emit('background-set', this.props.backgrounds);
this.channel.emit(Events.SET, this.props.backgrounds);
}
componentWillUnmount() {
this.channel.emit('background-unset');
this.channel.emit(Events.UNSET);
}
render() {

View File

@ -5,6 +5,8 @@ import m from 'mithril';
import addons from '@storybook/addons';
import Events from './events';
export class BackgroundDecorator {
constructor(vnode) {
this.props = vnode.attrs;
@ -22,11 +24,11 @@ export class BackgroundDecorator {
}
oncreate() {
this.channel.emit('background-set', this.props.backgrounds);
this.channel.emit(Events.SET, this.props.backgrounds);
}
onremove() {
this.channel.emit('background-unset');
this.channel.emit(Events.UNSET);
}
view() {

View File

@ -1,5 +1,7 @@
import addons from '@storybook/addons';
import Events from './events';
export const vueHandler = (channel, backgrounds) => (getStory, context) => ({
data() {
return {
@ -14,11 +16,11 @@ export const vueHandler = (channel, backgrounds) => (getStory, context) => ({
},
created() {
channel.emit('background-set', backgrounds);
channel.emit(Events.SET, backgrounds);
},
beforeDestroy() {
channel.emit('background-unset');
channel.emit(Events.UNSET);
},
});

1
addons/centered/html.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/html');

View File

@ -0,0 +1,46 @@
import { document, Node } from 'global';
import styles from './styles';
const INNER_ID = 'sb-addon-centered-inner';
const WRAPPER_ID = 'sb-addon-centered-wrapper';
function getOrCreate(id, style) {
const elementOnDom = document.getElementById(id);
if (elementOnDom) {
return elementOnDom;
}
const element = document.createElement('div');
element.setAttribute('id', id);
Object.assign(element.style, style);
return element;
}
function getInnerDiv() {
return getOrCreate(INNER_ID, styles.innerStyle);
}
function getWrapperDiv() {
return getOrCreate(WRAPPER_ID, styles.style);
}
export default function(storyFn) {
const inner = getInnerDiv();
const wrapper = getWrapperDiv();
wrapper.appendChild(inner);
const component = storyFn();
if (typeof component === 'string') {
inner.innerHTML = component;
} else if (component instanceof Node) {
inner.innerHTML = '';
inner.appendChild(component);
} else {
return component;
}
return wrapper;
}

View File

@ -2,28 +2,13 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import m from 'mithril';
const style = {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'auto',
};
const innerStyle = {
margin: 'auto',
};
import styles from './styles';
export default function(storyFn) {
return {
view: () => (
<div style={style}>
<div style={innerStyle}>{m(storyFn())}</div>
<div style={styles.style}>
<div style={styles.innerStyle}>{m(storyFn())}</div>
</div>
),
};

View File

@ -1,26 +1,11 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import React from 'react';
const style = {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'auto',
};
const innerStyle = {
margin: 'auto',
};
import styles from './styles';
export default function(storyFn) {
return (
<div style={style}>
<div style={innerStyle}>{storyFn()}</div>
<div style={styles.style}>
<div style={styles.innerStyle}>{storyFn()}</div>
</div>
);
}

View File

@ -0,0 +1,18 @@
const styles = {
style: {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'auto',
},
innerStyle: {
margin: 'auto',
},
};
export default styles;

View File

@ -1,3 +1,5 @@
import styles from './styles';
export default function() {
return {
template: `
@ -8,22 +10,7 @@ export default function() {
</div>
`,
data() {
return {
style: {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'auto',
},
innerStyle: {
margin: 'auto',
},
};
return styles;
},
};
}

1
addons/events/html.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/html');

View File

@ -20,6 +20,7 @@
},
"dependencies": {
"@storybook/addons": "4.0.0-alpha.4",
"@storybook/core-events": "4.0.0-alpha.4",
"babel-runtime": "^6.26.0",
"format-json": "^1.0.3",
"prop-types": "^15.6.1",

27
addons/events/src/html.js Normal file
View File

@ -0,0 +1,27 @@
import addons from '@storybook/addons';
import CoreEvents from '@storybook/core-events';
import { EVENTS } from './constants';
let prevEvents;
let currentEmit;
const onEmit = event => {
currentEmit(event.name, event.payload);
};
const subscription = () => {
const channel = addons.getChannel();
channel.on(EVENTS.EMIT, onEmit);
return () => channel.removeListener(EVENTS.EMIT, onEmit);
};
export default ({ emit, events }) => story => {
if (prevEvents !== events) {
addons.getChannel().emit(EVENTS.ADD, events);
prevEvents = events;
}
currentEmit = emit;
addons.getChannel().emit(CoreEvents.REGISTER_SUBSCRIPTION, subscription);
return story();
};

View File

@ -16,9 +16,9 @@ You can also use Knobs as a dynamic variable inside stories in [Storybook](https
This is how Knobs look like:
[![Storybook Knobs Demo](docs/storybook-knobs-example.png)](https://git.io/vXdhZ)
[![Storybook Knobs Demo](docs/storybook-knobs-example.png)](https://storybooks-official.netlify.com/?knob-Dollars=12.5&knob-Name=Storyteller&knob-Years%20in%20NY=9&knob-background=%23ffff00&knob-Age=70&knob-Items%5B0%5D=Laptop&knob-Items%5B1%5D=Book&knob-Items%5B2%5D=Whiskey&knob-Other%20Fruit=lime&knob-Birthday=1484870400000&knob-Nice=true&knob-Styles=%7B%22border%22%3A%223px%20solid%20%23ff00ff%22%2C%22padding%22%3A%2210px%22%7D&knob-Fruit=apple&selectedKind=Addons%7CKnobs.withKnobs&selectedStory=tweaks%20static%20values&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybooks%2Fstorybook-addon-knobs)
> Checkout the above [Live Storybook](https://storybooks-official.netlify.com/) or [watch this video](https://www.youtube.com/watch?v=kopW6vzs9dg&feature=youtu.be).
> Checkout the above [Live Storybook](https://storybooks-official.netlify.com/?knob-Dollars=12.5&knob-Name=Storyteller&knob-Years%20in%20NY=9&knob-background=%23ffff00&knob-Age=70&knob-Items%5B0%5D=Laptop&knob-Items%5B1%5D=Book&knob-Items%5B2%5D=Whiskey&knob-Other%20Fruit=lime&knob-Birthday=1484870400000&knob-Nice=true&knob-Styles=%7B%22border%22%3A%223px%20solid%20%23ff00ff%22%2C%22padding%22%3A%2210px%22%7D&knob-Fruit=apple&selectedKind=Addons%7CKnobs.withKnobs&selectedStory=tweaks%20static%20values&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybooks%2Fstorybook-addon-knobs) or [watch this video](https://www.youtube.com/watch?v=kopW6vzs9dg&feature=youtu.be).
## Getting Started
@ -50,7 +50,7 @@ stories.addDecorator(withKnobs);
// Knobs for React props
stories.add('with a button', () => (
<button disabled={boolean('Disabled', false)} >
{text('Label', 'Hello Button')}
{text('Label', 'Hello Storybook')}
</button>
));
@ -77,17 +77,17 @@ const stories = storiesOf('Storybook Knobs', module);
stories.addDecorator(withKnobs);
// Knobs for Angular props
stories.add('with text', () => ({
stories.add('with a button', () => ({
component: Button,
props: {
text: text('text', 'Hello Button'), // The first param of the knob function has to be exactly the same as the component input.
text: text('text', 'Hello Storybook'), // The first param of the knob function has to be exactly the same as the component input.
},
}));
```
Categorize your knobs by assigning them a `groupId`. When a `groupId` exists, tabs will appear in the knobs storybook panel to filter between the groups. Knobs without a `groupId` are automatically categorized into the `ALL` group.
```
```js
// Knob assigned a groupId.
stories.add('as dynamic variables', () => {
const groupId = 'GROUP-ID1'
@ -130,13 +130,6 @@ You can see your Knobs in a Storybook panel as shown below.
![](docs/demo.png)
### Additional Links
- Introduction blog post.
- Watch this video on how to use knobs
- [Live Storybook with Knobs](https://goo.gl/uX9WLf)
- Have a look at this [sample Storybook repo](https://github.com/kadira-samples/storybook-knobs-example).
## Available Knobs
These are the knobs available for you to use. You can import these Knobs from the `@storybook/addon-knobs` module.

1
addons/knobs/html.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/html');

View File

@ -15,6 +15,7 @@
"dependencies": {
"@storybook/addons": "4.0.0-alpha.4",
"@storybook/components": "4.0.0-alpha.4",
"@storybook/core-events": "4.0.0-alpha.4",
"babel-runtime": "^6.26.0",
"deep-equal": "^1.0.1",
"escape-html": "^1.0.3",

View File

@ -0,0 +1,32 @@
import registerKnobs from './registerKnobs';
import {
knob,
text,
boolean,
number,
color,
object,
array,
date,
select,
files,
manager,
makeDecorators,
} from '../base';
export { knob, text, boolean, number, color, object, array, date, select, files };
export function button(name, callback) {
return manager.knob(name, { type: 'button', value: Date.now(), callback, hideLabel: true });
}
function prepareComponent({ getStory, context }) {
registerKnobs();
return getStory(context);
}
export const htmlHandler = () => getStory => context => prepareComponent({ getStory, context });
export const { withKnobs, withKnobsOptions } = makeDecorators(htmlHandler, { escapeHTML: true });

View File

@ -0,0 +1,64 @@
import addons from '@storybook/addons';
import Events from '@storybook/core-events';
import { manager } from '../base';
const { knobStore } = manager;
function forceReRender() {
addons.getChannel().emit(Events.FORCE_RE_RENDER);
}
function setPaneKnobs(timestamp = +new Date()) {
const channel = addons.getChannel();
channel.emit('addon:knobs:setKnobs', { knobs: knobStore.getAll(), timestamp });
}
function knobChanged(change) {
const { name, value } = change;
// Update the related knob and it's value.
const knobOptions = knobStore.get(name);
knobOptions.value = value;
knobStore.markAllUnused();
forceReRender();
}
function knobClicked(clicked) {
const knobOptions = knobStore.get(clicked.name);
knobOptions.callback();
}
function resetKnobs() {
knobStore.reset();
forceReRender();
const channel = addons.getChannel();
setPaneKnobs(channel, knobStore, false);
}
function disconnectCallbacks() {
const channel = addons.getChannel();
channel.removeListener('addon:knobs:knobChange', knobChanged);
channel.removeListener('addon:knobs:knobClick', knobClicked);
channel.removeListener('addon:knobs:reset', resetKnobs);
knobStore.unsubscribe(setPaneKnobs);
}
function connectCallbacks() {
const channel = addons.getChannel();
channel.on('addon:knobs:knobChange', knobChanged);
channel.on('addon:knobs:knobClick', knobClicked);
channel.on('addon:knobs:reset', resetKnobs);
knobStore.subscribe(setPaneKnobs);
return disconnectCallbacks;
}
function registerKnobs() {
addons.getChannel().emit(Events.REGISTER_SUBSCRIPTION, connectCallbacks);
}
export default registerKnobs;

View File

@ -95,6 +95,22 @@ storiesOf('Href', module)
});
```
## withLinks decorator
`withLinks` decorator enables a declarative way of defining story links, using data attributes.
Here is an example in React, but it works with any framework:
```js
import { storiesOf } from '@storybook/react'
import { withLinks } from '@storybook/addon-links'
storiesOf('Button', module)
.addDecorator(withLinks)
.add('First', () => (
<button data-sb-kind="OtherKind" data-sb-story="OtherStory">Go to "OtherStory"</button>
))
```
## LinkTo component (React only)
One possible way of using `hrefTo` is to create a component that uses native `a` element, but prevents page reloads on plain left click, so that one can still use default browser methods to open link in new tab.

View File

@ -22,6 +22,7 @@
"dependencies": {
"@storybook/addons": "4.0.0-alpha.4",
"@storybook/components": "4.0.0-alpha.4",
"@storybook/core-events": "4.0.0-alpha.4",
"babel-runtime": "^6.26.0",
"global": "^4.3.2",
"prop-types": "^15.6.1"

View File

@ -3,7 +3,7 @@ export const EVENT_ID = `${ADDON_ID}/link-event`;
export const REQUEST_HREF_EVENT_ID = `${ADDON_ID}/request-href-event`;
export const RECEIVE_HREF_EVENT_ID = `${ADDON_ID}/receive-href-event`;
export { linkTo, hrefTo } from './preview';
export { linkTo, hrefTo, withLinks } from './preview';
let hasWarned = false;

View File

@ -1,4 +1,7 @@
import { document } from 'global';
import addons from '@storybook/addons';
import Events from '@storybook/core-events';
import { EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from './';
export const openLink = params => addons.getChannel().emit(EVENT_ID, params);
@ -19,3 +22,21 @@ export const hrefTo = (kind, story) =>
channel.on(RECEIVE_HREF_EVENT_ID, resolve);
channel.emit(REQUEST_HREF_EVENT_ID, { kind, story });
});
const linksListener = e => {
const { sbKind, sbStory } = e.target.dataset;
if (sbKind || sbStory) {
e.preventDefault();
linkTo(sbKind, sbStory)();
}
};
const linkSubscribtion = () => {
document.addEventListener('click', linksListener);
return () => document.removeEventListener('click', linksListener);
};
export const withLinks = story => {
addons.getChannel().emit(Events.REGISTER_SUBSCRIPTION, linkSubscribtion);
return story();
};

View File

@ -243,6 +243,18 @@ initStoryshots({suite: 'Image storyshots', test: imageSnapshot({storybookUrl: 'h
`getScreenshotOptions` receives an object `{ context: {kind, story}, url}`. _kind_ is the kind of the story and the _story_ its name. _url_ is the URL the browser will use to screenshot.
### Specifying custom Chrome executable path (puppeteer API)
You might use `chromeExecutablePath` to specify the path to a different version of Chrome, without downloading Chromium. Will be passed to [Runs a bundled version of Chromium](https://github.com/GoogleChrome/puppeteer#default-runtime-settings)
```js
import initStoryshots, { imageSnapshot } from '@storybook/addon-storyshots';
const chromeExecutablePath = '/usr/local/bin/chrome';
initStoryshots({suite: 'Image storyshots', test: imageSnapshot({storybookUrl: 'http://localhost:6006', chromeExecutablePath})});
```
### Integrate image storyshots with regular app
You may want to use another Jest project to run your image snapshots as they require more resources: Chrome and Storybook built/served.
@ -422,7 +434,7 @@ Like the default, but allows you to specify a set of options for the test render
### `multiSnapshotWithOptions(options)`
Like `snapshotWithOptions`, but generate a separate snapshot file for each stories file rather than a single monolithic file (as is the convention in Jest). This makes it dramatically easier to review changes.
Like `snapshotWithOptions`, but generate a separate snapshot file for each stories file rather than a single monolithic file (as is the convention in Jest). This makes it dramatically easier to review changes. If you'd like the benefit of separate snapshot files, but don't have custom options to pass, simply pass an empty object.
#### integrityOptions
This option is useful when running test with `multiSnapshotWithOptions(options)` in order to track snapshots are matching the stories. (disabled by default).

View File

@ -2,8 +2,9 @@ import loaderReact from './react/loader';
import loaderRn from './rn/loader';
import loaderAngular from './angular/loader';
import loaderVue from './vue/loader';
import loaderHTML from './html/loader';
const loaders = [loaderReact, loaderAngular, loaderRn, loaderVue];
const loaders = [loaderReact, loaderAngular, loaderRn, loaderVue, loaderHTML];
function loadFramework(options) {
const loader = loaders.find(frameworkLoader => frameworkLoader.test(options));

View File

@ -0,0 +1,32 @@
import global from 'global';
import runWithRequireContext from '../require_context';
import loadConfig from '../config-loader';
function test(options) {
return options.framework === 'html';
}
function load(options) {
global.STORYBOOK_ENV = 'html';
const { content, contextOpts } = loadConfig({
configDirPath: options.configPath,
babelConfigPath: '@storybook/html/dist/server/config/babel',
});
runWithRequireContext(content, contextOpts);
return {
framework: 'html',
renderTree: require.requireActual('./renderTree').default,
renderShallowTree: () => {
throw new Error('Shallow renderer is not supported for HTML');
},
storybook: require.requireActual('@storybook/html'),
};
}
export default {
load,
test,
};

View File

@ -0,0 +1,20 @@
import { document, Node } from 'global';
function getRenderedTree(story, context) {
const component = story.render(context);
if (component instanceof Node) {
return component;
}
const section = document.createElement('section');
section.innerHTML = component;
if (section.childElementCount > 1) {
return section;
}
return section.firstChild;
}
export default getRenderedTree;

View File

@ -11,6 +11,7 @@ const noop = () => {};
const defaultConfig = {
storybookUrl: 'http://localhost:6006',
chromeExecutablePath: undefined,
getMatchOptions: noop,
getScreenshotOptions: defaultScreenshotOptions,
beforeScreenshot: noop,
@ -20,6 +21,7 @@ const defaultConfig = {
export const imageSnapshot = (customConfig = {}) => {
const {
storybookUrl,
chromeExecutablePath,
getMatchOptions,
getScreenshotOptions,
beforeScreenshot,
@ -70,7 +72,10 @@ export const imageSnapshot = (customConfig = {}) => {
testFn.beforeAll = () =>
puppeteer
// add some options "no-sandbox" to make it work properly on some Linux systems as proposed here: https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322851507
.launch({ args: ['--no-sandbox ', '--disable-setuid-sandbox'] })
.launch({
args: ['--no-sandbox ', '--disable-setuid-sandbox'],
executablePath: chromeExecutablePath,
})
.then(b => {
browser = b;
})

View File

@ -1,6 +1,6 @@
import { renderNgApp } from './angular/helpers';
export default function render({ story, showMain }) {
renderNgApp(story);
showMain();
renderNgApp(story);
}

3
app/html/.npmignore Normal file
View File

@ -0,0 +1,3 @@
docs
src
.babelrc

26
app/html/README.md Normal file
View File

@ -0,0 +1,26 @@
# Storybook for HTML <sup>alpha</sup>
* * *
Storybook for HTML is a UI development environment for your plain HTML snippets.
With it, you can visualize different states of your UI components and develop them interactively.
![Storybook Screenshot](https://github.com/storybooks/storybook/blob/master/app/html/docs/demo.png)
Storybook runs outside of your app.
So you can develop UI components in isolation without worrying about app specific dependencies and requirements.
## Getting Started
```sh
npm i -g @storybook/cli
cd my-app
getstorybook --html
```
For more information visit: [storybook.js.org](https://storybook.js.org)
* * *
Storybook also comes with a lot of [addons](https://storybook.js.org/addons/introduction) and a great API to customize as you wish.
You can also build a [static version](https://storybook.js.org/basics/exporting-storybook) of your storybook and deploy it anywhere you want.

3
app/html/bin/build.js Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../dist/server/build');

3
app/html/bin/index.js Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../dist/server');

BIN
app/html/docs/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

50
app/html/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "@storybook/html",
"version": "4.0.0-alpha.4",
"description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.",
"homepage": "https://github.com/storybooks/storybook/tree/master/apps/html",
"bugs": {
"url": "https://github.com/storybooks/storybook/issues"
},
"license": "MIT",
"main": "dist/client/index.js",
"bin": {
"build-storybook": "./bin/build.js",
"start-storybook": "./bin/index.js",
"storybook-server": "./bin/index.js"
},
"repository": {
"type": "git",
"url": "https://github.com/storybooks/storybook.git"
},
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/core": "4.0.0-alpha.4",
"@storybook/react-dev-utils": "^5.0.0",
"airbnb-js-shims": "^1.4.1",
"babel-loader": "^7.1.4",
"babel-plugin-macros": "^2.2.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-preset-minify": "^0.4.0",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0",
"case-sensitive-paths-webpack-plugin": "^2.1.2",
"common-tags": "^1.4.0",
"core-js": "^2.5.5",
"dotenv-webpack": "^1.5.5",
"global": "^4.3.2",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"raw-loader": "^0.5.1",
"webpack": "^4.6.0",
"webpack-hot-middleware": "^2.22.1"
},
"peerDependencies": {
"babel-core": "^6.26.0 || ^7.0.0-0",
"babel-runtime": ">=6.0.0"
}
}

View File

@ -0,0 +1,9 @@
export {
storiesOf,
setAddon,
addDecorator,
addParameters,
configure,
getStorybook,
forceReRender,
} from './preview';

View File

@ -0,0 +1,17 @@
import { start } from '@storybook/core/client';
import render from './render';
const { clientApi, configApi, forceReRender } = start(render);
export const {
storiesOf,
setAddon,
addDecorator,
addParameters,
clearDecorators,
getStorybook,
} = clientApi;
export const { configure } = configApi;
export { forceReRender };

View File

@ -0,0 +1,24 @@
import { document, Node } from 'global';
import { stripIndents } from 'common-tags';
const rootElement = document.getElementById('root');
export default function renderMain({ story, selectedKind, selectedStory, showMain, showError }) {
const component = story();
showMain();
if (typeof component === 'string') {
rootElement.innerHTML = component;
} else if (component instanceof Node) {
rootElement.innerHTML = '';
rootElement.appendChild(component);
} else {
showError({
title: `Expecting an HTML snippet or DOM node from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Did you forget to return the HTML snippet from the story?
Use "() => <your snippet or node>" or when defining the story.
`,
});
}
}

12
app/html/src/server/build.js Executable file
View File

@ -0,0 +1,12 @@
import { buildStatic } from '@storybook/core/server';
import path from 'path';
import packageJson from '../../package.json';
import getBaseConfig from './config/webpack.config.prod';
import loadConfig from './config';
buildStatic({
packageJson,
getBaseConfig,
loadConfig,
defaultFavIcon: path.resolve(__dirname, 'public/favicon.ico'),
});

View File

@ -0,0 +1,8 @@
import { configLoaderCreator } from '@storybook/core/server';
import defaultConfig from './config/babel';
const configLoader = configLoaderCreator({
defaultBabelConfig: defaultConfig,
});
export default configLoader;

View File

@ -0,0 +1,28 @@
module.exports = {
// Don't try to find .babelrc because we want to force this configuration.
babelrc: false,
presets: [
[
require.resolve('babel-preset-env'),
{
targets: {
browsers: ['last 2 versions', 'safari >= 7'],
},
modules: process.env.NODE_ENV === 'test' ? 'commonjs' : false,
},
],
require.resolve('babel-preset-stage-0'),
],
plugins: [
require.resolve('babel-plugin-macros'),
require.resolve('babel-plugin-transform-regenerator'),
[
require.resolve('babel-plugin-transform-runtime'),
{
helpers: true,
polyfill: true,
regenerator: true,
},
],
],
};

View File

@ -0,0 +1,33 @@
module.exports = {
// Don't try to find .babelrc because we want to force this configuration.
babelrc: false,
presets: [
[
require.resolve('babel-preset-env'),
{
targets: {
browsers: ['last 2 versions', 'safari >= 7'],
},
modules: false,
},
],
require.resolve('babel-preset-stage-0'),
[
require.resolve('babel-preset-minify'),
{
mangle: false,
},
],
],
plugins: [
require.resolve('babel-plugin-transform-regenerator'),
[
require.resolve('babel-plugin-transform-runtime'),
{
helpers: true,
polyfill: true,
regenerator: true,
},
],
],
};

View File

@ -0,0 +1,4 @@
import { window } from 'global';
window.STORYBOOK_REACT_CLASSES = {};
window.STORYBOOK_ENV = 'HTML';

View File

@ -0,0 +1,3 @@
import 'core-js/es6/symbol';
import 'core-js/fn/array/iterator';
import 'airbnb-js-shims';

View File

@ -0,0 +1,35 @@
import path from 'path';
export const includePaths = [path.resolve('./')];
export const excludePaths = [path.resolve('node_modules')];
export const nodeModulesPaths = path.resolve('./node_modules');
export const nodePaths = (process.env.NODE_PATH || '')
.split(process.platform === 'win32' ? ';' : ':')
.filter(Boolean)
.map(p => path.resolve('./', p));
// Load environment variables starts with STORYBOOK_ to the client side.
export function loadEnv(options = {}) {
const defaultNodeEnv = options.production ? 'production' : 'development';
const env = {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || defaultNodeEnv),
// This is to support CRA's public folder feature.
// In production we set this to dot(.) to allow the browser to access these assests
// even when deployed inside a subpath. (like in GitHub pages)
// In development this is just empty as we always serves from the root.
PUBLIC_URL: JSON.stringify(options.production ? '.' : ''),
};
Object.keys(process.env)
.filter(name => /^STORYBOOK_/.test(name))
.forEach(name => {
env[name] = JSON.stringify(process.env[name]);
});
return {
'process.env': env,
};
}

View File

@ -0,0 +1,106 @@
import path from 'path';
import webpack from 'webpack';
import Dotenv from 'dotenv-webpack';
import InterpolateHtmlPlugin from '@storybook/react-dev-utils/InterpolateHtmlPlugin';
import WatchMissingNodeModulesPlugin from '@storybook/react-dev-utils/WatchMissingNodeModulesPlugin';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import {
managerPath,
getPreviewHeadHtml,
getManagerHeadHtml,
indexHtmlPath,
iframeHtmlPath,
} from '@storybook/core/server';
import { includePaths, excludePaths, nodeModulesPaths, loadEnv, nodePaths } from './utils';
import babelLoaderConfig from './babel';
import { version } from '../../../package.json';
export default function(configDir, quiet) {
const config = {
mode: 'development',
devtool: 'cheap-module-source-map',
entry: {
manager: [require.resolve('./polyfills'), managerPath],
preview: [
require.resolve('./polyfills'),
require.resolve('./globals'),
`${require.resolve('webpack-hot-middleware/client')}?reload=true`,
],
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'static/[name].bundle.js',
publicPath: '/',
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
chunks: ['manager'],
chunksSortMode: 'none',
data: {
managerHead: getManagerHeadHtml(configDir),
version,
},
template: indexHtmlPath,
}),
new HtmlWebpackPlugin({
filename: 'iframe.html',
excludeChunks: ['manager'],
chunksSortMode: 'none',
data: {
previewHead: getPreviewHeadHtml(configDir),
},
template: iframeHtmlPath,
}),
new InterpolateHtmlPlugin(process.env),
new webpack.DefinePlugin(loadEnv()),
new webpack.HotModuleReplacementPlugin(),
new CaseSensitivePathsPlugin(),
new WatchMissingNodeModulesPlugin(nodeModulesPaths),
quiet ? null : new webpack.ProgressPlugin(),
new Dotenv({ silent: true }),
].filter(Boolean),
module: {
rules: [
{
test: /\.js$/,
loader: require.resolve('babel-loader'),
query: babelLoaderConfig,
include: includePaths,
exclude: excludePaths,
},
{
test: /\.html$/,
use: [
{
loader: require.resolve('html-loader'),
},
],
},
{
test: /\.md$/,
use: [
{
loader: require.resolve('raw-loader'),
},
],
},
],
},
resolve: {
// Since we ship with json-loader always, it's better to move extensions to here
// from the default config.
extensions: ['.js', '.json'],
// Add support to NODE_PATH. With this we could avoid relative path imports.
// Based on this CRA feature: https://github.com/facebookincubator/create-react-app/issues/253
modules: ['node_modules'].concat(nodePaths),
},
performance: {
hints: false,
},
};
return config;
}

View File

@ -0,0 +1,108 @@
import webpack from 'webpack';
import Dotenv from 'dotenv-webpack';
import InterpolateHtmlPlugin from '@storybook/react-dev-utils/InterpolateHtmlPlugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import {
managerPath,
getPreviewHeadHtml,
getManagerHeadHtml,
indexHtmlPath,
iframeHtmlPath,
} from '@storybook/core/server';
import babelLoaderConfig from './babel.prod';
import { includePaths, excludePaths, loadEnv, nodePaths } from './utils';
import { version } from '../../../package.json';
export default function(configDir) {
const entries = {
preview: [require.resolve('./polyfills'), require.resolve('./globals')],
manager: [require.resolve('./polyfills'), managerPath],
};
const config = {
mode: 'production',
bail: true,
devtool: '#cheap-module-source-map',
entry: entries,
output: {
filename: 'static/[name].[chunkhash].bundle.js',
// Here we set the publicPath to ''.
// This allows us to deploy storybook into subpaths like GitHub pages.
// This works with css and image loaders too.
// This is working for storybook since, we don't use pushState urls and
// relative URLs works always.
publicPath: '',
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
chunks: ['manager', 'runtime~manager'],
chunksSortMode: 'none',
data: {
managerHead: getManagerHeadHtml(configDir),
version,
},
template: indexHtmlPath,
}),
new HtmlWebpackPlugin({
filename: 'iframe.html',
excludeChunks: ['manager', 'runtime~manager'],
chunksSortMode: 'none',
data: {
previewHead: getPreviewHeadHtml(configDir),
},
template: iframeHtmlPath,
}),
new InterpolateHtmlPlugin(process.env),
new webpack.DefinePlugin(loadEnv({ production: true })),
new Dotenv({ silent: true }),
],
module: {
rules: [
{
test: /\.js$/,
loader: require.resolve('babel-loader'),
query: babelLoaderConfig,
include: includePaths,
exclude: excludePaths,
},
{
test: /\.html$/,
use: [
{
loader: require.resolve('html-loader'),
},
],
},
{
test: /\.md$/,
use: [
{
loader: require.resolve('raw-loader'),
},
],
},
],
},
resolve: {
// Since we ship with json-loader always, it's better to move extensions to here
// from the default config.
extensions: ['.js', '.json'],
// Add support to NODE_PATH. With this we could avoid relative path imports.
// Based on this CRA feature: https://github.com/facebookincubator/create-react-app/issues/253
modules: ['node_modules'].concat(nodePaths),
},
optimization: {
// Automatically split vendor and commons for preview bundle
// https://twitter.com/wSokra/status/969633336732905474
splitChunks: {
chunks: chunk => chunk.name !== 'manager',
},
// Keep the runtime chunk seperated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
runtimeChunk: true,
},
};
return config;
}

12
app/html/src/server/index.js Executable file
View File

@ -0,0 +1,12 @@
import { buildDev } from '@storybook/core/server';
import path from 'path';
import packageJson from '../../package.json';
import getBaseConfig from './config/webpack.config';
import loadConfig from './config';
buildDev({
packageJson,
getBaseConfig,
loadConfig,
defaultFavIcon: path.resolve(__dirname, 'public/favicon.ico'),
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -21,6 +21,6 @@ export default function renderMain({ story, selectedKind, selectedStory, showMai
return;
}
m.mount(rootEl, { view: () => m(element) });
showMain();
m.mount(rootEl, { view: () => m(element) });
}

View File

@ -9,14 +9,16 @@ export default function renderMain({ story, selectedKind, selectedStory, showMai
if (!component) {
showError({
message: `Expecting a Polymer component from the story: "${selectedStory}" of "${selectedKind}".`,
stack: stripIndents`
title: `Expecting a Polymer component from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Did you forget to return the Polymer component from the story?
Use "() => '&lt;your-component-name&gt;&lt;/your-component-name\&gt;'" when defining the story.
`,
});
return;
}
showMain();
if (typeof component === 'string') {
rootElement.innerHTML = component;
} else if (component instanceof TemplateResult) {
@ -28,5 +30,4 @@ export default function renderMain({ story, selectedKind, selectedStory, showMai
rootElement.innerHTML = '';
rootElement.appendChild(component);
}
showMain();
}

View File

@ -43,6 +43,6 @@ export default function renderMain({ story, selectedKind, selectedStory, showMai
// This could leads to issues like below:
// https://github.com/storybooks/react-storybook/issues/81
ReactDOM.unmountComponentAtNode(rootEl);
render(element, rootEl);
showMain();
render(element, rootEl);
}

View File

@ -23,8 +23,8 @@ export default function render({
if (!component) {
showError({
message: `Expecting a Vue component from the story: "${selectedStory}" of "${selectedKind}".`,
stack: stripIndents`
title: `Expecting a Vue component from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Did you forget to return the Vue component from the story?
Use "() => ({ template: '<my-comp></my-comp>' })" or "() => ({ components: MyComp, template: '<my-comp></my-comp>' })" when defining the story.
`,
@ -32,11 +32,11 @@ export default function render({
return;
}
showMain();
renderRoot({
el: '#root',
render(h) {
return h('div', { attrs: { id: 'root' } }, [h(component)]);
},
});
showMain();
}

View File

@ -42,7 +42,7 @@ Storyshots is a way to automatically jest-snapshot all your stories. [More info
Redirects console output (logs, errors, warnings) into Action Logger Panel. `withConsole` decorator notifies from what stories logs are coming.
### [Backgrounds](https://github.com/storybooks/storybook/tree/master/addons/background)
### [Backgrounds](https://github.com/storybooks/storybook/tree/master/addons/backgrounds)
With this addon, you can switch between background colors and background images for your preview components. It is really helpful for styleguides.

View File

@ -32,7 +32,7 @@ Now when you are writing a story it like this and add some notes:
```js
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { WithNotes } from '@storybook/addon-notes';
import { withNotes } from '@storybook/addon-notes';
import Button from './Button';

View File

@ -0,0 +1,111 @@
---
id: 'guide-html'
title: 'Storybook for HTML'
---
You may have tried to use our quick start guide to setup your project for Storybook. If you want to set up Storybook manually, this is the guide for you.
> This will also help you to understand how Storybook works.
## Starter Guide HTML
Storybook has its own Webpack setup and a dev server.
In this guide, we will set up Storybook for your HTML project.
## Table of contents
- [Add @storybook/html](#add-storybookhtml)
- [Add babel-runtime and babel-core](#add-babel-runtime-and-babel-core)
- [Create the config file](#create-the-config-file)
- [Write your stories](#write-your-stories)
- [Run your Storybook](#run-your-storybook)
## Add @storybook/html
First of all, you need to add `@storybook/html` to your project. To do that, simply run:
```sh
npm i --save-dev @storybook/html
```
If you don't have `package.json` in your project, you'll need to init it first:
```sh
npm init
```
## Add babel-runtime and babel-core
Make sure that you have `babel-runtime` and `babel-core` in your dependencies as well because we list these as a peerDependency:
```sh
npm i --save-dev babel-runtime
npm i --save-dev babel-core
```
Then add the following NPM script to your package json in order to start the storybook later in this guide:
```json
{
"scripts": {
"storybook": "start-storybook -p 9001 -c .storybook"
}
}
```
## Create the config file
Storybook can be configured in several different ways.
Thats why we need a config directory. We've added a `-c` option to the above NPM script mentioning `.storybook` as the config directory.
For the basic Storybook configuration file, you don't need to do much, but simply tell Storybook where to find stories.
To do that, simply create a file at `.storybook/config.js` with the following content:
```js
import { configure } from '@storybook/html';
function loadStories() {
require('../stories/index.js');
// You can require as many stories as you need.
}
configure(loadStories, module);
```
That'll load stories in `../stories/index.js`.
## Write your stories
Now you can write some stories inside the `../stories/index.js` file, like this:
```js
/* global document */
import { storiesOf } from '@storybook/html';
storiesOf('Demo', module)
.add('heading', () => '<h1>Hello World</h1>')
.add('button', () => {
const button = document.createElement('button');
button.innerText = 'Hello Button';
button.addEventListener('click', e => console.log(e));
return button;
});
```
Story is a single HTML snippet or DOM node. In the above case, there are two stories:
1. heading — an HTML snippet
2. button — a DOM node with event listener
## Run your Storybook
Now everything is ready. Simply run your storybook with:
```sh
npm run storybook
```
Now you can change components and write stories whenever you need to.

View File

@ -12,6 +12,7 @@ title: 'Live Examples'
- [Polymer](https://storybooks-polymer.netlify.com/)
- [Mithril](https://storybooks-mithril.netlify.com/)
- [Marko](https://storybooks-marko.netlify.com/)
- [HTML](https://storybooks-html.netlify.com/)
### 3.4
- [React Official](https://release-3-4--storybooks-official.netlify.com)

View File

@ -13,6 +13,11 @@ getstorybook
```
The `-g` global install is used to run our cli tool in your project directory to generate templates for your existing projects. To avoid the global install and start your project manually, take a look at our [Slow Start Guide](/basics/slow-start-guide/).
To install storybook for HTML, add `--html` argument:
```
getstorybook --html
```
This will configure your app for Storybook. After that, you can run your Storybook with:
```sh
@ -23,6 +28,12 @@ Then you can access your storybook from the browser.
* * *
To learn more about what `getstorybook` command does, have a look at our [Start Guide for React](/basics/guide-react/) or [Start Guide for Vue](/basics/guide-vue/) or [Start Guide for Angular](/basics/guide-angular/) or [Start Guide for Mithril](/basics/guide-mithril/).
To learn more about what `getstorybook` command does, have a look at our slow start guides:
* [React](/basics/guide-react/)
* [Vue](/basics/guide-vue/)
* [Angular](/basics/guide-angular/)
* [Mithril](/basics/guide-mithril/)
* [HTML](/basics/guide-html/)
If you prefer a guided tutorial to reading docs, head to [Learn Storybook](https://www.learnstorybook.com) for a step-by-step guide (currently React-only).

View File

@ -10,3 +10,4 @@ Storybook supports multiple UI libraries. The manual setup for each is different
- [Storybook for Angular](/basics/guide-angular/)
- [Storybook for Mithril](/basics/guide-mithril/)
- [Storybook for Marko](/basics/guide-marko/)
- [Storybook for HTML](/basics/guide-html/)

View File

@ -4,39 +4,28 @@ import 'jasmine';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(
async(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
}).compileComponents();
})
);
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
}).compileComponents();
}));
it(
'should create the app',
async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
})
);
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(
`should have as title 'app'`,
async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
})
);
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it(
'should render title in a h1 tag',
async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
})
);
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});

View File

@ -0,0 +1,11 @@
import '@storybook/addon-a11y/register';
import '@storybook/addon-actions/register';
import '@storybook/addon-backgrounds/register';
import '@storybook/addon-events/register';
import '@storybook/addon-jest/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-links/register';
import '@storybook/addon-notes/register';
import '@storybook/addon-options/register';
import '@storybook/addon-storysource/register';
import '@storybook/addon-viewport/register';

View File

@ -0,0 +1,16 @@
import { configure } from '@storybook/html';
import { setOptions } from '@storybook/addon-options';
setOptions({
hierarchyRootSeparator: /\|/,
});
// automatically import all files ending in *.stories.js
const req = require.context('../stories', true, /.stories.js$/);
function loadStories() {
// Make welcome story default
require('../stories/index.stories');
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);

View File

@ -0,0 +1,12 @@
const path = require('path');
module.exports = (storybookBaseConfig, configType, defaultConfig) => {
defaultConfig.module.rules.push({
test: [/\.stories\.js$/, /index\.js$/],
loaders: [require.resolve('@storybook/addon-storysource/loader')],
include: [path.resolve(__dirname, '../stories')],
enforce: 'pre',
});
return defaultConfig;
};

View File

@ -0,0 +1,40 @@
{
"name": "html-kitchen-sink",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate-addon-jest-testresults": "jest --config=tests/addon-jest.config.json --json --outputFile=stories/addon-jest.testresults.json",
"storybook": "start-storybook -p 9006",
"build-storybook": "build-storybook"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"@storybook/addons": "^4.0.0-alpha.3",
"@storybook/addon-a11y": "^4.0.0-alpha.3",
"@storybook/addon-actions": "^4.0.0-alpha.3",
"@storybook/addon-backgrounds": "^4.0.0-alpha.3",
"@storybook/addon-centered": "^4.0.0-alpha.3",
"@storybook/addon-events": "^4.0.0-alpha.3",
"@storybook/addon-jest": "^4.0.0-alpha.3",
"@storybook/addon-knobs": "^4.0.0-alpha.3",
"@storybook/addon-links": "^4.0.0-alpha.3",
"@storybook/addon-notes": "^4.0.0-alpha.3",
"@storybook/addon-options": "^4.0.0-alpha.3",
"@storybook/addon-storyshots": "^4.0.0-alpha.3",
"@storybook/addon-storysource": "^4.0.0-alpha.3",
"@storybook/addon-viewport": "^4.0.0-alpha.3",
"@storybook/core": "^4.0.0-alpha.3",
"@storybook/core-events": "^4.0.0-alpha.3",
"@storybook/html": "^4.0.0-alpha.3",
"babel-core": "^6.26.0",
"babel-runtime": "^6.26.0",
"eventemitter3": "^3.1.0",
"format-json": "^1.0.3",
"global": "^4.3.2",
"jest": "^22.4.3"
}
}

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons|a11y Default 1`] = `<button />`;
exports[`Storyshots Addons|a11y Delayed render 1`] = `<div />`;
exports[`Storyshots Addons|a11y Disabled 1`] = `
<button
disabled=""
>
Testing the a11y addon
</button>
`;
exports[`Storyshots Addons|a11y Invalid contrast 1`] = `
<button
style="color: black; background-color: brown;"
>
Testing the a11y addon
</button>
`;
exports[`Storyshots Addons|a11y Label 1`] = `
<button>
Testing the a11y addon
</button>
`;

View File

@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons|Actions Decorated actions + config 1`] = `
<button
type="button"
>
Hello World
</button>
`;
exports[`Storyshots Addons|Actions Decorated actions 1`] = `
<button
type="button"
>
Hello World
</button>
`;
exports[`Storyshots Addons|Actions Hello World 1`] = `
<button
type="button"
>
Hello World
</button>
`;
exports[`Storyshots Addons|Actions Multiple actions + config 1`] = `
<button
type="button"
>
Hello World
</button>
`;
exports[`Storyshots Addons|Actions Multiple actions 1`] = `
<button
type="button"
>
Hello World
</button>
`;
exports[`Storyshots Addons|Actions Multiple actions, object + config 1`] = `
<button
type="button"
>
Hello World
</button>
`;
exports[`Storyshots Addons|Actions Multiple actions, object 1`] = `
<button
type="button"
>
Hello World
</button>
`;
exports[`Storyshots Addons|Actions Multiple actions, selector 1`] = `
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons|Backgrounds story 1 1`] = `
<span
style="color: white"
>
You should be able to switch backgrounds for this story
</span>
`;
exports[`Storyshots Addons|Backgrounds story 2 1`] = `
<span
style="color: white"
>
This one too!
</span>
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons|Centered button in center 1`] = `
<div
id="sb-addon-centered-wrapper"
style="position: fixed; top: 0px; left: 0px; bottom: 0px; right: 0px; display: flex; overflow: auto;"
>
<div
id="sb-addon-centered-inner"
style="margin: auto;"
>
<button>
I am a Button !
</button>
</div>
</div>
`;

View File

@ -0,0 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons|Events Logger 1`] = `
`;

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons|jest withTests 1`] = `This story shows test results`;

Some files were not shown because too many files have changed in this diff Show More